看似普通的Android开发黑科技 - Kotlin 委托

1. 前言

我们都知道委托模式也叫代理模式,指的是一个对象接收到请求之后将请求转交由另外的对象来处理。
Kotlin直接支持委托模式,kotlin通过关键字by实现委托,更加优雅,简洁。

我们在使用Kotlin开发的时候,经常会使用by来实现一些操作,使用by lazy来实现懒加载。
虽然日常用的比较多,但却不明白其原理,lazy又代表着什么 ?
还有看似普通的by,实则可以实现很多的功能,可以更方便我们的开发。
接下来,我们来陆续回答这些问题。

Kotlin中,委托分为类委托属性委托

2. 类委托

类委托的核心思想是将一个类的一些具体实现委托给另一个类去完成。
我们新建一个接口Base,声明方法print()

interface Base {
    fun print()
}

然后创建一个实现类Impl1

class Impl1() : Base {
    override fun print1() {
        Log.i("TAG", "------打印了print1------")
    }

    override fun print2() {
        Log.i("TAG", "------打印了print2------")
    }
}

接着,创建一个实现类Impl2,其实现会委托给Impl1
同时,重写了print1方法,这意味着print1会由Impl2自己来实现

class Impl2(base: Base) : Base by base {
    override fun print1() {
        Log.i("TAG", "------重写了print1------")
    }
}

运行程序,可以发现打印如下内容

TAG: ------重写了print1------
TAG: ------打印了print2------

3. 属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代理类,从而实现对该类的属性统一管理。

3.1 属性委托的示例

首先,我们需要先定义一个被委托类,实现setValue / getValue方法,这是使用by的一个约定,否则是无法使用by的。
注意 : 如果是只读的(val)只需实现getValue(),如果读写都需要(var),setValue / getValue都需要实现

class Delegate {
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("$thisRef, 这里委托了 ${property.name} 属性")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef${property.name} 属性赋值为 $value")
        this.value = value
    }
}

需要注意的是setValue / getValue方法的几个参数

  • thisRef:必须与 属性所有者 类型(对于扩展属性 - 指被扩展的类型)相同或者是它的超类型
  • property:必须是类型 KProperty<*>或其超类型
  • value:必须与属性同类型或者是它的子类型

接着定义包含属性委托的类

class Example {
    var p: String by Delegate()
}

进行调用

fun main() {
    val example = Example()
    println("第一次打印:" + example.p) // 访问该属性,调用 getValue() 函数

    example.p = "Heiko" // 调用 setValue() 函数
    println("第二次打印:" + example.p)
}

可以看到打印的日志如下

I/System.out: com.heiko.koltintest.Test2$Example@9c2d655, 这里委托了 p 属性
I/System.out: 第一次打印:
I/System.out: com.heiko.koltintest.Test2$Example@9c2d655 的 p 属性赋值为 Heiko
I/System.out: com.heiko.koltintest.Test2$Example@9c2d655, 这里委托了 p 属性
I/System.out: 第二次打印:Heiko

3.2 将委托属性和被委托类合并到一个类中

class Example {
    var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("$thisRef, 这里委托了 ${property.name} 属性")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef${property.name} 属性赋值为 $value")
        this.value = value
    }
}

fun main() {
    var e by Example()
    println(e) // 访问该属性,调用 getValue() 函数

    e = "Heiko" // 调用 setValue() 函数
    println(e)
}

打印日志如下

I/System.out: null, 这里委托了 e 属性
I/System.out: null 的 e 属性赋值为 Heiko
I/System.out: null, 这里委托了 e 属性
I/System.out: Heiko

3.3 结合inline内联函数一起使用

class Example {
    var value: String = ""
}

inline operator fun Example.getValue(thisRef: Any?, property: KProperty<*>): String {
    println("$thisRef, 这里委托了 ${property.name} 属性")
    return value
}

inline operator fun  Example.setValue(
    thisRef: Any?,
    property: KProperty<*>,
    value: String
) {
    println("$thisRef${property.name} 属性赋值为 $value")
    this.value = value
}

fun main() {
    var example by Example()
    println(example) // 访问该属性,调用 getValue() 函数

    example = "Heiko" // 调用 setValue() 函数
    println(example)
}

打印日志如下

I/System.out: null, 这里委托了 example 属性
I/System.out: null 的 example 属性赋值为 Heiko
I/System.out: null, 这里委托了 example 属性
I/System.out: Heiko

3.4 一种更简单的方式

实现属性委托,需要实现getValue/setValue方法,这相对麻烦,也容易写错,为此,kotlin为我们提供了operator方法的 ReadOnlyProperty/ReadWriteProperty 接口。

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
  • ReadOnlyProperty : val属性实现这个
  • ReadWriteProperty : var属性实现这个

我们来修改下代码

class Example : ReadWriteProperty<Any?, String> {
    var value: String = ""

    override operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("$thisRef, 这里委托了 ${property.name} 属性")
        return value
    }

    override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef${property.name} 属性赋值为 $value")
        this.value = value
    }
}

fun main() {
    var e by Example()
    println(e) // 访问该属性,调用 getValue() 函数

    e = "Heiko" // 调用 setValue() 函数
    println(e)
}

可以发现运行也是一样的

4. kotlin标准库中提供的几种委托

  • 延迟属性(lazy): 其值只在首次访问时才进行初始化
  • 可观察属性(observable / vetoable): 监听器会收到有关此属性变更的通知
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中
  • 委托给另一个属性(by this::xxxx) : 用来以一种向后兼容的方式重命名一个属性的时候
  • notNull : 适用于那些无法在初始化阶段就确定属性值的场合

4.1 延迟属性 Lazy

lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy()lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

lazy还可以传入参数 (如果不传入参数,那么使用的是LazyThreadSafetyMode.SYNCHRONIZED)

public enum class LazyThreadSafetyMode {
    SYNCHRONIZED,
    PUBLICATION,
    NONE,
}
  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode. PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

4.2 可观察属性 Observable

Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值与新值:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}
4.2.1 vetoable 函数

vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以控制返回值来决定属性是否生效

4.3 将属性储存在映射中

一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

在这个例子中,构造函数接受一个映射参数

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

委托属性会从这个映射中取值(通过字符串键——属性的名称)

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

这也适用于 var 属性,需要把只读的 Map 换成 MutableMap

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

4.4 委托给另一个属性

从 Kotlin 1.4 开始,一个属性可以把它的 getter 与 setter 委托给另一个属性。这种委托对于顶层和类的属性(成员和扩展)都可用。该委托属性可以为

  • 顶层属性
  • 同一个类的成员或扩展属性
  • 另一个类的成员或扩展属性

为将一个属性委托给另一个属性,应在委托名称中使用恰当的 :: 限定符,例如,this::delegate 或 MyClass::delegate。

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt
    
    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

这是很有用的,例如,当想要以一种向后兼容的方式重命名一个属性的时候:引入一个新的属性、 使用 @Deprecated 注解来注解旧的属性、并委托其实现。

class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}

fun main() {
   val myClass = MyClass()
   // 通知:'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

4.5 notNull

notNull适用于那些无法在初始化阶段就确定属性值的场合

class Foo{
     var notNullBar:String by Delegates.notNull<String>()
}

foo.notNullBar="bar"
println(foo.notNullBar)

需要注意,如果属性在赋值前就被访问的话则会抛出异常

5. ViewBinding和kotlin委托结合使用

除了以上的功能,还有很多地方可以用到kotlin委托,比如我们可以用来封装SharedPreferences,View属性获取/设置的委托,当然,还能和ViewBinding结合使用,使ViewBinding的声明更加便捷。

以前我们使用ViewBinding,需要如下定义

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    private val binding : ActivityMainBinding by viewBinding(ActivityMainBinding::bind)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}

我们可以使用Kotlin委托去实现ViewBindinginflate过程

class ActivityViewBindings<in A : ComponentActivity, out T : ViewBinding>(private val viewBinder: (A) -> T) :
    ReadOnlyProperty<A, T> {
    override fun getValue(thisRef: A, property: KProperty<*>): T {
        return viewBinder(thisRef)
    }
}

public inline fun <A : ComponentActivity, T : ViewBinding> ComponentActivity.viewBinding(
    crossinline vbFactory: (View) -> T,
    crossinline viewProvider: (A) -> View = ::findRootView
): ActivityViewBindings<A, T> {
    return ActivityViewBindings { activity -> vbFactory(viewProvider(activity)) }
}

fun findRootView(activity: Activity): View {
    val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
    checkNotNull(contentView) { "Activity has no content view" }
    return when (contentView.childCount) {
        1 -> contentView.getChildAt(0)
        0 -> error("Content view has no children. Provide a root view explicitly")
        else -> error("More than one child view found in the Activity content view")
    }
}

然后进行使用,就可以省略ActivityMainBinding.inflate()这一步了

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    private val binding : ActivityMainBinding by viewBinding(ActivityMainBinding::bind)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}

当然,这只是最粗略的写法,还有其他更简单的写法,具体详见我的另一篇博客 ViewBinding与Kotlin委托结合使用,去除setContentView,其实现原理解析

6. 实现一个自己的by lazy

我们来写个伪代码,来实现一个自己的by lazy,实现的效果如下

private val hello : String by myLazy {
    "hello"
}

6.1 定义MyLazy接口

首先,我们定义一个接口叫做MyLazy

public interface MyLazy<out T> {
    public val value: T
}

6.2 实现Kotlin委托

然后,按照Kotlin委托的规则,我们需要实现getValue()

public inline operator fun <T> MyLazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

6.3 实现MyLazy接口实现类

然后,定义MyLazy接口的实现类SynchronizedMyLazyImpl

internal object UNINITIALIZED_VALUE

private class SynchronizedMyLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : MyLazy<T> {
    private var initializer: (() -> T)? = initializer
    //用来保存值,如果已经被初始化,就不是默认值了
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    private val lock = lock ?: this

    override val value: T
        get() { //其实就是一个双重校验锁实现的单例
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                //当value已初始化,直接返回其值
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                //通过加锁的方式,再次进行判断value是否已初始化
                if (_v2 !== UNINITIALIZED_VALUE) {
                    //当value已初始化,直接返回其值
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    //当value未初始化,调用 initializer!!() 进行初始化
                    val typedValue = initializer!!()
                    //赋值 _value
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
}

6.4 定义函数myLazy

定义函数myLazy,参数需要传入initializer: () -> T

public fun <T> myLazy(initializer: () -> T): MyLazy<T> = SynchronizedMyLazyImpl(initializer)

6.5 进行使用

然后进行使用就可以了

private val hello : String by myLazy {
    "hello"
}

推荐我的另一篇文章 :
ViewBinding与Kotlin委托结合使用,去除setContentView,其实现原理解析

参考
一文彻底搞懂Kotlin中的委托
委托 - Kotlin 语言中文站
Kotlin by 关键字
Kotlin常用的by lazy你真的了解吗
Kotlin原理-by关键字
kotlin by 关键字用法及使用场景

### 回答1: Android开发中使用Kotlin语言开发Material Design项目可以带来很多好处。Kotlin是一种现代化的编程语言,它可以提高开发效率、减少代码量、提高代码可读性和可维护性。同时,Material Design是一种现代化的设计语言,它可以提高用户体验、提高应用的可用性和可访问性。因此,使用Kotlin开发Material Design项目可以使应用更加现代化、高效、易于维护和易于使用。 ### 回答2: 随着移动设备的迅速普及,Android操作系统已经成为全球最流行的移动操作系统之一。这使得Android应用程序的开发变得越来越受欢迎,许多开发人员也开始学习和掌握这个平台。随着时间的推移,开发人员也在不断寻找最好的解决方案来创建优秀的应用程序,其中kotlin和material design就是两种最受欢迎的选择。 Kotlin是一种高级编程语言,它是Java虚拟机的官方语言之一。KotlinAndroid开发中的流行程度日益增加,因为它具有许多特性,如可空类型、lambdas、扩展函数等,使得开发Android应用程序更加便捷和高效。Kotlin支持Java虚拟机,并非Android特定的开发语言,因此具有更广泛的用途,可以与其他语言无缝集成。同时,Kotlin还有很多实用特性,如可空类型、类型推断、lambda表达式等,能够在Android开发中大幅提高开发效率,在代码中减少了很多荣誉的代码和冗长表达式的麻烦。 Material Design是一种设计语言,由Google推出,旨在为移动和Web应用程序提供一致的极致体验。Material Design提供了一系列的设计指南、模式和组件,以便开发人员可以为他们的应用程序在不同平台和设备上提供一致的体验,从而使应用程序更加具有现代感和吸引力。 Material Design为Android开发者提供了一些标准的界面组件,如浮动操作按钮、抽屉式导航等,同时也支持进行自定义设计,为应用程序增加独特的特色。 综合考虑,使用Kotlin和Material Design组合开发Android应用程序可以有很多好处。Kotlin可以使代码更加简洁,同时使用Material Design的组件和元素可以使应用程序显得更加现代和美观。 这种组合还可以提高开发效率,减少代码中的bug,在Android平台上提供更好的用户体验,从而为应用程序的成功打下坚实的基础。总的来说,采用这种开发方式的应用程序将具有更高的可维护性和可扩展性,也将在市场上拥有更高的竞争力。 ### 回答3: 在当前的移动应用开发市场中,Android系统已经成为了主流之一,其开发工具也被越来越多的开发者采用。而Kotlin语言作为一种新兴的编程语言,因其简洁、安全、互通性和易用性等特点,在Android系统开发中越来越受到开发者的欢迎。 在这种情况下,开发Material Design项目需要用到的技术和工具也应该是非常有趣的。 Android系统采用Material Design作为其UI设计风格,提供了一套完备的UI组件库。要开发Material Design的应用程序,需要遵循Google的Material Design规范以及使用相关的Android开发API和工具。Kotlin语言提供了很多的便利,在使用Android开发API和工具的同时,还提供了方便的语法和Lambda表达式。 使用Kotlin开发Material Design项目还可以提高应用程序的安全性,避免因类型不安全、空指针等问题导致的错误。Kotlin还支持函数式编程,可以提供更好的编写UI代码的方法,帮助开发人员高效编写代码,提高开发效率。 Kotlin增加了一些与Java不同的特性,使得开发者能够更快、更方便地编写代码。例如,Kotlin具有空安全机制,可帮助开发者尽早发现和解决可能导致应用程序崩溃的问题,从而提高应用程序的质量。Kotlin还提供了lambda表达式、扩展功能和集合操作,更方便开发人员在项目中增加新特性,缩短应用开发周期。 虽然Kotlin开发Material Design项目相对于Java来说还是一个相对新的领域,但随着Kotlin使用率的逐渐增加,越来越多的开发者正在使用和探索KotlinAndroid开发中的应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

氦客

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值