Java回调的痛

Android在几年前普遍都运用Java开发。现在在Google宣告“Kotlin First”之后,加上不断高涨的Kotlin User数量,现在开端往kt转变了。Java言语,广为咱们吐槽的一点便是异步操作需求通过回调来设置,并且还要特别注意线程切换,一不小心就会报:

Only the original thread that created a view hierarchy can touch its views

例如,我此前做过的一个弹窗倒计时更新按钮状况的需求,就要阅历初始化一个倒计时东西类,然后设置回调,回调里写逻辑,最后再调用开端start。就像下面这样,设置过程中产生一堆override的回调,极其不美观。

      CountDownUtils count = new CountDownUtils(5, 1000);
      count.setTimerCallBack(new CountDownUtils.OnTimerCallBack() {
            @Override
            public void onStart() {
                btn_close.setClickable(false);
                btn_close.getBackground().setAlpha(88);
                btn_close.setTextColor(Color.argb(80, 0xff, 0xff, 0xff));
                btn_close.setText("封闭 5s");
            }
            @Override
            public void onTick(int times) {
                btn_close.setText("封闭 " + times + "s");
            }
            @Override
            public void onFinish() {
                btn_close.getBackground().setAlpha(255);
                btn_close.setTextColor(Color.argb(255, 0xff, 0xff, 0xff));
                btn_close.setClickable(true);
                btn_close.setText("封闭");
            }
      });
      count.start();

后来又出了RxJava这个强壮的异步库,可是其运用也过于杂乱,各种操作符又有比较高的学习成本,做一个简略的倒计时的需求还用不上这个东西。在学习Kotlin之后,开端着手自己规划了一个简略的倒计时的东西类。

东西类用到的Kotlin的几个特性

单例类声明

过去Java中,单例东西类有一套通用的写法,一般都是getInstance同步办法,保证获取处回来单例:

public class BSingleton {
    private static BSingleton bSingleton;
    private BSingleton() {
    }
    public synchronized static BSingleton getbSingleton(){
        if(bSingleton == null){
            bSingleton = new BSingleton();
        }
        return bSingleton;
    }
}

Kotlin贴心肠优化了单例类的声明,直接以object要害字来声明一个单例类,内部的fun函数都是默认的static函数,外部可直接调用:

object BSingleton{
}

默认函数参数

Java中遇到同名办法可是不同参数,一般会设置一堆重载函数来提供调用,在调用时其实最终也便是调用的参数最少的那一个。Kotlin增加了默认参数这个规划,在函数声明时,能够给参数一个默认值,调用时能够不传,函数履行时直接运用这个默认值。

fun printSomething(text:String = "default"){
    Log.i("MYTAG", text)
}
// 调用时不写参数便是运用的text默认值"default"
printSomething()printSomething("my special text")

这一点能够有用消除重载办法的书写,别的还能够参加可空类型的运用,合作elvis符,?: 参数为空时,能够防止操控空指针过错。

函数式参数Lambda表达式

和上一个概念或许有点简略搞混淆,函数式参数其实能够了解成一种回调,而这个运用函数式参数的函数也叫高阶函数。

高阶函数的界说:一个函数假如参数类型是函数或许回来值类型是函数,那么这便是一个高阶函数。Kotlin 支撑函数作为参数传递,无需构建目标来包装函数。最典型的比如便是给按钮设置点击回调的时候,setOnClickListener便是一个高阶函数,例如下面的写法:binding.btnFastclick.setOnClickListener { infoLog("test") }

来看另一个简略的运用,getStringLenth函数,传进去一个字符串String目标a,和一个获取字符串长度的“函数目标” b,这个b实际上不是一个目标,实际上还是匿名内部类来完成的。

val lenth = getStringLenth("Android") {    it.length}
/** * 将另一个函数当作参数的函数称为高阶函数 */fun getStringLenth(str: String, getLenth: (String) -> Int): Int {    return getLenth(str)}

能够看到在界说时,第二个参数的类型是(String) -> Int)即其是一个接收String类型,回来Int类型的函数。调用途,当最后一个参数为函数式参数时,能够写成lambda表达式的写法,如上所示。

协程简略运用

协程是一种规划理念,Java在JDK21里参加的虚拟线程也是这样的规划理念。一种将代码使命在不同线程上切换,并支撑运用者自在切换效果域,采用同步的方法书写代码的方便规划。举一个全网通用的比如。IO线程获取网络数据,展示到界面:

// viewModel或许controller里获取数据逻辑
// 运用suspend约束在协程里运用;withContext切换调度器,指定在IO线程履行下面的使命
suspend fun getUserName() = withContext(Dispatchers.IO) {    debugLog("thread name: ${Thread.currentThread().name}")    ServiceCreator.createService<UserService>()        .getUserName("2cd1e3c5ee3cda5a")        .execute()        .body()}
// Activity调用途
override fun onCreate(savedInstanceState: Bundle?){
// 最直接的声明办法,CoroutineScope(Dispatchers.Main),在主线程履行下面的逻辑
    CoroutineScope(Dispatchers.Main).launch {
        // 相当于get这一半是在IO线程履行,拿到结果后的变量赋值这一半操作由调度器自动切换到主线程来履行了
        val userName = mViewModel.getUserName()        infoLog("userName: $userName")        binding.tvUserName.text = userName
    }
}

以上便是设置一个suspend挂起式函数来获取网络数据能够了解成需求等待一段时间才会履行完毕的函数,只能在协程里或许其他挂起式函数里调用。

假如在其他地方调用则会编译报错提示:Suspend function 'XXXXXX' should be called only from a coroutine or another suspend function

协程里假如有延时操作,能够直接调用顶层函数delay()。顶层函数便是Java里的static函数,可直接写在KT文件中,不隶属于恣意一个类。后边完成简略的计时类便是直接运用的delay(1000L),来完成1s的间隔tick操作。并且,delay为非堵塞式的挂起函数,即使在主线程delay好久,也不会呈现ANR。假如咱们不运用 delay(),而是运用 Thread.sleep(),那么协程就不会呼应撤销,而是继续履行,直到循环完毕。这是由于 Thread.sleep() 是一个堵塞函数,它不会查看协程的撤销状况,也不会抛出任何反常。因而,咱们应该尽量避免在协程中运用堵塞函数,而是运用挂起函数。

完成CountDownUtil

CountDownUtil只有两个函数,一个开端start,一个撤销cancel。

其间,start函数里需求传五个参数,其间总时长和步进(ms)需求能整除,否则抛出IllegalArgumentException。别的三个参数:

onStart: () -> Unit = {},

onTick: (currentTime: Int) -> Unit = {},

onFinish: () -> Unit = {}

这三个为函数式参数,分别是开端时触发onStart调用,步进触发onTick调用,完毕时触发onFinish调用。且默认值均为空,调用时,能够都不传,也能够设置部分。

别的,为了撤销初始化的new操作,在东西类里维护一个map,用id来对应履行计时操作的协程,需求撤销计时时,通过id查询写成目标,来履行撤销操作。

注意运用Map时还有一个坑,便是访问不存在的key时,其不会报错,只会回来一个空值。那么咱们输错了id,协程也不会撤销,也不会产生报错,非常尴尬。所以手动增加一个id查看,元素不在map里时抛出RuntimeException。

object CountDownUtil {    private val coroutingMap = mutableMapOf<Int, CoroutineScope>()    /**     * totalTime总时长,单位s     * interval步进,单位ms,默认值1000ms     * onStart和onFinish可空     */    fun start(        coroutineId: Int,        totalTime: Long,        interval: Long = 1000,        onStart: () -> Unit = {},        onTick: (currentTime: Int) -> Unit = {},        onFinish: () -> Unit = {}    ) {        // 整除校验        if ((totalTime * 1000 % interval).toInt() != 0) {            throw IllegalArgumentException("CountDownUtil: remainder is not 0")        }        // 和id一起参加map,方便后续定点cancel        // 假如你看过协程的官方文档或视频。你应该会知道Job和SupervisorJob的一个区别是,Job的子协程产生反常被撤销会同时撤销Job的其它子协程,而SupervisorJob不会。        val countDownCoroutine = CoroutineScope(            Dispatchers.Main + SupervisorJob()        )        coroutingMap[coroutineId] = countDownCoroutine        // 开端计时        countDownCoroutine.launch(CoroutineExceptionHandler { _, e ->            e.message?.let { errorLog(it) }        }) {            // 开端            onStart()            // 循环触发onTick            repeat((totalTime * 1000 / interval).toInt()) {                delay(interval)                onTick((totalTime * 1000 / interval - (it + 1)).toInt())            }            // 循环完毕,触发onFinish            onFinish()        }    }    /**     * 以ID标识,撤销协程,停止计时     */    fun cancel(coroutineId: Int) {        if (!coroutingMap.contains(coroutineId)) throw RuntimeException("Can't find your Id in the Coroutine map")        coroutingMap[coroutineId]?.cancel()        coroutingMap.remove(coroutineId)    }}

在调用时:

CountDownUtil.start(12355, 5, 1000,    onStart = { infoLog("CountDown  start") },    onTick = { infoLog("CountDown  tick: $it") },    onFinish = { infoLog("CountDown  finish ") })
// 延时3s撤销测试
sleep(3000L)
CountDownUtil.cancel(123) // java.lang.RuntimeException: Can't find your Id in the Coroutine map

至此咱们的计时东西类就完成了,在函数调用处,通过lambda表达式的方法来设置开端,进行中,完毕的不同操作,消除传统的回调形式,关于开发者更友爱。

假如只需求一个finish的操作,写法将愈加高雅:

CountDownUtil.start(12355, 5, 1000) { infoLog("CountDown finish ") }