KotlinAndroid开发经验谈Android开发

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

2019-06-27  本文已影响11人  唐子玄

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

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

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

  3. 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`,省略了更简洁)。
  1. 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()
    }
}

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()将不同类型的逻辑分段,这样的代码更容易理解和修改。这个例子逻辑比较简单,只有一句话,将他们合并在一起也没什么不好。

知识点总结

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

上一篇下一篇

猜你喜欢

热点阅读