你应该知道的kotlin实用技巧

6,776

前言

众所周知,kotlin是google力推的用以取代java的android开发语言
kotlin使用起来比较方便,同时有许多语法糖
本文主要讲解了一些比较实用的kotlin技巧

自定义圆角矩形

在项目中,我们常常要定义圆角矩形背景,一般是用自定义drawable实现的
但是圆角矩形的背景与圆角常常会有细微的变化,而一旦变化我们又要新创建一个drawable文件
这样就会导致文件爆炸的问题

我们可以利用kotlin的扩展函数,来实现简单方便的圆角矩形背景

fun View.setRoundRectBg(color: Int = Color.WHITE, cornerRadius: Int = 15.dp) {
    background = GradientDrawable().apply {
        setColor(color)
        setCornerRadius(cornerRadius.toFloat())
    }
}

对于需要自定义背景的View,直接调用setRoundRectBg即可,简单方便

reified使用

reified,kotlin中的泛型实化关键字,使抽象的东西更加具体或真实。
我们举两个例子来看看怎么使用reified

startActivity例子

我们一般startActivity是这样写的

startActivity(context, NewActivity::class.java)  

我们利用reified定义一个扩展函数

// Function
inline fun <reified T : Activity> Activity.startActivity(context: Context) {
    startActivity(Intent(context, T::class.java))
}

// Caller
startActivity<NewActivity>(context)

使用 reified,通过添加类型传递简化泛型参数
这样就不用手动传泛型的类型过去了

Gson解析例子

我们首先看下一般我们使用gson解析json是怎么做的
在Java序列化库(如Gson)中,当您想要反序列化该JSON字符串时,您最终必须将Class对象作为参数传递,以便Gson知道您想要的类型。

User user = new Gson().fromJson(getJson(), User.class)

现在,让我们一起展示reified类型实化参数的魔法 我们将创建一个非常轻量级的扩展函数来包装Gson方法:

inline fun <reified T> Gson.fromJson(json: String) = 
        fromJson(json, T::class.java)

现在,在我们的Kotlin代码中,我们可以反序列化JSON字符串,甚至根本不需要传递类型信息!

val user: User = Gson().fromJson(json)

Kotlin根据它的用法推断出类型 - 因为我们将它分配给User类型的变量,Kotlin使用它作为fromJson()的类型参数

kotin接口支持SAM转换

什么是SAM转换?可能有的同学还不太了解,这里先科普一下:

SAM 转换,即 Single Abstract Method Conversions,就是对于只有单个非默认抽象方法接口的转换 —— 对于符合这个条件的接口(称之为 SAM Type ),在 Kotlin 中可以直接用 Lambda 来表示 —— 当然前提是 Lambda 的所表示函数类型能够跟接口的中方法相匹配。

在Kotlin1.4之前,Kotlin是不支持Kotlin的SAM转换的,只支持Java SAM转换,官方给出的的解释是:是 Kotlin 本身已经有了函数类型和高阶函数,不需要在去SAM转化。 这个解释开发者并不买账,如果你用过Java Lambda和Fuction Interface。当你切换到Kotlin时,就会很懵逼。看来Kotlin是意识到了这个,或者是看到开发者的反馈,终于支持了。

在1.4之前,只能传递一个对象,是不支持Kotlin SAM的,而在1.4之后,可以支持Kotlin SAM,但是用法有一丢丢变化。interface需要使用fun关键字声明。使用fun关键字标记接口后,只要将此类接口作为参数,就可以将lambda作为参数传递。

// 注意需用fun 关键字声明
fun interface Action {
    fun run()
}

fun runAction(a: Action) = a.run()

fun main(){
	// 1.4之前,只能使用object
    runAction(object : Action{
        override fun run() {
            println("run action")
        }
    })
     // 1.4-M1支持SAM,OK
    runAction {
        println("Hello, Kotlin 1.4!")
    }
}


委托

有时候,完成一些工作的方法是将它们委托给别人。这里不是在建议您将自己的工作委托给朋友去做,而是在说将一个对象的工作委托给另一个对象。

当然,委托在软件行业不是什么新鲜名词。委托 (Delegation) 是一种设计模式,在该模式中,对象会委托一个助手 (helper) 对象来处理请求,这个助手对象被称为代理。代理负责代表原始对象处理请求,并使结果可用于原始对象。

类委托

举个例子,当我们要实现一个增强版的ArrayList,支持恢复最后一次删除的item

实现这个用例的一种方式,是继承 ArrayList 类。由于新的类继承了具体的 ArrayList 类而不是实现 MutableList 接口,因此它与 ArrayList 的实现高度耦合。
如果只需要覆盖 remove() 函数来保持对已删除项目的引用,并将 MutableList 的其余空实现委托给其他对象,那该有多好啊。为了实现这一目标,Kotlin 提供了一种将大部分工作委托给一个内部 ArrayList 实例并且可以自定义其行为的方式,并为此引入了一个新的关键字: by。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class ListWithTrash <T>(private val innerList: MutableList<T> = ArrayList<T>()) : MutableCollection<T> by innerList {
	var deletedItem : T? = null
	override fun remove(element: T): Boolean {
	       deletedItem = element
			return innerList.remove(element)
	}
	fun recover(): T? {
		return deletedItem
	}
}

by 关键字告诉 Kotlin 将 MutableList 接口的功能委托给一个名为 innerList 的内部 ArrayList。通过桥接到内部 ArrayList 对象方法的方式,ListWithTrash 仍然支持 MutableList 接口中的所有函数。与此同时,现在您可以添加自己的行为了。

属性委托

除了类代理,您还可以使用 by 关键字进行属性代理。通过使用属性代理,代理类会负责处理对应属性 get 与 set 函数的调用。这一特性在您需要在其他对象间复用 getter/setter 逻辑时十分有用,同时也能让您可以轻松地对简单支持字段的功能进行扩展

举个例子,利用委托属性可以封装SharedPreference
将数据存储操作委托给代理类有几个好处
1.则精简了代码,方便了存储与读取调用
2.与SP进行了解耦,后续如果要替换存储库,只需要修改代理类即可

调用如下:

object Pref: PreferenceHolder() {
    var isFirstInstall: Boolean by bindToPreferenceField(false)
    var time: Long? by bindToPreferenceFieldNullable()
}

具体实现可见:SharedPreferences用Kotlin应该这样写

带状态的LiveData

目前我们在开发的过程中越来越多的使用MVVM模式与ViewModel
我们也常常用LiveData来标识网络请求状态
我们需要定义请求开始,请求成功,请求失败,三个LiveData

这其实也是很冗余重复的代码,因此我们可以进行一定的封装,封装一个带状态的LiveData

定义如下:

typealias StatefulLiveData<T> = LiveData<RequestState<T>>
typealias StatefulMutableLiveData<T> = MutableLiveData<RequestState<T>>

@MainThread
inline fun <T> StatefulLiveData<T>.observeState(
    owner: LifecycleOwner,
    init: ResultBuilder<T>.() -> Unit
) {
    val result = ResultBuilder<T>().apply(init)

    observe(owner) { state ->
        when (state) {
            is RequestState.Loading -> result.onLading.invoke()
            is RequestState.Success -> result.onSuccess(state.data)
            is RequestState.Error -> result.onError(state.error)
        }
    }
}

使用如下

val data = StatefulMutableLiveData<String>()
viewModel.data.observeState(viewLifecycleOwner) {
            onLading = {
                //loading
            }
            onSuccess = { data ->
                //success
            }
            onError = { exception ->
                //error
            }
        }

通过以上封装,可以比较优雅简洁的封装网络请求的loading,success,error状态,精简了代码,结构也比较清晰

DSL

DSL(domain specific language),即领域专用语言:专门解决某一特定问题的计算机语言,比如大家耳熟能详的 SQL 和正则表达式。
但是,如果为解决某一特定领域问题就创建一套独立的语言,开发成本和学习成本都很高,因此便有了内部 DSL 的概念。所谓内部 DSL,便是使用通用编程语言来构建 DSL。比如,本文提到的 Kotlin DSL,我们为 Kotlin DSL 做一个简单的定义:

“使用 Kotlin 语言开发的,解决特定领域问题,具备独特代码结构的 API 。”

举个例子,我们使用TabLayout时,如果要为他添加监听,需要实现以下3个方法

override fun onTabReselected(tab: TabLayout.Tab?){

}

override fun onTabUnselected(tab: TabLayout.Tab?){

}
    
override fun onTabSelected(tab: TabLayout.Tab?){

}

其实我们一般只会用到onTabSelected方法,其余两个一般是空实现
我们利用DSL对OnTabSelectedListener进行封装,即可避免写不必要的空实现代码

具体实现如下:

private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit

class OnTabSelectedListenerBuilder : TabLayout.OnTabSelectedListener {

    private var onTabReselectedCallback: OnTabCallback? = null
    private var onTabUnselectedCallback: OnTabCallback? = null
    private var onTabSelectedCallback: OnTabCallback? = null

    override fun onTabReselected(tab: TabLayout.Tab?) =
            onTabReselectedCallback?.invoke(tab) ?: Unit

    override fun onTabUnselected(tab: TabLayout.Tab?) =
            onTabUnselectedCallback?.invoke(tab) ?: Unit

    override fun onTabSelected(tab: TabLayout.Tab?) =
            onTabSelectedCallback?.invoke(tab) ?: Unit

    fun onTabReselected(callback: OnTabCallback) {
        onTabReselectedCallback = callback
    }

    fun onTabUnselected(callback: OnTabCallback) {
        onTabUnselectedCallback = callback
    }

    fun onTabSelected(callback: OnTabCallback) {
        onTabSelectedCallback = callback
    }

}

fun registerOnTabSelectedListener(function: OnTabSelectedListenerBuilder.() -> Unit) =
        OnTabSelectedListenerBuilder().also(function)

定义DSL的一般步骤:

  • 1.先定义一个类去实现回调接口,并且实现它的回调方法。
  • 2.观察回调方法的参数,提取成一个函数类型(function type),并且按照需要使用类型别名给函数类型起一个别称,并且用私有修饰。
  • 3.在类里面声明一些可空的函数类型的可变(var)私有成员变量,并且在回调函数中拿到对应的变量实现它的invoke函数,传入对应的参数。
  • 4.在类中定义一些跟回调接口一样名字,但是参数是对应的函数类型的函数,并且将函数类型赋值给当前类的对应的成员变量。
  • 5.定义一个成员函数,参数是一个带有我们定好那个类的接受者对象并且返回Unit的Lambda表达式,在函数里创建相应的对象,并且使用also函数把Lambda表达式传进去。

调用如下:

tabLayout.addOnTabSelectedListener(registerOnTabSelectedListener {
    onTabSelected { vpOrder.currentItem = it?.position ?: 0 }
})

如上,就可以避免写一些不必要的空实现代码了