全面了解Kotlin,2021大厂Android高级面试题及答案

Posted m0_66264856

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全面了解Kotlin,2021大厂Android高级面试题及答案相关的知识,希望对你有一定的参考价值。

val str : String = “test”

根据上图,我们需要注意两点:

2.1.val等于final

我们可以看到var定义的num可以被重新赋值,str却不可以。上图的val实际上就等于Java中的final String,也就是val定义的变量默认添加了final关键字。

2.2.可空?以及空匹配

第二点就是num变量在定义为Int的时候是不能赋值为Null的,如果需要我们需要这么定义

var num : Int?

2.3.类型推断

val str = “”

kotlin具有类型推断功能,上面的语句等于Java

final String str = “”

自定义getter&setter

var msg: String
get() = field
set(value)
field = value

3.函数

3.1.定义

3.1.1普通函数

定义一个名为test函数,返回值为String?,可能返回为空

fun test(): String?
return null

调用:

调用和Java类似,由于test返回的是可空的字符串,添加?:表示当前面为空取冒号后面的值。

val result = test() ?: “x”

也可以

fun isEmpty(str: String) = str.isEmpty()

其中isEmpty函数的返回值为后面isEmpty()的返回值。

3.1.2默认参数

kotlin支持带默认参数的函数,默认参数不传则为默认值。

data class EnvConfig(val baseUrl: String, val isDebug: Boolean = false)

//构造1 等于 EnvConfig(“https://xx.com”,false)
val env1 = EnvConfig(“https://xx.com”)

3.1.3命名参数

kotlin方法调用可以指明参数名称,以避免混淆。更加直观。

EnvConfig(
baseUrl = “https://xx2.com”,
isDebug = true
)

3.2顶层函数和属性

kotlin可以定义全局可以调用的工具函数,它会编译成该文件的静态方法以供调用。

TopFunc.kt

fun toString(obj:Any) = obj.toString()

翻译成Java类

public final class TopFuncKt
@NotNull
public static final String toString(@NotNull Object obj)
Intrinsics.checkParameterIsNotNull(obj, “obj”);
return obj.toString();

同理顶层属性

var count = 0

Java

public static final int getCount()
return count;

public static final void setCount(int var0)
count = var0;

3.3 给别人的类添加方法: 拓展函数和属性

3.3.1拓展函数

拓展函数非常简单,它就是一个类的成员函数。

TopFunc.kt

//定义一个成员函数 方法内的this会指向被拓展的对象。
//即这里的this是这个字符串
fun String.print() = println(this)

//使用
“string extension”.print()

//输出
string extension

拓展函数也是顶层函数,所以它在Java中也是静态函数,调用如下:

TopFuncKt.print(“extension in Java”);

拓展函数仅仅是为了缩短语法而存在。并非真正意义上的"拓展",也做不到真正的拓展,所以拓展函数无法进行重写,或者在Java中当作成员函数来调用。

3.3.2拓展属性

类似拓展函数,拓展属性提供了一种方法,用来拓展类的API,可以用来访问属性,用的是属性语法而不是函数的语法。**尽管它们被称为属性,但他们可以没有任何状态,也没有合适的地方来存储它们,**不能给现有的Java对象的实例添加额外的字段。

var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char)
setCharAt(length - 1, value)

**上面的代码只是利用拓展属性提供了一种快捷访问该类成员方法的途径,但是并没有给StringBuilder这个类添加lastChar这个属性。**尽管如此,拓展属性依旧十分实用,比如android中常用的Float转换为dp:

val Float.dp
get() = get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)

3.4 可变参数和中缀调用

vararg:可变参数。只需要注意一点

fun test(vararg string: String)
test2(string)//这里传递可变参数需要添加

fun test2(vararg string: String)

中缀调用:

//允许使用中缀符号调用函数,函数会返回一个Pair对象
infix fun Any.with(other:Any) = Pair(this,other)

val c = “3” with 4 // c为pair对象
val (f, s) = “3” with 4 //val (f,s)称为解构声明,将Pair的first给f,second给s.
println(“f: f , s : f,s: f,s:s”)

//输出结果
f:3,s:4

4.程序逻辑控制

4.1带返回值的if

kotlin没有三目运算符,取而代之的是带返回值的if语句。而且是把每一个条件中的最后一行代码作为返回值

//Kotlin 中把每一个条件中的最后一行代码作为返回值
fun largeNumber(number1: Int,number2: Int) : Int
return if(number1 > number2)
number1
else
number2

//根据上面学习的语法糖和 Kotlin 类型推导机制,我们还可以简写 largeNumber 函数,最终变成了这样
fun largeNumber(number1: Int,number2: Int) = if(number1 > number2) number1 else number 2

4.2带返回值的when

类比 Java 中的 Switch 语句学习,Java 中的 Switch 并不怎么好用:

①Switch 语句只能支持一些特定的类型,如整型,短于整型,字符串,枚举类型。如果我们使用的并非这几种类型,Switch 并不可用

②Switch 语句的 case 条件都要在最后加上一个 break

这些问题在 Kotlin 中都得到了解决,而且 Kotlin 还加入了许多强大的新特性:

when 条件语句也是有返回值的,和 if 条件语句类似,条件中的最后一行代码作为返回值

when 条件语句允许传入任意类型的参数

when 条件体中条件格式:匹配值 -> 执行逻辑

⑥when 条件语句和 if 条件语句一样,当条件体里面只有一行代码的时候,条件体的 可省略

//when 中有参数的情况
fun getScore(name: String) = when (name)
“tom” -> 99
“jim” -> 80
“lucy” -> 70
else -> 0

//when 中无参数的情况,Kotin 中判断字符串或者对象是否相等,直接使用 == 操作符即可
fun getScore(name: String) = when
name == “tom” -> 99
name == “jim” -> 80
name ==“lucy” -> 70
else -> 0

4.3循环语句

kotlin中主要有两种循环语句:while和for-in。其中while和Java中使用一致。

而kotlin中的for-in则比Java中更为方便易用。

for-in:

//使用 … 表示创建两端都是闭区间的升序区间[0,10]
for (i in 0…10)
print("$i ")

//打印结果
0 1 2 3 4 5 6 7 8 9 10

for-until:

//使用 until 表示创建左端是闭区间右端是开区间的升序区间[0,10)
for (i in 0 until 10)
print("$i ")

//打印结果
0 1 2 3 4 5 6 7 8 9

for-downTo:

//使用 downTo 表示创建两端都是闭区间的降序区间[10,0]
for (i in 10 downTo 0)
print("$i ")

//打印结果
10 9 8 7 6 5 4 3 2 1 0

步进:

//使用 downTo 表示创建两端都是闭区间的降序区间,每次在跳过3个元素
for (i in 10 downTo 0 step 3)
print("$i ")

迭代list

//使用withIndex迭代list
val list = arrayListOf(“10”,“11”,“100”)
for ((index,element) in list.withIndex())//解构申明
println(“ i n d e x : index: index:element”)

//打印结果
0:10
1:11
2:100

迭代map

val map = mapOf(1 to “Java”, 2 to “Kotlin”, 3 to “Flutter”)//中缀调用
for ((key, value) in map) //解构
println(“ k e y : key: key:value”)

//打印结果
1:Java
2:Kotlin
3:Flutter

5.类

和Java一样,类的定义如下

class Base
var num = 1

但是意义却不太一样。

5.1可见性

**Kotlin中,默认类的可见性为public以及final的。**内部类默认为static的,用inner标记非静态内部类。

①Kotlin 中可见性
  • private :私有,本类内部可见
  • protected :子类可见
  • internal :模块内可见
  • public :默认,公有
②对比 Java
  • private :私有,本类内部可见
  • protected :子类可见
  • default :默认,包内可见
  • public :公有

单个构造函数&私有构造

class Response private constructor(val code: Int, val msg: String)
override fun toString(): String
return “code: c o d e , m s g : code,msg: code,msg:msg”

多个构造函数

//非open不可被继承
class Response
val code: Int
val msg: String

constructor(code: Int)
this.code = code
msg = “”

constructor(code: Int, msg: String)
this.code = code
this.msg = msg

override fun toString(): String
return “code: c o d e , m s g : code,msg: code,msg:msg”

其中code和msg的getter会自动生成。

kotlin中也是单继承多实现,共同存在时,继承类写到第一位,后面追加逗号跟上接口接口。

public class String : Comparable, CharSequence

密封类(private构造,默认open)

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()

5.2Object关键字

①类单例

object SingleTon

@JvmStatic
fun isEmpty(str: String) = str.isEmpty()

反编译成Java

public final class SingleTon
public static final SingleTon INSTANCE;

@JvmStatic
public static final boolean isEmpty(@NotNull String str)
Intrinsics.checkParameterIsNotNull(str, “str”);
CharSequence var1 = (CharSequence)str;
boolean var2 = false;
return var1.length() == 0;

private SingleTon()

static
SingleTon var0 = new SingleTon();
INSTANCE = var0;

根据面可以看出,object单独修饰一个类,表示为静态代码块实现的单例

@JvmStatic修饰的方法,kotlin会在其方法上添加static关键字,而static关键字在kotlin中是不存在的。

②匿名类

test(object : Listener

)
interface Listener

kotlin中没有new关键字,所以匿名类用object实现。

③伴生对象

kotlin没有static,那么如何实现静态变量以及常量等的定义和使用呢?

答案是伴生对象

class UserManager

companion object
val USER_TYPE = 0x01

上面的companion object会生成一个内部类Companion,并添加返回USER_TYPE的静态getter,如下

public final class UserManager
private static final int USER_TYPE = 1;
public static final UserManager.Companion Companion = new UserManager.Companion((DefaultConstructorMarker)null);

public static final class Companion
public final int getUSER_TYPE()
return UserManager.USER_TYPE;



PS:const关键字

const关键字只能用在静态类中, 只能与val连用,即const val,而且只能修饰基本类型。意义为编译期常量,在用到的地方替换为该常量的值。如下:

object SingleTon
const val str = “const”

fun test(): String?
return SingleTon.str

其中
test反编译Java如下:

public final String test()
return “const”;

可以看到kotlin对const常量做了内联

5.3类委托

equals

在Java中,可以使用来比较基本数据类型和引用类型,基本数据类型比较的是值,引用类型上比较的是引用。在kotlin中就等于调用Java中的equals。如果需要比较引用则需要用===。

by关键字

装饰器模式的代码通常就较为模板,kotlin中可以利用by关键字来实现类的委托。比如:

class MyList : List by ArrayList()
//这里面默认利用ArrayList实现了List的所有接口

转换成Java:

public final class MyList implements List, KMappedMarker
// $FF: synthetic field
private final ArrayList KaTeX parse error: Expected '', got 'EOF' at end of input: … return this.delegate_0.get(index);


当然,我们也可以通过重写来实现自己的逻辑。

by也可以用来实现延迟加载:

private val arr by lazy MyList()

它的实现是double-check的懒加载方式,如下:

private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this

override val value: T
get()
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE)
@Suppress(“UNCHECKED_CAST”)
return _v1 as T

return synchronized(lock)
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE)
@Suppress(“UNCHECKED_CAST”) (_v2 as T)
else
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue


override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else “Lazy value not initialized yet.”

private fun writeReplace(): Any = InitializedLazyImpl(value)

6.lambda

6.1语法

lambda表达式,本质上就是可以传递给其他函数的一小段代码。

kotlin中简单lambda:

button.setOnclickListener

kotlin中lambda始终被花括号包围。可以把lambda表达式存储在变量中:

val sum = x:Int,y:Int -> x + y
println(sum(1,2))

3

kotlin中,lambda作为最后一个参数可以把它放到()后面如下1;如果只有lambda作为参数,可以省略(),如下2。

list.maxBy(it.length)
list.maxBy()it.length//1
list.maxByit.length//2

6.2集合函数API

比如list过滤:

val list = arrayListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
list.filter it % 2 == 0
.forEach print(it)

//打印结果
246810

类似的还有:

all:全部匹配返回true

any:存在一个匹配返回true

count:返回匹配的数量

firstOrNull:第一个匹配,没有返回null

first:第一个匹配,没有抛出异常

find:找到第一个匹配 等于 firstOrNull

还有map,flatmap,groupby…

基本涵盖了RxJava的常用操作符。

apply,let,also…

apply改变this指向调用者。方便各种操作,返回调用者

val str = “123456”
val rec = str.apply
println(“lastChar:KaTeX parse error: Expected 'EOF', got '' at position 20: …(lastIndex)") ̲ println("rec:rec”)

//打印结果
lastChar:6
rec:123456

with改变this指向参数,返回lambda最后一行结果

let创建局部变量,返回最后一行结果。

val str :String ? = “123456”
val res = str?.let
println(“it:KaTeX parse error: Expected 'EOF', got '' at position 15: it") "return" ̲ println("res:res”)

//打印结果
it:123456
res:return

also:创建it,返回调用者

run:改变this指向调用者,返回最后一行

类似的语法糖takeIf,repeat等等,都在Standard.kt中有定义。

二、深入理解

1.kotlin类型系统

1.1可空性

kotlin中类型定义如果没有添加为可空,当它接受到一个null时,kotlin会在运行时抛出ERROR:Type mismatch的错误。

当一个类规定为可空,可以使用安全调用?.,后面可以跟上Elvis运算符?:。标识在前面?.调用者为null时执行。

val str :String ? = “123456”
str?.get(str.lastIndex) ?: toast(“str is null”) //toast会在str为null时执行

安全转换:as?

is:检查类型,可以自动转型

val obj: Any = “str”
if (obj is String)
println(“obj:$obj”)//obg自动转型为string

//打印结果
obj:str

as:类型转换,as?安全类型转换

val obj: Any = “str”
(obj as? String)?.print()//在obj为String时才会执行后面的语句,print为本地定义的拓展函数

//打印结果
str

!!非空断言

让kotlin在对象为空的时候抛出异常。

val obj: Any? = “str”
obj!!.toString()
obj.hashCode()//不需要再加!!,kotlin编译器自动检查

2.运算符重载

data class Point(val x: Int, val y: Int)
operator fun plus(other: Point): Point
return Point(x + other.x, y + other.y)

val p1 = Point(1,2)
val p2 = Point(5,6)
val p = p1 + p2
println(“p:$p”)//自动调用toString

//打印结果
p:Point(x=6, y=8)

可用于重载的运算符:

表达式函数名
a * btimes
a / bdiv
a % bmod
a + bplus
a - bminus

PS:位运算也有自己的符号

运算符操作
shl带符号左移
shr带符号右移
ushr无符号右移
and按位与
or按位或
xor按位异或
inv按位取反

3.lambda作为形参

无其他参数:

fun exec(func:()->Unit)
func.invoke()

exec
println(“hello world”)

带其他参数:

fun exec(msg: String,func:(msg:String)->Unit)
func.invoke(msg)

exec(“hello world”) msg->
println(msg)

以上的lambda是作为不能为空的形参。如果为空,需要将其定义用()?包裹。如下:

fun exec(msg: String,func:((msg:String)->Unit)?)
func?.invoke(msg)

lambda作为参数传递虽然好,但是其实现传递的还是对象(匿名类),在每一次调用都会创建一个对象,如何避免这部分开销提升性能?

答案是内联函数。

al p = p1 + p2
println(“p:$p”)//自动调用toString

//打印结果
p:Point(x=6, y=8)

可用于重载的运算符:

表达式函数名
a * btimes
a / bdiv
a % bmod
a + bplus
a - bminus

PS:位运算也有自己的符号

运算符操作
shl带符号左移
shr带符号右移
ushr无符号右移
and按位与
or按位或
xor按位异或
inv按位取反

3.lambda作为形参

无其他参数:

fun exec(func:()->Unit)
func.invoke()

exec
println(“hello world”)

带其他参数:

fun exec(msg: String,func:(msg:String)->Unit)
func.invoke(msg)

exec(“hello world”) msg->
println(msg)

以上的lambda是作为不能为空的形参。如果为空,需要将其定义用()?包裹。如下:

fun exec(msg: String,func:((msg:String)->Unit)?)
func?.invoke(msg)

lambda作为参数传递虽然好,但是其实现传递的还是对象(匿名类),在每一次调用都会创建一个对象,如何避免这部分开销提升性能?

答案是内联函数。

超长文,带你全面了解Kotlin的协程


/   今日科技快讯   /


工信部近日发布了2019年1-11月通信业经济运行情况。数据显示,截至11月底,三家基础电信企业的移动电话用户总数达16亿户,同比增长2.7%,较上年末净增3486万户。其中,4G用户规模为12.76亿户,占移动电话用户的79.7%,占比较上年末提高5.3个百分点。


/   作者简介   /


又到了开心的周五,看完本篇,即将放假,提前祝大家周末愉快!

本篇文章自代码都tm飞了的投稿,全盘分析了Kotlin协程Coroutines,相信对大家有所帮助。同时也感谢作者贡献的精彩文章!


代码都tm飞了的博客地址:
https://me.csdn.net/NJP_NJP


/   理解异步回调本质   /

学习了Kotlin协程之后感觉协程是个可以化腐朽为神奇的东西,但是如果习惯了之前的编程方式,刚接触Kotlin协程的话理解起来还是比较吃力的。这里我总结了自己对于Kotlin协程的理解和学习经验,希望对大家的学习有所帮助。

1. 什么是异步


我记得小学二年级碰到过一个让我受益终身的数学题:
烧开水需要15分钟,洗碗需要5分钟,扫地需要5分钟,请问做完这三件事,总共需要几分钟?从此我做什么事,都事先想想先后顺序,看看可不可以一并去做。

长大后才知道这就是异步的用法,它其实已经渗透到你的生活中。


上面这段话节选自:余叶《代码里的世界观——通往架构师之路》,这段话中揭示了异步的本质。异步意味着同时进行一个以上彼此目的不同的任务。


如果上面三个任务一个一个按部就班地去做的话,你可能总共需要25分钟。但是烧开水的时候并不需要你在一旁一直等待着,如果你利用烧开水的时间去完成洗碗和扫地的任务,你只需要15分钟就可以完成以上三个任务。


超长文,带你全面了解Kotlin的协程


接着试着对着Android(或者其他任何UI开发框架)的线程模型类比一下:

你是主线程。烧开水是个耗时操作(比如网络请求),洗碗和扫地是与视图相关的非耗时操作。洗碗和扫地必须由你本人亲自完成(视图相关的工作只能交给主线程),烧开水可以交给电磁炉完成,你只需要按下电磁炉的开关(可以类比成网络请求的发起)。

没有使用异步也就意味着你在烧开水的时候一直在旁边等待着,无法完成其他工作,这也就意味着Android在等待网络请求的时候主线程阻塞,视图卡顿无法交互,这在Android中当然是不允许的。

所以必须使用异步的方式,你(主线程)在按下电磁炉开关(发起网络请求)之后就继续完成洗碗扫地(视图交互)等其他任务了。


所以异步很好理解,就是同时进行着不同的任务就叫做异步。


2. 为什么需要回调


当你按下电磁炉的按钮,并地利用这段时间完成了扫地的任务,你感到很有成就感。心里想:异步机制真好,原来25分钟的工作如今只需要15分钟就能完成,以后我任何的工作都要异步地完成。

不过你并没有高兴太久,在洗碗时你遇到了麻烦。碗和盘子上沾满了油污,单靠自来水和洗洁精根本搞不定,这时你想到了别人教你的方法,常温的水洗不掉的油污用热水就可以轻松洗掉。

但是你发现暖水瓶是空的,而放在电磁炉上的水刚烧了5分钟,还不够热,也就是说你必须等待水烧开了才能开始洗碗。


这时你不禁陷入了思考:异步机制真的是万能的吗?对于有前后依赖关系的任务,异步该如何处理呢?这段等待烧水的时间我可以去做其他工作吗?我怎么确定水什么时候才能烧开呢?

这时,你眼前一亮:你发现了买水壶时赠送的一个配件,那是一个汽笛,它可以在水烧开的时候发出鸣叫声。听到了汽笛声你就可以知道水烧开了,接着就可以用刚烧开的热水来刷碗,并且烧水的过程中你仍然可以去完成其他工作(比如看技术博客),而不用担心烧水的问题。这个汽笛就可以看成异步中的回调机制。


超长文,带你全面了解Kotlin的协程


同样地我们来类比一下Android开发中的场景:

洗碗(渲染某个视图)依赖于烧开水(网络请求)这个耗时操作的结果才能进行,所以你(主线程)在按下电磁炉开关(发起网络请求)的时候,为水壶装上了汽笛(为网络请求配置了回调),以便在水烧开(网络请求完成)的时候,汽笛发出鸣叫(回调函数被调用)你(主线程)就可以继续用烧开的水(网络请求的结果)洗碗(渲染某个视图)了,而等待水烧开(等待网络请求结果)的时候还可以去看技术博客(视图渲染与交互)。

这在Android开发过程中几乎是基础得不能再基础的应用场景了,可以说几乎所有的Android应用程序都有这样的一个过程。


所以理解为什么需要回调也很简单:因为不同的任务之间存在前后的依赖关系。


3. 回调的缺点


以上的应用场景相对简单,回调处理起来也游刃有余,可以描述为以下代码:


  
    
    
  
//烧2000mL热水来洗碗
boilWater(2000) { water ->
     washDishes(water)           
}


但函数回调也有缺陷,就是代码结构过分耦合,遇到多重函数回调的嵌套,代码难以维护。

比如客户端顺序进行多次网络异步请求:

  
    
    
  
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(parameter) { value1 ->
    request2(value1) { value2 ->
        request3(value2) { value3 ->
            updateUI(value3)            
        } 
    }              
}


这种结构的代码无论是阅读起来还是维护起来都是极其糟糕的。对多个回调组成的嵌套耦合,我亲切地称为“回调地狱(Callback Hell)”。

解决回调地狱的方案有很多,其中比较常见的有:链式调用结构。例如:

  
    
    
  
request1(parameter)
    .map { value1 ->
         request2(value1)
       }.map { value2 ->
        request3(value2)
       }.subscribe { value3 ->
        updateUI(value3)
       }


上面的代码看起来就舒服多了,这就是链式调用结构的魅力。实现链式调用结构的常见方式就是使用RxJava,RxJava是一个强大的工具,它是反应函数式编程在Java中的实现,我们可以通过RxJava中的“流”来构建链式调用结构。

虽然RxJava足够强大,但是它也足够复杂,RxJava中“流”的创建、转化与消费都需要使用到它提供的各种类和丰富的操作符,所以要想对RxJava运用自如就需要对这些类和操作符非常熟悉,这也加大了RxJava的学习成本了。


我们可以链式调用结构中获得一些启发,虽然回调嵌套和链式调用在代码结构上完全不一样,但是其表达的东西完全一致。也就是说回调嵌套和链式调用者两种结构表达的都是同一种逻辑,这不禁让我们想对于回调的本质做一些深入思考,究竟回调的背后是什么东西呢?


4. 深入理解异步回调(重点)

在接触多线程编程之前,我们天真地认为代码就是从上到下一行一行地执行的。代码中的基本结构只有三种:顺序、分支、循环,顺序结构中写在上面的代码就是比写在下面的代码先执行,写在下面的代码就是要等到上面的代码执行完了才能得到执行。

但接触到了多线程和并发之后,我们之前在脑袋里建立的秩序的世界完全崩塌了,取而代之的是一个混沌的世界。代码的执行顺序好像完全失控了,可能有多处的代码一起执行,写在下面的代码也可能先于上面的执行。


举个简单的例子:

  
    
    
  
//1.简单秩序的串行世界:
print("Hello ")
print("World!")
//结果为:Hello World!

//2.复杂混沌的并行世界:
Thread { 
    Thread.sleep(2000)
    print("Hello "
}.start()
print("World!")


那么我们思考一下在串行的世界里,由回调组织起来的代码结构属于顺序、分支、循环哪种呢?应该不难发现:烧完水再洗碗,网络请求成功再更新UI,这些看似复杂的回调结构其实表达的就是一种代码的顺序执行的方式。


回过头来看看之前提到的回调嵌套的例子,如果放在简单的串行世界里代码其实完全可以写成这样:

  
    
    
  
val value1 = request1(parameter)
val value2 = request2(value1)
val value3 = request2(value2)
updateUI(value3)


上面代码的执行顺序与下面的回调方式组织代码的执行顺序完全相同:


  
    
    
  
request1(parameter) { value1 ->
    request2(value1) { value2 ->
        request3(value2) { value3 ->
            updateUI(value3)            
        } 
    }              
}


既然代码执行顺序完全一致为什么我们还要使用回调这么麻烦的方式来顺序执行代码呢?原因就在于我们的世界不是简单的串行世界,实际的程序也不是只有一个线程那么简单的。


顺序代码结构是阻塞式的,每一行代码的执行都会使线程阻塞在那里,但是主线程的阻塞会导致很严重的问题,所以也就决定了所有的耗时操作不能在主线程中执行,所以就需要多线程来执行。


对于上面的例子,虽然代码执行顺序是:request1 -> request2 -> request3 -> updateUI。

超长文,带你全面了解Kotlin的协程


但是他们是有可能工作在不同的线程上的,比如:request1(work thread) -> request2(work thread) -> request3(work thread) -> updateUI(main thread)。也就是说虽然代码确实是顺序执行的,但其实是在不同的线程上顺序执行的。

超长文,带你全面了解Kotlin的协程


通常线程切换的工作是由异步函数内部完成的,通过回调的方式异步调用外界注入的代码。也就是说:异步回调其实就是代码的多线程顺序执行。


那么能不能既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行呢?有没有一个东西可以帮助我自动地完成线程的切换工作呢?答案当然是肯定的,接下来就轮到Kotlin协程大显身手的时候了。

/   Coroutines初体验   /


1. 添加依赖


Kotlin协程不属于Kotlin语言本身,使用之前必须手动引入。在Android平台上使用可以添加Gradle依赖:

  
    
    
  
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'


2. 启动协程


首先看下如下代码:


  
    
    
  
GlobalScope.launch {
    delay(1000L)    
    println("Hello,World!")
}


上述代码使用launch方法启动了一个协程,launch后面的花括号就是协程,花括号内的代码就是运行在协程内的代码。

接着来深入了解一下launch方法的声明:

  
    
    
  
public fun CoroutineScope.launch(    
    context: CoroutineContext = EmptyCoroutineContext,    
    start: CoroutineStart = CoroutineStart.DEFAULT,    
    block: suspend CoroutineScope.()
 -> Unit): Job {...}


可以看到launch方法是CoroutineScope的拓展方法,也就是说我们启动协程要在一个指定的CoroutineScope上来启动。

CoroutineScope翻译过来就是“协程范围”,指的是协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行,上面第一段代码中的GlobalScope指的是与应用进程相同的协程范围,也就是在进程没有结束之前协程内的代码都可以运行。

除此之外为了方便我们的使用,在Google的Jetpack中也提供了一些生命周期感知型协程范围。实际开发中我们可以方便地选择适当的协程范围来为耗时操作(网络请求等)指定自动取消执行的时机,详情见:https://developer.android.google.cn/topic/libraries/architecture/coroutines


接着可以看下launch方法的其他参数:


  1. context:协程上下文,可以指定协程运行的线程。默认与指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默认运行在一个后台工作线程内。也可以通过显示指定参数来更改协程运行的线程,Dispatchers提供了几个值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。

  2. start:协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT是指协程立即执行,除此之外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。

  3. block:协程主体。也就是要在协程内部运行的代码,可以通过lamda表达式的方式方便的编写协程内运行的代码。

  4. CoroutineExceptionHandler:除此之外还可以指定CoroutineExceptionHandler来处理协程内部的异常。

返回值Job:对当前创建的协程的引用。可以通过Job的start、cancel、join等方法来控制协程的启动和取消。


启动协程不是只有launch一个方法的,还有async等其他方法可以启动协程,不过launch是最常用的一种方法,其他的方法大家可以去自行了解。

3. 调用挂起函数


回到上面的代码:

  
    
    
  
println("Start")
GlobalScope.launch(Dispatchers.Main) {
    delay(1000L)
    println("Hello World")
}
println("End")


首先通过GlobalScope.launch启动了一个协程,这里指定协程运行的线程为主线程,接着协程内只有两行代码,协程启动之后就立即执行。首先直接输出了"Start"和"End",接着1秒钟后又输出了"Hello World"。这结果看起来看似顺理成章,因为我们使用非常相似的Thread相关的代码也完全可以实现以上代码的效果:


  
    
    
  
println("Start")
Thread {
    Thread.sleep(1000L)
    println("Hello World")
}.start()
println("End")


两段代码看起来长得几乎一模一样,运行结果也完全一致。那究竟协程的神奇之处在哪里呢?顺序编写异步代码有体现在什么地方呢?

我们在上面两段代码的所有输出的位置上全部加上输出当前线程名的操作:

  
    
    
  
//协程代码
println("Start ${Thread.currentThread().name}")
GlobalScope.launch(Dispatchers.Main) {
    delay(1000L)
    println("Hello World ${Thread.currentThread().name}")
}
println("End ${Thread.currentThread().name}")


  
    
    
  
//线程代码
println("Start ${Thread.currentThread().name}")
Thread {
    Thread.sleep(1000L)
    println("Hello World ${Thread.currentThread().name}")
}.start()
println("End ${Thread.currentThread().name}")


线程代码输出为:“Start main”->“End main”->“Hello World Thread-2”。这个结果也很好理解,首先在主线程里输出"Start",接着创建了一个新的线程并启动后阻塞一秒,这时主线程继续向下执行输出"End",这时启动的线程阻塞时间结束,在当前创建的线程输出"Hello World"。


协程代码输出为:“Start main”->“End main”->“Hello World main”。前两个输出很好理解与上面一致,但是等待一秒之后协程里面的输出结果却显示当前输出的线程为主线程!

这是个很神奇的事情,输出"Start"之后就立即输出了"End"说明了我们的主线程并没有被阻塞,等待的那一秒钟被阻塞的一定是其他线程。

但是阻塞结束后的输出却发生在主线程中,这说明了一件事:协程中的代码自动地切换到其他线程之后又自动地切换回了主线程!这不正是我们一直想要的效果吗?

超长文,带你全面了解Kotlin的协程


还记得上一章中说到的吗?这个例子中delay和println两行代码紧密地写在协程之中,他们的执行也严格按照从上到下一行一行地顺序执行,但是这两行的代码却运行在完全不同的两个线程中,这就是我们想要的“既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行”的“顺序编写异步代码的效果”。顺序编写保证了逻辑上的直观性,协程的自动线程切换又保证了代码的非阻塞性。

超长文,带你全面了解Kotlin的协程


那为什么协程中的delay函数没有在主线程中执行呢?而且执行完毕为什么还会自动地切回主线程呢?这是怎么做到的呢?我们可以来看一下delay函数的定义:


  
    
    
  
public suspend fun delay(timeMillis: Long) {...}


可以发现这个函数与正常的函数相比前面多了一个suspend关键字,这个关键字翻译过来就是“挂起”的意思,suspend关键字修饰的函数也就叫“挂起函数”。

关于挂起函数有个规定:挂起函数必须在协程或者其他挂起函数中被调用,换句话说就是挂起函数必须直接或者间接地在协程中执行。


关于挂起的概念大家不要理解错了,挂起的不是线程而是协程。遇到了挂起函数,协程所在的线程不会挂起也不会阻塞,但是协程被挂起了,就是说协程被挂起时当前协程与它所运行在的线程脱钩了。

线程继续执行其他代码去了,而协程被挂起等待着,等待着将来线程继续回来执行自己的代码。也就是协程中的代码对线程来说是非阻塞的,但是对协程自己本身来说是阻塞的。换句话说,协程的挂起阻塞的不是线程而是协程。


超长文,带你全面了解Kotlin的协程


所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的重新代码进入协程所在线程的过程。协程就是通过的这个挂起恢复机制进行线程的切换。


4. 线程切换


既然协程执行到了挂起函数会被挂起,那么是suspend关键字进行的线程切换吗?怎么指定切换到哪个线程呢?对此我们可以做一个简单的试验:

  
    
    
  
GlobalScope.launch(Dispatchers.Main) {
    println("Hello ${Thread.currentThread().name}")    
    test()
    println("End ${Thread.currentThread().name}")
}

suspend fun test(){
    println("World ${Thread.currentThread().name}")
}


执行结果为:Hello main -> World main -> End main,也就是说这个suspend函数仍然运行在主线程中,suspend并没有切换线程的作用。

实际上我们可以withContext方法来在suspend函数中进行线程的切换:

  
    
    
  
GlobalScope.launch(Dispatchers.Main) {
    println("Hello ${Thread.currentThread().name}")    
    test()
    println("End ${Thread.currentThread().name}")
}

suspend fun test(){
   withContext(Dispatchers.IO){
        println("World ${Thread.currentThread().name}")
   }
}


执行的结果为:Hello main -> World DefaultDispatcher-worker-1 -> End main,这说明我们的suspend函数的确运行在不同的线程之中了。就是说实际是上withContext方法进行的线程切换的工作,那么suspend关键字有什么用处呢?


其实,忽略原理只从使用上来讲,suspend关键字只起到了标志这个函数是一个耗时操作,必须放在协程中执行的作用。关于线程切换其实还有其他方法,但是withContext是最常用的一个,其他的如感兴趣可以自行了解。


5. 顺序执行与并发执行

5.1 顺序执行

这是上一章中演示回调地狱的代码:

  
    
    
  
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(parameter) { value1 ->
    request2(value1) { value2 ->
        request3(value2) { value3 ->
            updateUI(value3)            
        } 
    }              
}


超长文,带你全面了解Kotlin的协程


我们试着用刚刚学到的协程的方式来改进这个代码:


  
    
    
  
//用协程改造回调代码
GlobalScope.launch(Dispatchers.Main) {
    //三次请求顺序执行
    val value1 = request1(parameter)
    val value2 = request2(value1)
    val value3 = request2(value2)
    //用最终结果更新UI
    updateUI(value3)
}

//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}


前提是request相关的API已经改造成了适应协程的方式,并在内部进行了线程切换。这样代码看起来是不是整洁多了?没有了烦人的嵌套,所有的逻辑都体现在了代码的先后顺序上了,是不是一目了然呢?


5.2 并发执行


那么接下来实现一些有挑战性的东西:如果三次网络请求并不存在前后的依赖关系,也就是说三次请求要并发进行,但是最终更新UI要将三次请求的结果汇总才可以。这样的需求如果没有RxJava或Kotlin协程这种强大的工具支持,单靠自己编码实现的确是一个痛苦的过程。

超长文,带你全面了解Kotlin的协程


不过Kotlin协程提供了一种简单的方案:async await方法。


  
    
    
  
//并发请求
GlobalScope.launch(Dispatchers.Main) {
    //三次请求并发进行
    val value1 = async { request1(parameter1) }
    val value2 = async { request2(parameter2) }
    val value3 = async { request3(parameter3) }
    //所有结果全部返回后更新UI
    updateUI(value1.await(), value2.await(), value3.await())
}

//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}


上面的代码中我们用async方法包裹执行了suspend方法,接着在用到结果的时候使用了await方法来获取请求结果,这样三次请求就是并发进行的,而且三次请求的结果都返回之后就会切回主线程来更新UI。


5.3 复杂业务逻辑


实际开发遇到了串行与并行混合的复杂业务逻辑,那么我们当然也可以混合使用上面介绍的方法来编写对应的代码。比如这样的业务逻辑:request2和request3都依赖于request1的请求结果才能进行,request2和request3要并发进行,更新UI依赖request2和request3的请求结果。

超长文,带你全面了解Kotlin的协程


这样的复杂业务逻辑,如果自己实现是不是感觉要被逼疯?来看看Kotlin协程给出的方案:


  
    
    
  
//复杂业务逻辑的Kotlin协程实现
GlobalScope.launch(Dispatchers.Main) {
    //首先拿到request1的请求结果
    val value1 = request1(parameter1)
    //将request1的请求结果用于request2和request3两个请求的并发进行
    val value2 = async { request2(value1) }
    val value3 = async { request2(value1) }
    //用request2和request3两个请求结果更新UI
    updateUI(value2.await(), value3.await())
}

//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}


怎么样?发现没有,无论怎样的复杂业务逻辑,用Kotlin协程表达出来始终是从上到下整齐排列的四行代码,无任何耦合嵌套,有没有从中感受到Kotlin协程的这股化腐朽为神奇的神秘力量。


了解了Kotlin协程的用法之后,是不是迫不及待地想要在实际Android项目中使用它了?接下来我们来在项目中使用Kotlin协程的最佳实践。


/   理解异步回调本质   /


我们在前两章中讲解了Kotlin协程的基本用法和所解决的关键性问题,接下来让我们来看看在实际项目中该怎么使用Kotlin协程这一利器呢。接下来一起来将Kotlin协程与Jetpack中的架构组件结合起来搭建个简单的项目吧。以下是此Demo的效果:

超长文,带你全面了解Kotlin的协程


可以看到功能其实很简单,界面由一个按钮和三个图片组成。每次按下刷新按钮,就都会从网络上获取三张图片显示到界面上。从网络上获取图片的时候刷新按钮变为不可用状态,刷新完成后按钮恢复可用状态。

1. 添加依赖


  
    
    
  
//添加Retrofit网络库和gsonConverter的依赖,注意一定要2.6.0版本以上
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
//添加Jetpack中架构组件的依赖,注意viewmodel要添加viewmodel-ktx的依赖
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
//添加Glide的依赖用于图片加载
implementation 'com.github.bumptech.glide:glide:4.10.0'


这里需要注意的是retrofit版本要求2.6.0以上,因为2.6.0以上的retrofit对于Kotlin协程提供了不错的支持,用起来也更方便。另外添加ViewModel的依赖一定要添加Kotlin版本的,因为这个版本为我们提供了viewModelScope这个协程范围的支持,这让我们可以方便地将生命周期管理和网络请求自动取消的任务交给它。


2. 编写UI界面


  
    
    
  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_marginTop="10dp"
    tools:context=".MainActivity">


    <Button
        android:id="@+id/button"
        android:text="refresh"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>


    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scaleType="centerCrop"
        android:layout_marginTop="10dp"/>


    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scaleType="centerCrop"
        android:layout_marginTop="10dp"/>


    <ImageView
        android:id="@+id/imageView3"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scaleType="centerCrop"
        android:layout_marginTop="10dp"/>

</LinearLayout>


界面没什么可说的,从上到下垂直排列的一个按钮和三个图片,一个LinearLayout全部搞定。

3. 编写网络层接口


首先来看一下我们要使用到的搜狗美图的api接口:https://api.ooopn.com/image/sogou/api.php?type=json


此接口每次随机返回一张图片的url地址,返回数据格式为:

  
    
    
  
{
    "code""200",
    "imgurl""https://img02.sogoucdn.com/app/a/100520113/20140811192414"
}


数据格式很简单,我们可以很容易地创建出对应的实体类:

  
    
    
  
data class ImageDataResponseBody(
    val code: String,
    val imgurl: String
)


接着我们可以先创建个network包来存放网络层相关的代码:

超长文,带你全面了解Kotlin的协程


ApiService为我们网络接口的访问单例类,NetworkService为定义的网络接口:

  
    
    
  
import com.njp.coroutinesdemo.bean.ImageDataResponseBody
import retrofit2.http.GET
import retrofit2.http.Query

//网络接口
interface ApiService {

    //声明为suspend方法
    @GET("image/sogou/api.php")
    suspend fun getImage(@Query("type") type: String = "json"): ImageDataResponseBody
}


  
    
    
  
import com.njp.coroutinesdemo.bean.ImageDataResponseBody
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.util.concurrent.TimeUnit

//网络层访问统一入口
object NetworkService {

    //retorfit实例,在这里做一些统一网络配置,如添加转换器、设置超时时间等
    private val retrofit = Retrofit.Builder()
        .client(OkHttpClient.Builder().callTimeout(5, TimeUnit.SECONDS).build())
        .baseUrl("https://api.ooopn.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    //网络层访问服务
    val apiService = retrofit.create<ApiService>()

}


值得注意的是我们在定义我们的接口的时候一定要声明为suspend方法,这样就完成了对Kotlin协程的完美支持了,怎么样,是不是很简单呢?


4. 编写ViewModel和View层代码 

首先由于我们的项目中要对网络加载的状态进行监听,以此来进行对刷新按钮是否可点击状态的设置和错误信息的显示。所以我们可以编写一个LoadState类来作为网络加载状态信息的承载:


  
    
    
  
sealed class LoadState(val msg: String) {
    class Loading(msg: String = "") : LoadState(msg)
    class Success(msg: String = "") : LoadState(msg)
    class Fail(msg: String) : LoadState(msg)
}


这里使用了sealed类,sealed类是一种特殊的父类,它只允许内部继承,所以在与when表达式合用来判断状态时很适合。其中Fail状态必须指定错误信息,其他的状态信息可为空。我们可以将其与ImageDataResponseBody一起放在新建的bean包下:

超长文,带你全面了解Kotlin的协程


接着我们来创建我们的ViewModel:

  
    
    
  
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.xxx.coroutinesdemo.bean.LoadState
import com.xxx.coroutinesdemo.network.NetworkService

class MainViewModel : ViewModel() {

    //存放三张图片的url数据
    val imageData = MutableLiveData<List<String>>()
    //存放网路加载状态信息
    val loadState = MutableLiveData<LoadState>()

    //从网络加载数据
    fun getData() {...}

}


在其中放了两个LiveData作为数据,第一个存放三张图片的url数据,第二个就是我们的网络加载的状态信息啦。

在具体实现我们的getData具体的方法体之前,我们先实现一下我们Activity中的View层代码:

  
    
    
  
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.bumptech.glide.Glide
import com.xxx.coroutinesdemo.R
import com.xxx.coroutinesdemo.bean.LoadState
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //获取ViewModel
        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

        //对加载状态进行动态观察
        viewModel.loadState.observe(this, Observer {
            when (it) {
                is LoadState.Success -> button.isEnabled = true
                is LoadState.Fail -> {
                    button.isEnabled = true
                    Toast.makeText(this, it.msg, Toast.LENGTH_SHORT).show()
                }
                is LoadState.Loading -> {
                    button.isEnabled = false
                }
            }

        })

        //对图片Url数据进行观察
        viewModel.imageData.observe(this, Observer {
            //用Glide加载三张图片
            Glide.with(this)
                .load(it[0])
                .into(imageView1)
            Glide.with(this)
                .load(it[1])
                .into(imageView2)
            Glide.with(this)
                .load(it[2])
                .into(imageView3)
        })

        //点击刷新按钮来网络加载
        button.setOnClickListener {
            viewModel.getData()
        }
    }
}


这里使用了Kotlin为我们提供的直接引用xml中控件id的方式,这样可以避免编写findViewById代码。首先我们用ViewModelProviders将我们的MainViewModel注入MainActivity中,接着分别对MainViewModel中的两组数据进行观察并更新我们的UI。


加载状态为LoadState.Loading的时候我们要设置刷新按钮为不可用状态,LoadState.Success和LoadState.Fail两种状态要将其设置为可用状态。此外失败状态还有将错误信息通过Toast显示出来。图片url数据更新时我们就使用Glide将三张图片加载到三个ImageView上即可。接着为刷新按钮设置点击事件,直接调用MainViewModel的getData方法即可。


我们可以将这两个类放在同一个包中(如果有其他新的页面的话需要二级分包):

 超长文,带你全面了解Kotlin的协程

5. 实现getData方法


接下来我们就具体地来实现一下最核心的getData方法:

  
    
    
  
fun getData() {
    viewModelScope.launch(CoroutineExceptionHandler { _, e ->
            //加载失败的状态
            loadState.value = LoadState.Fail(e.message ?: "加载失败")
        }) {
            //更新加载状态
            loadState.value = LoadState.Loading()

            //并发请求三张图片的数据
            val data1 = async { NetworkService.apiService.getImage() }
            val data2 = async { NetworkService.apiService.getImage() }
            val data3 = async { NetworkService.apiService.getImage() }
            //通过为LiveData设置新的值来触发更新UI
            imageData.value = listOf(data1.await(), data2.await(), data3.await()).map {
                it.imgurl
            }

            //更新加载状态
            loadState.value = LoadState.Success()
        }
}


首先我们用Jetpack组件提供给我们的viewModelScope开启一个协程,我们可以稍微看下这个viewModelScope:

  
    
    
  
/**
 * ...
 * This scope is bound to [Dispatchers.Main]
 */

val ViewModel.viewModelScope: CoroutineScope
        get() {...}


可以看到viewModelScope是通过Kotlin的拓展属性的方式添加到ViewModel上的,并且其所处的线程是主线程,所以我们可以放心地在其中更新UI的操作。并且其与ViewModel的声明周期绑定,我们在这个协程范围内的耗时操作会在其生命周期结束时自动取消,不用担心内存泄漏之类的性能问题。


而且我们在开启协程的时候为其指定了CoroutineExceptionHandler,所有在协程中出现的错误都将回调这个方法。在加载数据时我们调用了apiService的suspend方法,并通过async方式来实现并发数据请求,最后通过为LiveData设置新值的方式触发UI的更新。


但是目前只有一个页面,只要一个ViewModel,所以这样的写法不会有什么问题。但是当页面数量和ViewModel的数量多起来的时候,每一次网络请求都要写一些模板代码总是有些不舒服,所以接下来我们来对网络请求的代码进行进一步的优化。


可能遇到重复代码的时候大家一般的想法是创建一个BaseViewModel类,重复的模板代码写在这个基类中,接着所有我们的ViewModel继承这个BaseViewModel。

这样的做法的确是可行的,但是我们只有一个很小的功能需要抽象出来,可能基类中也就只有这么一个方法,而且如果你的项目已经成型的时候,这种做法会严重破坏项目结构,你需要手动更改所有ViewModel的父类,接着更改所有的对应方法。

Kotlin为ViewModel添加viewModelScope的做法值得我们借鉴,我们可以为ViewModel添加一个拓展方法,而不需要更改其自身的继承结构:

  
    
    
  
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

fun ViewModel.launch(
    block: suspend CoroutineScope.()
 -> Unit,
    onError: (e: Throwable) -> Unit = {},
    onComplete: () -> Unit = {}
) {
    viewModelScope.launch(CoroutineExceptionHandler { _, e -> onError(e) }) {
        try {
            block.invoke(this)
        } finally {
            onComplete()
        }
    }
}


我们可以新建一个ViewModelExt.kt文件,在其中为ViewModel编写一个launch方法。我们为方法设置了三个参数:


  1. block:协程主体;
  2. onError:错误回调;
  3. onComplete:完成回调。

接着我们在方法体中调用了viewModelScope.launch方法,并把我们的协程主体传入,并在其CoroutineExceptionHandler中调用了我们的onError,在viewModelScope.launch中我们通过一个try{...}finally{...}块包裹了方法体,但是我们没有catch任何错误,所以这在保证了onComplete一定得到执行的同时也保证了onError可以接受到所有的错误。


接着我们使用新的方法来重写我们的MainViewModel:

  
    
    
  
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.xxx.coroutinesdemo.bean.LoadState
import com.xxx.coroutinesdemo.launch
import com.xxx.coroutinesdemo.network.NetworkService
import kotlinx.coroutines.async

class MainViewModel : ViewModel() {

    val imageData = MutableLiveData<List<String>>()
    val loadState = MutableLiveData<LoadState>()

    fun getData() {
        launch(
            {
                loadState.value = LoadState.Loading()
                val data1 = async { NetworkService.apiService.getImage() }
                val data2 = async { NetworkService.apiService.getImage() }
                val data3 = async { NetworkService.apiService.getImage() }
                imageData.value = listOf(data1.await(), data2.await(), data3.await()).map {
                    it.imgurl
                }
                loadState.value = LoadState.Success()
            },
            {
                loadState.value = LoadState.Fail(it.message ?: "加载失败")
            }
        )
    }   
}


是不是感觉简洁了许多呢?

整体项目结构:

超长文,带你全面了解Kotlin的协程


/   一些不足   /

其实这个演示项目中还留下了一些坑,因为我们的重点是讲Kotlin协程的实际应用,有些坑就没有处理,在这里我提一下。网络加载错误不一定只有网络连接和超时等这些明显的错误,对于业务上的错误我们没有做进一步的处理,相信实际项目中网络接口的结构都类似这种三段式的结构:

  
    
    
  
{
    "code"200,
    "data": {...},
    "msg""OK"
}


那么我们可以定义一个包装类ResonseBody:

  
    
    
  
data class ResponseBody<T>(
    val code: Int,
    val msg: String,
    val data: T
)


接着建立一个独立的网络访问层Repository:

  
    
    
  
object Repository {

    //数据脱壳与错误预处理
    fun <T> preprocessData(responseBody: ResponseBody<T>): T {
        return if (responseBody.code == 200) responseBody.data else throw Throwable(responseBody.msg)
    }

    suspend fun getImageData(paramter: Paramter1): ImageData {
        //调用ApiService定义的接口方法
        val responseBody = ApiService.getImage(paramter)
        //返回处理后的数据
        return preprocessData<ImageData>(responseBody)
    }

    suspend fun getOtherData(paramter: Paramter2): OtherData {...}

    ...
}


这样在网络层就将所有可能遇到的错误处理完毕了,ViewModel层直接拿到的就是脱壳后的正确数据,也不需要额外处理这些业务错误了,因为这里throw的错误最终都会由我们的onError回调接收到,我们编写的launch方法可以完美的对其进行处理。

项目地址:
https://github.com/NaJiPeng/Coroutines-Demo



推荐阅读:




欢迎关注我的公众号
学习技术或投稿



长按上图,识别图中二维码即可关注


以上是关于全面了解Kotlin,2021大厂Android高级面试题及答案的主要内容,如果未能解决你的问题,请参考以下文章

超长文,带你全面了解Kotlin的协程

Android 全面屏处理(适配挖孔屏刘海屏) kotlin

2021年最新Android大厂面试题来袭!已拿到offer

为啥我会收到错误消息:Android Gradle 插件仅支持 Kotlin Gradle 插件版本 1.3.10 及更高版本

Android面试官:入职大厂的Android程序员具备怎样的专业素养?

Android面试官:入职大厂的Android程序员具备怎样的专业素养?