阅读 1134

Kotlin实战:用实战代码更深入地理解预定义扩展函数

这是该系列的第三篇,系列文章目录如下:

  1. Kotlin基础:白话文转文言文般的Kotlin常识

  2. Kotlin基础:望文生义的Kotlin集合操作

  3. Kotlin实战:用实战代码更深入地理解预定义扩展函数

  4. Kotlin实战:使用DSL构建结构化API去掉冗余的接口方法

  5. Kotlin基础:属性也可以是抽象的

  6. Kotlin进阶:动画代码太丑,用DSL动画库拯救,像说话一样写代码哟!

  7. Kotlin基础:用约定简化相亲

理论和实践之间那条鸿沟一直在那,如果不去跨越,就只能发出“懂了这么多道理,依然过不好这一生”这样的感叹。这一篇就试着用项目中的实战代码来跨越这条鸿沟。本文的口号是“demo code is cheap, show me the real project code!”。包含如下知识点:函数类型、扩展函数、带接收者的lambda、apply()、also()、let()、安全调用运算符、Elvis运算符。

apply()

第一篇中提到过apply()函数,这一次结合实战代码,讲的更深入一点。

在 Android 将多个动画组合在一起会用到 AnimatorSet,使用apply()可以让构建组合动画的代码减少重复的对象名,让整个构建语义一目了然:

val span = 300
AnimatorSet().apply {
    playTogether(
            ObjectAnimator.ofPropertyValuesHolder(
                    tvTitle,
                    PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
                    PropertyValuesHolder.ofFloat("translationY", 0f, 100f)).apply {
                interpolator = AccelerateInterpolator()
                duration = span
            },
            ObjectAnimator.ofPropertyValuesHolder(
                    ivAvatar,
                    PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f),
                    PropertyValuesHolder.ofFloat("translationY", 0f,100f)).apply {
                interpolator = AccelerateInterpolator()
                duration = span
            }
    )
    start()
}
复制代码

同时对 tvTitle 和 ivAvatar 控件做透明度和位移动画,并设置了动画时间和插值器。代码中没有出现多个AnimatorSet对象及多个Animator对象,这都得益于apply()

  1. object.apply()接收一个 lambda 作为参数。它的语义是:将lambda应用于object对象,其中的 lambda 是一种特殊的 lambda,称为带接收者的lambda。这是 kotlin 中特有的,java 中没有。

    带接收者的lambda的函数体除了能访问其所在类的成员外,还能访问接收者的所有非私有成员,这个特性是它具有魅力的关键。(这个特性还使得它非常适用于构建DSL,下一篇会提到)

    上述代码中紧跟在apply()后的 lambda 函数体除了访问其外部的变量span,还访问了 AnimatorSet 的playTogether()start(),就好像在 AnimatorSet 类内部一样。(可以在这两个函数前面加上this,省略了更简洁)。

  2. object.apply()的另一个特点是:在它对 object 对象进行了一段操作后还会返回 object 对象本身。

所以apply()适用于 “构建对象后紧接着还需要调用该对象的若干方法进行设置并最终返回这个对象实例” 的场景

let()

let()apply()非常像,但因为下面的两个区别,使得它的应用场景和apply()不太一样:

  1. 它接收一个普通的 lambda 作为参数。
  2. 它将 lambda 的值作为返回值。

在项目中有这样一个场景:启动一个 Fragment 并传 bundle 类型的参数,如果其中的 duration 值不为 0 则显示视图A,否则显示视图B。用let()实现如下:

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        arguments?.let { arg ->
            //调用对象方法
            arg.getBundle(KEY)?.takeIf { it[DURATION] != 0 }?.let { duration -> 
                //将对象作为参数传递给另一个函数
                showA(duration)
            } ?: showB()
        }
    }
}
复制代码

上述代码展示了let()的三个用法惯例:

  1. 通常情况下let()会和安全调用运算符?一起使用,即object?.let(),它的语义是:如果object不为空则对它做一些操作,这些操作可以是调用它的方法,或者将它作为参数传递给另一个函数

    apply()对比一下,因为apply()通常用于构建新对象(let()用于既有对象),新建的对象不可能为空,所以不需要?,而且就使用习惯而言,apply()后的 lambda 中通常只有调用对象的方法,而不会将对象作为参数传递给另一个函数(虽然也可以这么做,只要传this就可以)

  2. let()也会结合Elvis运算符?:实现空值处理,当调用let()的对象为空时,其 lambda 中的逻辑不会被执行,如果需要指定此时执行的逻辑,可以使用?:

  3. let()嵌套时,显示地指明 lambda 参数名称避免it的歧义。在 kotlin 中如果 lambda 参数只有一个 可以将参数声明省略,并用it指代它。但当 lambda 嵌套时,it的指向就有歧义。所以代码中用arg显示指明这是 Fragment 的参数,用duration显示指明这是 Bundle 中的 duration。

除了上面这种用法,还可以把let()当做变换函数使用,就好像RxJava中的map()操作符。因为let()将 lambda 的值作为其返回值。

在项目中有这样一个需求:为某界面的所有点击事件添加数据埋点。

当然可以将埋点逻辑散落在各个控件的OnClickListener中,但如果希望对埋点逻辑统一控制,就可以用下面的这个方案:

  1. 先定义一个包含点击响应逻辑的类
class OnClickListenerBuilder {
    //'点击响应逻辑'
    var onClickAction: ((View) -> Unit)? = null

    //'为点击响应逻辑赋值的函数'
    fun onClick(action: (View) -> Unit) {
        onClickAction = action
    }
}
复制代码

函数式编程中,把函数当做值来对待,你可以把函数当做值到处传递,也可以把函数独立地声明并存储在一个变量中,但是最常见的还是直接声明它并传递给函数作为参数。

OnClickListenerBuilder定义了一个函数类型的成员变量onClickAction,它的类型是((View) -> Unit)?,这和View.OnClickListener中的void onClick(View view)函数一摸一样,即输入一个View并返回空值。这个成员变量的值是可空的,所以在原本的函数类型(View) -> Unit外面又套了一个括号和问号。

这个类的目的是将自定义的点击响应逻辑保存在函数类型的变量中,当点击事件发生时应用这段逻辑。

  1. 为 View 设置点击事件并应用自定义点击响应逻辑
//'定义扩展函数'
fun View.setOnDataClickListener(action: OnClickListenerBuilder.() -> Unit) {
    setOnClickListener(
            OnClickListenerBuilder().apply(action).let { builder ->
                View.OnClickListener { view ->
                    //'埋点逻辑'
                    Log.v(“ttaylor”, “view{$view} is clicked”)
                    //'点击响应逻辑'
                    builder.onClickAction?.invoke(view)
                }
            }
    )
}

//'在界面中使用扩展函数为控件设置点击事件'
btn.setOnDataClickListener {
    onClick {
        Toast.makeText(this@KotlinExample, “btn is click”, Toast.LENGTH_LONG).show()
    }
}
复制代码
  • 扩展函数

    View声明了一个扩展函数setOnDataClickListener()。扩展函数是一个类的成员函数,但它定义在类体外面。这样定义的好处是,可以在任何时候任何地方给类添加功能。

    在扩展函数中,可以像类的其他成员函数一样访问类的属性和方法(除了被private和protected修饰的成员),在本例中调用了setOnClickListener()为控件设置点击事件。

  • 带接收者的lambda

    为了应用保存在OnClickListenerBuilder中的点击响应逻辑,必须先构建其实例。代码中的OnClickListenerBuilder().apply(action)在构建实例的同时对其应用了一个 lambda ,这是一个带接收者的lambda,且接收者是OnClickListenerBuilder。它作为扩展函数的参数传入。当调用setOnDataClickListener()的时候,我们在传入的 lambda 中轻松地调用了onClick()方法,因为带接收者的lambda函数体中可以访问接收者的非私有成员。这样就实现了将点击响应逻辑保存在函数类型变量onClickAction中。

  • let()map()

    由于 API 的限定View.setOnClickListener()的参数必须是View.OnClickListener类型的,所以必须将OnClickListenerBuilder转化成View.OnClickListener。调用let()就可以轻松做到,因为它将 lambda 的值作为返回值。

    最后在原生点击事件响应函数中实现了埋点逻辑并调用了保存在函数类型变量中的自定义点击响应逻辑。

also()

also()几乎和let()相同,唯一的却别是它会返回调用者本身而不是将 lambda 的值作为返回值。

和同样返回调用者本身的apply()相比:

  1. 就传参而言,apply()传入的是带接收者的lambda,而also()传入的是普通 lambda。所以在 lambda 函数体中前者通过this引用调用者,后者通过it引用调用者(如果不定义参数名字,默认为it)
  2. 就使用场景而言,apply()更多用于构建新对象并执行一顿操作,而also()更多用于对既有对象追加一顿操作。

在项目中,有一个界面初始化的时候需要加载一系列图片并保存到一个列表中:

listOf(
    R.drawable.img1,
    R.drawable.img2, 
    R.drawable.img3,
    R.drawable.img4, 
).forEach {resId->
    BitmapFactory.decodeResource(
        resources, 
        resId, 
        BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.ARGB_4444
            inMutable = true
        }
    ).also { bitmap -> imgList.add(bitmap) }
}
复制代码

这个场景中用let()也没什么不可以。但是如果还需要将解析的图片轮番显示出来,用also()就再好不过了:

listOf(
    R.drawable.img1,
    R.drawable.img2, 
    R.drawable.img3,
    R.drawable.img4, 
).forEach {resId->
    BitmapFactory.decodeResource(
        resources, 
        resId, 
        BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.ARGB_4444
            inMutable = true
        }
    ).also { 
        //存储逻辑
        bitmap -> imgList.add(bitmap) 
    }.also {
        //显示逻辑
        ivImg.setImageResource(it)   
    }
}
复制代码

因为also()返回的是调用者本身,所以可以also()将不同类型的逻辑分段,这样的代码更容易理解和修改。这个例子逻辑比较简单,只有一句话,将他们合并在一起也没什么不好。

知识点总结

  • 在函数式编程中,把函数当做值来对待,你可以把函数当做值到处传递,也可以把函数独立地声明并存储在一个变量中。
  • 函数类型是一种新的类型,它用 lambda 来描述一个函数的输入和输出。
  • 扩展函数是一种可以在类体外为类新增功能的特性,在扩展函数体中可以访问类的成员(除了被private和protected修饰的成员)
  • 带接收者的lambda是一种特殊的lambda,在函数体中可以访问接收者的非私有成员。可以把它理解成接收者的扩展函数,只不过这个扩展函数没有函数名。
  • apply() also() let()是系统预定义的扩展函数。用于简化代码,减少重复的对象名。
  • ?.称为安全调用运算符,若object?.fun()中的 object 为空,则fun()不会被调用。
  • ?:称为Elvis运算符,它为 null 提供了默认逻辑,funA() ?: funB(),如果 funA() 返回值不为 null 执行它 并将它的返回值作为整个表达式的返回值,否则执行 funB() 并采用它的返回值。

下一篇会在这篇的基础上讲解一个更加复杂的例子并引出DSL的概念。

关注下面的标签,发现更多相似文章
评论