kotlin学习第四篇文章!
历史文章
[AS3.6.1]Kotlin学习笔记1(基本声明,函数,条件)
[AS3.6.1]Kotlin学习笔记2(常量,数组,修饰符)
[AS3.6.1]Kotlin学习笔记3(简化操作,泛型)
接口在java中使用的频率,那可是相当高。因为我们经常会重写各个view的clickListener方法。如下
//java
View view = new View(this);
View.OnClickListener click = null;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (click != null) {
click.onClick(v);
}
}
});
//kotlin
val view= View(this)
val click: View.OnClickListener ?= null
view.setOnClickListener(View.OnClickListener {
v -> click?.onClick(v)
});
//简化1 可以去掉类型 kotlin可以自动识别返回类型和用it替代单个参数对象
view.setOnClickListener({
click?.onClick(it)
})
//简化2 函数是唯一返回参数的时候可以去掉括号
view.setOnClickListener{
click?.onClick(it)
}
这边我们看到kotlin对java的简化了许多,但这边是简化了Lambda
的写法,在导入jkd8之后java也可以使用了Lambda
简化代码,上例简化
View view = new View(this);
View.OnClickListener click = null;
view.setOnClickListener(v -> {
if (click != null) {
click.onClick(v);
}
});
再讲kotlin和javaLambda
区别之前我们需要先学会kotlin的高阶函数
那么什么是高阶函数呢?这个说法来自于数学中的高阶函数
简单理解就是一个函数使用函数作为参数或者结果,它就是高阶函数
我们可以看下下面的例子
class KotlinC {
//首先我们有一个int转str的函数
fun int2Str(i: Int) :String{
return i.toString()
}
//我们可以在设置一个获取这个函数值的函数
fun getI2S(i2s: (Int) -> String, i: Int): String{
return i2s(i)
}
}
上诉kotlin的getI2S
函数可以理解为我们使用一个函数来直接调用函数int2Str
的结果,如果不理解可以看上面的java接口内容,我们可以看到这就是java中的接口调用,只不过java不支持高阶函数,所以使用接口来进行传递方法。如下
View.OnClickListener click = null;
view.setOnClickListener(v -> {
if (click != null) {
click.onClick(v);
}
});
就可以近似于的kotlin写法,当然只是近似于而已
fun setClickListener(listener: (View) -> Unit, v: View){
listener(v)
}
fun clickListener(v: View){
//do
}
这样就可以理解高阶函数了,然后我们又发现在java中接口是可以直接生成对象的,那么函数可以转成对象么?
::
函数引用kotlin中的函数引用就是::
,我们就可以理解为一个函数类型的对象,案例如下
//还是刚才的java代码,我们看到了我们设置了一个接口对象click
View.OnClickListener click = null;
view.setOnClickListener(v -> {
if (click != null) {
click.onClick(v);
}
});
//有上诉看成kotlin的写法之后的函数引用
val click = ::clickListener
setClickListener(click, view)
fun setClickListener(listener: (View) -> Unit, v: View){
listener(v)
}
fun clickListener(v: View){
//do
}
至此我们在回看原来的高阶函数,可以写成以下形式
class KotlinC {
//新增的test函数
fun test(){
val a = ::int2Str
val b = getI2S(a, 5)
val c = getI2S(::int2Str, 6)
println(a)
println(b)
println(c)
}
//首先我们有一个int转str的函数
fun int2Str(i: Int) :String{
return i.toString()
}
//我们可以在设置一个获取这个函数值的函数
fun getI2S(i2s: (Int) -> String, i: Int): String{
return i2s(i)
}
}
//调用
val kotlinC = KotlinC()
kotlinC.test()
打印结果如下
I/System.out: function int2Str (Kotlin reflection is not available)
I/System.out: 5
I/System.out: 6
很明显的看到直接打印::
的是一个函数类型的对象,还是说了kotlin不支持反射。因为是对象所以我们可以直接使用,案例如下
val a = ::int2Str
a(1) // 等同于java中的直接调用int2Str
::int2Str(1) //报错 因为这不是一个对象,要::拼上方法才能成为对象
(::int2Str)(1) //这样才是对的
我们看到::
生成的就是一个可以用的对象 实际使用的对象的是invoke
方法,即
a(1)
a.invoke(1)
(::int2Str)(1)
(::int2Str).invoke(1)
这也说明了为什么要把::
和方法拼接起来,因为这是为了形成一个对象。
经过上面的高阶函数和::
说明之后我们就可以讲解kotlin的Lambda了,我们看到开始点击接口的各种简化如下
val view= View(this)
val click: View.OnClickListener ?= null
view.setOnClickListener(View.OnClickListener {
v -> click?.onClick(v)
});
//简化1 可以去掉类型 kotlin可以自动识别返回类型和用it替代单个参数对象
view.setOnClickListener({
click?.onClick(it)
})
//简化2 函数是唯一返回参数的时候可以去掉括号
view.setOnClickListener{
click?.onClick(it)
}
现在我们就能很简单的看懂简化规则了,我们再看下下面这个简化过程
val a = fun(i:Int): String {
return i.toString()
}
//Lambda简化1 去掉函数标示 即直接将函数变成匿名函数
//需要设置输入参数类型
val b = { i: Int ->
i.toString()
}
//Lambda简化2 用it代替参数
val c: (Int) -> String = {
it.toString()
}
这边我们看到将函数简写成Lambda
这边需要注意的是在Lambda中不能写return
,因为直接在Lambda中写了return
的话,等于直接在代码中写了return
会直接不走后续的代码并返回结果。
在Android 11中要取消用了AsyncTask
,替代方法就是kotlin中的协程。
协程在kotlin中的就是指代线程API可以理解为Java提供的Executor
,在Android中为我们封装了AsyncTask
或者使用Thread
+Handler
,所以我们从使用线程的调用来了解协程。
在kotlin中也是可以是直接使用Thread
的,写法如下
//java
new Thread(new Runnable() {
@Override
public void run() {
//操作
}
}).start();
//kotlin
Thread({
//操作
}).start()
我们可以看到这就是简单的kotlin简化java的写法,实际使用还是一样的,并没有太大的区别。和Thread
一样如果使用Executor
和AsyncTask
的话也是按照java的写法然后简化成kotlin而已,对实际的简化很少。
那么我们来看下kotlin的协程实现线程相关的操作和一些函数
runBlocking
正如名称这个函数在启动的协程任务会阻断当前线程,直到该协程执行结束。
println("当前线程id=${Thread.currentThread().id},name=${Thread.currentThread().name}")
println("runBlocking Start")
runBlocking {
//循环7次
repeat(7) {
println("循环协程$it id=${Thread.currentThread().id},name=${Thread.currentThread().name}")
delay(100)
}
}
println("runBlocking End")
打印的log如下
I/System.out: 当前线程id=2,name=main
I/System.out: runBlocking Start
I/System.out: 循环协程0 id=2,name=main
I/System.out: 循环协程1 id=2,name=main
I/System.out: 循环协程2 id=2,name=main
I/System.out: 循环协程3 id=2,name=main
I/System.out: 循环协程4 id=2,name=main
I/System.out: 循环协程5 id=2,name=main
I/System.out: 循环协程6 id=2,name=main
I/System.out: runBlocking End
launch
这是kotlin使用协程中最常用的启动协程的方式,它返回的是一个Job
类型的对象,这个Job
实际是一个接口,内部包含了常用的方法。
println("launch Start")
val job = GlobalScope.launch {
repeat(2) {
println("循环协程$it id=${Thread.currentThread().id},name=${Thread.currentThread().name}")
delay(100)
}
}
println("launch End")
log打印入下
I/System.out: launch Start
I/System.out: launch End
I/System.out: 循环协程0 id=139538,name=DefaultDispatcher-worker-1
I/System.out: 循环协程1 id=139540,name=DefaultDispatcher-worker-3
我们很明显的可以看到launch
并不会阻止当前线程,而是开启了一个新的线程执行操作。这就和java中的Thread
一样的效果。Job
自带的一些常用方法可以看官方文档中的Job API,这边就列举几个如isActive
、isCancelled
、isCompleted
、cancel()
、join()
等。
经过runBlocking
和 launch
的基本使用,我们如果去看源码会发现runBlocking
是一个官方写好的顶层函数而launch
是CoroutineScope
对象的方法,示例中的GlobalScope
其实是官方创建的CoroutineScope
单例对象。在源码中他是需要我们传三个参数的
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
由于用了这三个参数(协程上下文,启动模式,协程体)我们才可以做到切换线程和使用自己创建的CoroutineScope
对象来调用 launch
,因为GlobalScope
是官方写的单例生命周期和app相同并且不能取消。
上下文可以携带参数等数据,也有一个重要作用就是用来切换线程,为了方便kotlin这边为我们提供了几种调度器。
调度器 | 说明 |
---|---|
Dispatchers.Main | 在 Android 主线程上运行,可以理解为java中的runOnUiThread ,在UI线程中执行 |
Dispatchers.IO | 在主线程之外执行磁盘或网络 I/O运行,在线程池中执行 |
Dispatchers.Default | 在主线程之外执行 cpu 密集型的工作(如列表修改排序,json解析处理),在线程池中执行 |
Dispatchers.Unconfined | 在调用的线程直接执行 |
kotlin提供了四种启动模式,我们直接看下他们的区别吧。
模式 | 说明 |
---|---|
DEFAULT | 默认模式,立即执行协程体 |
LAZY | 懒加载模式,只有在需要的情况下运行 |
ATOMIC | 原型模式,立即执行协程体,但在开始运行之前无法取消 |
UNDISPATCHED | 未分配模式,立即在当前线程执行协程体,直到第一个 suspend 调用 |
协程体是一个用
suspend
关键字修饰的一个无参,无返回值的函数类型。被suspend
修饰的函数称为挂起函数,与之对应的是关键字resume
(恢复),注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。
为了说明协程体是啥,我们走一个实战中最常见的案例。
GlobalScope.launch(Dispatchers.Main) {
val token = getToken()
val userName = getUserName(token)
println("userName=$userName") //打印结果 userName=token_name
tvName.text = userName
}
private suspend fun getToken(): String = withContext(Dispatchers.IO) {
delay(200)
"token"
}
private suspend fun getUserName(token: String): String = withContext(Dispatchers.IO) {
delay(500)
"${token}_name"
}
我这边是模拟网络情况从服务器上面获取token和数据之后在ui线程修改布局。在原来的写法中,我们正常是需要在onCreate之后切到IO线程调用网络接口,然后再回调中设置数据并且还要将回调的结果在切回UI线程,就会形成多嵌套模式,虽然我们现在有RxJava和okhttp等方式简化嵌套,而kotlin是直接在协程中就对这种嵌套进行了优化。
这个案例中
val token = getToken()
val userName = getUserName(token)
println("userName=$userName") //打印结果 userName=token_name
tvName.text = userName
就是协程体,也就是开始设置协程上下文的时候各种调度器中的执行协程体。
async
async
跟launch
的用法基本一样,区别在于:async
的返回值是Deferred
,将最后一个封装成了该对象。async
可以支持并发,此时一般都跟await
一起使用。
使用并发案例如下
GlobalScope.launch(Dispatchers.Main) {
val result1 = async {
getResult(2000);
}
val result2 = async {
getResult(3000);
}
val result = result1.await() + result2.await()
println("result=$result") //打印结果 result=5
}
private suspend fun getResult(time: Long): Int {
delay(time)
return (time / 1000).toInt()
}
这个在实际开发中也很容易碰到,当需要从多个网络请求中获取不同的数据的时候并且双方又没有关联的时候,可能需要你手动加锁或者使用RxJava实现合并,在协程中加入的async
可以帮助开发快速实现,打印的时间是3秒而不是5秒。
我们按上面的几个模块的说明,对协程已经有一个大概的理解了。
runBlocking
(顶部函数)、launch
(CoroutineScope内部函数)、async
(CoroutineScope内部函数)suspend
(挂起函数)中使用withContext
快速切换线程目前我们的项目网络请求一般都是使用RxJava + Retrofit + Okhttp
封装的工具类,在kotlin的推广中Retrofit
已经在2.6.0开始支持协程了。所以我们来实际使用下kotlin的网络回调吧!
我们按照java的写法很快就可以实现一个简单的Retrofit
kotlin版本的使用,代码如下
//数据对象
data class GirlBean(
val `data`: List,
val page: Int,
val page_count: Int,
val status: Int,
val total_counts: Int
)
data class Data(
val _id: String,
val author: String,
val category: String,
val createdAt: String,
val desc: String,
val images: List,
val likeCounts: Int,
val publishedAt: String,
val stars: Int,
val title: String,
val type: String,
val url: String,
val views: Int
)
//网络接口
interface IUrl{
//https://gank.io/api/v2/data/category/Girl/type/Girl/page/1/count/10
@GET("category/Girl/type/Girl/page/{page}/count/{count}")
fun girls(@Path("page") page: Int, @Path("count") count: Int): Call
}
//使用
val retrofit = Retrofit.Builder()
.baseUrl("https://gank.io/api/v2/data/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(IUrl::class.java)
api.girls(1, 10).enqueue(object : Callback{
override fun onFailure(call: Call, t: Throwable) {
println("enqueue连接失败 $t")
}
override fun onResponse(call: Call, response: Response) {
println("enqueue连接成功")
}
})
可以看到就是基本Retrofit
使用,只是java写法改成了kotlin的并没有使用到协程。那么我们来看下2.6.0之后加入协程之后该如何写。
interface IUrl{
//https://gank.io/api/v2/data/category/Girl/type/Girl/page/1/count/10
@GET("category/Girl/type/Girl/page/{page}/count/{count}")
suspend fun girls2(@Path("page") page: Int, @Path("count") count: Int): GirlBean
}
//使用
lifecycleScope.launch {
val result = api.girls2(1, 10)
println("结果 $result")
}
这边我们发现我们对原来的请求方法加上了suspend
关键字直接标示为协程,并且返回的直接就可以是对象,不需要借助Retrofit
的Deferred
和Call
等原来需要我们转换的对象了。
使用也方便了很多 直接在协程体中运行,返回的结果直接就是能够使用的对象了。
本篇学习了kotlin的接口高阶函数等知识点,并且只是简单的对协程进行了简单的学习,后续我们在学习协程搭建一个简易的网络框架。
Kotlin 的 Lambda 表达式,大多数人学得连皮毛都不算
Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的
Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了
Kotlin协程
【思货】AndroidX+协程+Retrofit-我的新思考,请走开,所有的Rx请求库!