/ 今日科技快讯 /
据美国《新闻周刊》报道,社交平台脸书的创始人扎克伯格日前被《新共和》(The New Republic)刊物评为“年度恶人”,理由是他创建了“世界上最糟糕、最具破坏性的网站”。
/ 作者简介 /
本篇文章来自DylanCai的投稿,文章主要分享了他对Kotlin开发中委托方面的知识分享,相信会对大家有所帮助!
DylanCai的博客地址:
https://juejin.cn/user/4195392100243000
/ 前言 /
很多人写Kotlin只用到一些Java已有的东西,单纯地把Java代码翻译成Kotlin代码,而Kotlin一些好用的语法糖都没有用过。本文给大家介绍一个Java不常见但是很好用的Kotlin语法糖 —— Kotlin委托。
Kotlin委托包含了属性委托,是我个人很喜欢的一个特性。本文会用尽可能简单通俗的语言讲清楚委托到底是什么东西,什么实现属性委托。属性委托特别适合一些保存数据的场景,所以后面会给大家分享个人封装MMKV的思路,来实战一下属性委托。接下来为大家讲解Kotlin委托的本质和MMKV的封装思路。
/ Kotlin 委托的本质 /
什么是委托
讲Kotlin的委托之前,要先讲一下委托模式。委托模式又称代理模式,是常见的设计模式。
委托模式类似于我们生活中的代理、代购、中介。有些东西我们很难直接去买或者不知道怎么去买,但是我们能通过代理、代购、中介等方式间接去购买,这样我们也有具有了购买该东西的能力。
那代码怎么实现呢?首先我们定义一个接口,声明一个购买方法:
interface Subject {
fun buy()
}
然后创建一个代理类实现该购买功能:
class Delegate : Subject {
override fun buy() {
print("I can buy it because I live abroad.")
}
}
在某个类需要该功能但是没法直接实现的时候,就能通过代理间接地实现功能。
class RealSubject : Subject {
private val delegate: Subject = Delegate()
override fun buy() {
delegate.buy()
}
}
总结一下,委托(代理)模式其实就是将接口功能交给另一个接口实例对象去实现。所以委托模式是有模板代码的,每个接口方法调用了对应的代理对象方法。
虽然存在模板代码,但是Java没有什么好的办法能生成模板代码, 而Kotlin可以零模板代码地原生支持它。通过by关键字进行委托,我们就能把上面的代码改成:
class RealSubject : Subject by Delegate()
这个委托的代码最终会生成上面的代码,简而言之就是编译器帮我们生成了模板代码。另外by关键字后面的表达式可能会有很多写法:
class RealSubject(delegate: Subject) : Subject by delegate
class RealSubject : Subject by globalDelegate
class RealSubject : Subject by GlobalDelegate
class RealSubject : Subject by delegate {...}
虽然写法有很多种,但是记住一点,by后面的表达式一定是得到一个接口的实例对象。因为接口功能是要委托给一个具体的实例对象,而这个对象可能通过构造函数、顶级属性、单例、方法等方式得到。
对此不了解的话,看到by后面各式各样的写法会很懵。其实不是什么Kotlin语法,只是为了得到个对象而已。
什么是属性委托
接口是把接口方法委托出去,那属性要委托什么呢?其实也很容易想到,属性能把get、set方法委托出去。
val delegate = Delegate()
var message: String
get() = delegate.getValue()
set(value) = delegate.setValue(value)
Kotlin支持用by关键字将属性委托给一个实例对象,这就是Kotlin的属性委托:
var message: String by Delegate()
当然属性的委托类不能随便写,有一套具体的规则。先来看一个委托类的示例:
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
"$thisRef, thank you for delegating '${property.name}' to me!"
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) =
println("$value has been assigned to '${property.name}' in $thisRef.")
}
可能有些人看了就懵了,怎么这么多东西,为什么要这么写?
其实是有一套固定的模板,不过不用特地去背,因为官方提供了接口类给我们快速实现。
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
public override operator fun getValue(thisRef: T, property: KProperty<*>): V
public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}
但是这个接口并不是必须的,我们手敲出对应的方法也能进行属性委托,接下来我会和大家讲清楚方法里的每一个要点。
首先来看一下方法的第一个参数thisRef,顾名思义这是this的引用。因为这个属性可能是在一个类里面,可能需要调用该类的方法,这时如果连外层的this引用都拿不到,那还谈何委托。不过我们可能用不到外层的类,这就可以像上面的示例那样定义为Any?类型。
然后是第二个参数property,必须是KProperty<*>类型或其超类型。这个参数可以拿到属性的信息,比如属性的名字。为什么要这个参数呢?因为可能要根据不同的属性实现不同的业务。举个例子,假如我们是做房地产中介,我们不可能什么人都推荐一样的房子,有钱人可能要买别墅的。我们会根据不同的客户信息反馈不同的结果,这就需要拿到客户的资料。同理属性的委托类需要能拿到属性的信息。
还有最重要的一点,需要有operator关键字修饰的getValue()和setValue()方法。这就涉及到另一个Kotlin的进阶用法——重载操作符。先讲个场景让大家来了解重载操作符,比如我们写了一个类,我们并不能像Int类型那样使用a + b的方式把两个对象相加。但是重载操作符能帮我们做到这事,我们增加 operator 关键字修饰的 plus() 方法之后就能a + b了。还能重载很多操作符,如a++、a > b等,大家有兴趣自行去了解。能重载的方法名是固定的,比如重载plus()方法就对应加法操作。而重载getValue()和setValue()方法是对应该类的get、set方法。属性委托就是把属性的get、set方法委托给代理类的get、set方法。
以上都是一个属性委托的必要条件,你可能不用,但是你不能没有。只要委托实例不满足条件,编译器就会报错。
Kotlin标准库还提供了几种常用的标准委托,方便我们在一些常用的场景快速实现属性委托。
延时委托
用于延时初始化的场景。因为委托的逻辑是固定的,官方已经帮我们写好了委托类代码,提供了一个lazy()方法快速创建委托类的实例。
val loadingDialog by lazy { LoadingDialog(this) }
首次获取属性的值会执行返回lambda表达式的结果,后续获取属性都是直接拿缓存。其实就是执行了以下的逻辑。
private var _loadingDialog: LoadingDialog? = null
val loadingDialog: LoadingDialog
get() {
if (_loadingDialog == null) {
_loadingDialog = LoadingDialog(this)
}
return _loadingDialog!!
}
lazy()方法返回的是Lazy类的对象,那么编译器生成的委托类会是Lazy而不是前面的ReadWriteProperty。
val delegate: Lazy = SynchronizedLazyImpl<LoadingDialog>(...)
val loadingDialog: LoadingDialog
get() = delegate.value
可观察的委托
能够很方便地实现观察者模式。委托类的代码也是固定的,所以官方提供了Delegates.observable()方法创建委托的实例。每次设置属性的值都能在lambda表达式接收到回调。
var name: String by Delegates.observable("<no name>") { prop, old, new ->
println("$old -> $new")
}
该方法返回一个ObservableProperty对象,继承自ReadWriteProperty。内部实现很简单,就是在setValue()的时候执行回调方法。
委托映射的属性值
简单来说就是把一个属性委托给map。
class User(val map: Map<String, Any?>) {
val name: String by map
}
获取上面的属性其实是用map获取键名为name的值,编译器会生成以下逻辑。
class User(val map: Map<String, Any?>) {
val name: String
get() = map["name"] as String
}
属性委托给属性
听起来有点绕,讲一个我遇过的问题来让大家更好地理解这个特性。Jetpack MVVM 架构是让数据写在Repository层,Activity需要通过ViewModel去操作Repository的数据。所以我之前写了以下逻辑:
object DataRepository {
var isFirstLaunch: Boolean by MMKVDelegate()
}
class GuideViewModel : ViewModel() {
var isFirstLaunch = DataRepository.isFirstLaunch
}
// In GuideActivity.kt
viewModel.isFirstLaunch = false
简单来说就是ViewModel转发了Repository属性委托的属性,但代码执行下来并没有调用属性委托的set()方法,导致数据没保存到。这段代码看似没什么问题,实则是有陷阱的。
问题出在ViewModel,等号的写法是有问题的,那只是个赋值操作,并不是持有引用,所以修改ViewModel的属性不会影响到Repository的属性。我当时发现问题后改为了下面的写法:
class GuideViewModel : ViewModel() {
var isFirstLaunch: Boolean
get() = DataRepository.isFirstLaunch
set(value) {
DataRepository.isFirstLaunch = value
}
}
逻辑上没什么问题了,但是代码看起来很丑陋。后来发现Kotlin在1.4版本新增了特性来应对这种情况,能将一个属性委托给另一个属性,这样就完美解决了。
class GuideViewModel : ViewModel() {
var isFirstLaunch by DataRepository::isFirstLaunch
}
小结
Kotlin委托其实就是使用by语法后,编译器会帮我们生成了委托类的代码。如果是接口的委托:
class RealSubject : Subject by Delegate()
编译器就会帮我们生成委托模式的代码:
class RealSubject : Subject {
private val delegate: Subject = Delegate()
override fun buy() {
delegate.buy()
}
}
如果是属性的委托:
var name: String by PropertyDelegate()
编译器就会帮我们把属性的get、set方法委托出去:
val delegate = PropertyDelegate()
var name: String
get() = delegate.getValue()
set(value) = delegate.setValue(value)
by关键字后面的表达式可能有各种各样的写法,但一定是返回一个委托的实例。
/ 封装 MMKV /
MMKV很合适属性委托的场景,不过我们不可能只用属性委托进行封装,下面带大家完整地封装一下MMKV。
MMKV初始化
每次都要在Application调用初始化方法有点繁琐,我们可以使用App Startup实现自动初始化。
dependencies {
implementation "androidx.startup:startup-runtime:1.1.0"
}
class MMKVInitializer : Initializer<Unit> {
override fun create(context: Context) {
MMKV.initialize(context)
}
override fun dependencies() = emptyList<Class<Initializer<*>>>()
}
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.dylanc.mmkv.MMKVInitializer"
android:value="androidx.startup" />
</provider>
如果有需要自定义MMKV的根目录,可以配置App Startup移除MMKVInitializer。这样就不执行默认的初始化操作,然后根据需要手动在Application初始化即可。
管理MMKV实例
很多人会封装个工具类把获取MMKV实例的过程简化了,这样就能直接调用encode()和decode()方法。
object MMKVUtils {
private val kv = MMKV.defaultMMKV()
fun encode(key: String, value: Boolean) {
kv.encode(key, value)
}
fun decodeBool(key: String, value: Boolean): Boolean {
return kv.decodeBool(key, value)
}
fun encode(key: String, value: Int) {
kv.encode(key, value)
}
// ...
}
MMKVUtils.encode("bool", true)
val bValue = MMKVUtils.decodeBool("bool")
这种封装有个弊端是写死了默认的MMKV实例,然而MMKV是可以使用多个实例的,比如不同业务需要区别存储:
val kv = MMKV.mmkvWithID("MyID")
这个可以避免组件化项目的不同模块使用了同样的key值导致数据冲突的问题,比如A、B 模块保存位置都用position作为key值,这样数据会有冲突。如果使用了不同的MMKV实例,即使键名一样也不会有问题。
MMKV还能配置多进程访问或者加密,直接写死用默认实例并不好,应该要能配置。下面给出个人的封装方案:
val kv: MMKV = MMKV.defaultMMKV()
interface MMKVOwner {
val kv: MMKV get() = com.dylanc.mmkv.kv // 因为属性重名,要加包名声明是上面的顶级属性
}
只需这么几行代码,就能完美替代上面的工具类。
首先我们声明了一个MMKV的顶级属性,顶级属性能在任意地方获取,因为是最高级别的。就这么一行代码就已经做到上面那个工具类的所有事。
kv.encode("bool", true)
val bValue = kv.decodeBool("bool")
kv.clearAll()
其次我们还声明了一个MMKVOwner接口,可以在需要区别存储的时候,实现该接口,重写kv属性。这样就能在不影响原有代码的情况下,把该类的MMKV实例给替换了。
object UserRepository : MMKVOwner {
override val kv: MMKV = MMKV.mmkvWithID("user")
}
只需简单几行代码的封装,即保留了MMKV的所有功能,又能灵活切换MMKV实例。
使用属性委托
上面是获取MMKV实例的封装,还不够易用。我们能运用前面讲的属性委托进一步优化保存和读取的代码。创建一个类实现ReadWriteProperty接口,在getValue()和setValue()方法调用MMKV的编码解码方法,key值使用property.name 。
class MMKVBoolProperty : ReadWriteProperty<MMKVOwner, Boolean> {
override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): Boolean =
thisRef.kv.decodeBool(property.name)
override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: Boolean) {
thisRef.kv.encode(property.name, value)
}
}
注意thisRef的类型是故意定为MMKVOwner而不是Any?,也就是说必须实现了MMKVOwner接口才能使用MMKV属性委托。
为什么要这么做呢?首先你需要拥有了MMKV才能把属性委托给MMKV ,从逻辑上来说更加合理。其次如果能随意写MMKV属性委托的话,容易出现在多个类里写了同一个值的属性委托,一旦属性名敲错,数据就异常了,这是不推荐的用法。所以要求必须实现MMKVOwner接口,这样在多个类使用属性委托就变得更麻烦。
推荐用法是在一个数据管理类里使用MMKV的属性委托。比如写在Model类或者Repository类,保存和读取操作都是使用同一个属性,这就不会出错,并且能更容易追根溯源。
object DataRepository : MMKVOwner {
var isDarkMode: Boolean by MMKVBoolProperty()
}
// 读取数据
if (DataRepository.isDarkMode) {
...
}
// 缓存数据
DataRepository.isDarkMode = true
使用高阶函数能更好地复用委托类,下面是个人封装好的代码,把MMKV支持的9种数据类型都封装了,代码会有点多。
interface MMKVOwner {
val kv: MMKV get() = com.dylanc.mmkv.kv
}
val kv: MMKV = MMKV.defaultMMKV()
fun MMKVOwner.mmkvInt(default: Int = 0) =
MMKVProperty(MMKV::decodeInt, MMKV::encode, default)
fun MMKVOwner.mmkvLong(default: Long = 0L) =
MMKVProperty(MMKV::decodeLong, MMKV::encode, default)
fun MMKVOwner.mmkvBool(default: Boolean = false) =
MMKVProperty(MMKV::decodeBool, MMKV::encode, default)
fun MMKVOwner.mmkvFloat(default: Float = 0f) =
MMKVProperty(MMKV::decodeFloat, MMKV::encode, default)
fun MMKVOwner.mmkvDouble(default: Double = 0.0) =
MMKVProperty(MMKV::decodeDouble, MMKV::encode, default)
fun MMKVOwner.mmkvString() =
MMKVNullableProperty(MMKV::decodeString, MMKV::encode)
fun MMKVOwner.mmkvString(default: String) =
MMKVNullablePropertyWithDefault(MMKV::decodeString, MMKV::encode, default)
fun MMKVOwner.mmkvStringSet(): ReadWriteProperty<MMKVOwner, Set<String>?> =
MMKVNullableProperty(MMKV::decodeStringSet, MMKV::encode)
fun MMKVOwner.mmkvStringSet(default: Set<String>) =
MMKVNullablePropertyWithDefault(MMKV::decodeStringSet, MMKV::encode, default)
fun MMKVOwner.mmkvBytes() =
MMKVNullableProperty(MMKV::decodeBytes, MMKV::encode)
fun MMKVOwner.mmkvBytes(default: ByteArray) =
MMKVNullablePropertyWithDefault(MMKV::decodeBytes, MMKV::encode, default)
inline fun <reified T : Parcelable> MMKVOwner.mmkvParcelable() =
MMKVParcelableProperty(T::class.java)
inline fun <reified T : Parcelable> MMKVOwner.mmkvParcelable(default: T) =
MMKVParcelablePropertyWithDefault(T::class.java, default)
class MMKVProperty<V>(
private val decode: MMKV.(String, V) -> V,
private val encode: MMKV.(String, V) -> Boolean,
private val defaultValue: V
) : ReadWriteProperty<MMKVOwner, V> {
override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V =
thisRef.kv.decode(property.name, defaultValue)
override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V) {
thisRef.kv.encode(property.name, value)
}
}
class MMKVNullableProperty<V>(
private val decode: MMKV.(String, V?) -> V?,
private val encode: MMKV.(String, V?) -> Boolean
) : ReadWriteProperty<MMKVOwner, V?> {
override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V? =
thisRef.kv.decode(property.name, null)
override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V?) {
thisRef.kv.encode(property.name, value)
}
}
class MMKVNullablePropertyWithDefault<V>(
private val decode: MMKV.(String, V?) -> V?,
private val encode: MMKV.(String, V?) -> Boolean,
private val defaultValue: V
) : ReadWriteProperty<MMKVOwner, V> {
override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V =
thisRef.kv.decode(property.name, null) ?: defaultValue
override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V) {
thisRef.kv.encode(property.name, value)
}
}
class MMKVParcelableProperty<V : Parcelable>(
private val clazz: Class<V>
) : ReadWriteProperty<MMKVOwner, V?> {
override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V? =
thisRef.kv.decodeParcelable(property.name, clazz)
override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V?) {
thisRef.kv.encode(property.name, value)
}
}
class MMKVParcelablePropertyWithDefault<V : Parcelable>(
private val clazz: Class<V>,
private val defaultValue: V
) : ReadWriteProperty<MMKVOwner, V> {
override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V =
thisRef.kv.decodeParcelable(property.name, clazz) ?: defaultValue
override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V) {
thisRef.kv.encode(property.name, value)
}
}
完整用法
个人已经写好了一个MMKV-KTX库,如下所示:
https://github.com/DylanCaiCoding/MMKV-KTX
添加依赖
allprojects {
repositories {
//...
maven { url 'https://www.jitpack.io' }
}
}
dependencies {
implementation 'com.github.DylanCaiCoding:MMKV-KTX:1.2.11'
}
让一个类实现MMKVOwner接口,即可通过by mmkvXXXX()的方式将属性委托给MMKV,例如:
object DataRepository : MMKVOwner {
var isFirstLaunch by mmkvBool(default = true)
var user by mmkvParcelable<User>()
}
设置或获取属性的值就会调用对应的encode()或decode()方法,key值为属性名。支持以下类型:
在MMKVOwner的实现类可以获取kv对象进行删除值或清理缓存等操作:
kv.removeValueForKey(::isFirstLaunch.name)
kv.clearAll()
如果不同业务需要区别存储,可以重写kv属性来创建不同的MMKV实例:
object DataRepository : MMKVOwner {
override val kv: MMKV = MMKV.mmkvWithID("MyID")
}
完整的用法可查看单元测试代码。
/ 总结 /
本文详细地讲解了Kotlin委托的用法和本质,其实就是编译器帮我们生成了委托的代码。然后分享了接口+属性委托的MMKV封装思路,即保留了MMKV的所有功能,又能灵活切换MMKV实例。最后分享了个人实现好的开源库MMKV-KTX,方便大家日常开发使用。
推荐阅读:
PermissionX 1.5发布,支持申请Android特殊权限啦
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注