委托也叫代理,是一种可以以代理方式控制目标对象的访问,设计模式中成为-代理模式。
Java中,我们实现一个代理模式,会有以下对象;
- Base接口,代理和被代理对象都需要实现的接口。
- BaseImpl类,实现Base接口,是被代理的类。
- Derived类,代理类、委托类,也实现了Base接口,一般以构造方法或set方法注入BaseImpl类。
Java实现
- Base接口,被代理类和代理类都需要实现该接口
public interface Base {
void print();
}
- BaseImpl,被代理类
public class BaseImpl implements Base {
private String msg;
public BaseImpl(String msg) {
this.msg = msg;
}
@Override
public void print() {
System.out.println("msg:" + msg);
}
}
- Derived,代理、委托类
public class Derived implements Base {
private Base base;
//构造方法注入被代理类
public Derived(Base base) {
this.base = base;
}
@Override
public void print() {
//复写Base接口中的print()方法,转调被代理类的print()方法
this.base.print();
}
}
Java中并没有对代理模式做特定的封装和语法糖,而实际代理默认很常用。Kotlin为代理模式提供了语法糖,提供了by关键字来方便实现代理模式,by关键字用在代理类中,格式:by 被代理类实例,Kotlin会默认生成代理类覆写所有的Base接口抽象方法,都转调被代理类,如果需要特殊定义,直接复写目标方法即可。
- Base接口,被代理类和代理类都需要实现该接口
interface Base {
fun print()
}
- BaseImpl,被代理类
/**
* 被代理类
*/
class BaseImpl(private val msg: String) : Base {
override fun print() {
println("msg:$msg")
}
}
- Derived,代理、委托类
/**
* 委托类,实现类接口,通过by关键字,代理具体实现,编译会实现所有抽象方法,并且转调给实现类,如果需要修改,直接复写即可
*/
class Derived(private val impl: Base) : Base by impl {
override fun print() {
impl.print()
}
}
接下来介绍Kotlin的委托,Kotlin的委托分为2种:
- 类委托,就是上面那种
- 属性委托,将属性的get、set方法委托给另外一个类
自定义属性委托
自定义属性委托,需要新建一个类,提供getValue()方法和setValue(),并且这个2个方法都是模板,每个代理类都是一样的,只是方法内的处理需要自定义。
class MyDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
//...
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
//...
}
}
属性委托和类委托一样,也是使用by关键字,但是类委托是类名,by关键字后面跟着实现类实例名,而属性委托中是属性后,by关键字后面跟着代理类的类名。
实例:
新建一个代理类MyDelegate,添加getValue()和setValue()方法,这2个方法,我们简单打印一下thisRef被代理类的引用和property属性对象。Example类为被代理类,给msg属性,通过by关键字指定被MyDelegate代理。main()方法中,我们给Example的实例的msg属性调用set()、get()方法,就会转调到MyDelegate定义的setValue()和getValue()
class Example {
var msg: String by MyDelegate()
}
/**
* 建立一个委托类,编译器会生成Example的get、set方法转调MyDelegate中的getValue和setValue,再将结果返回
*/
class MyDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 这里委托了 ${property.name} 属性"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef 的 ${property.name} 属性赋值为 $value")
}
}
fun main(args: Array) {
//属性代理
val example = Example()
//访问属性的get方法
println(example.msg)
//访问属性的set方法
example.msg = "你好"
}
//输出
com.wally.hellokt.parttwo.Example@1a6c5a9e, 这里委托了 msg 属性
com.wally.hellokt.parttwo.Example@1a6c5a9e 的 msg 属性赋值为 你好
属性代理实践:
安卓开发中,Activity、Fragment之间的Intent数据传值,写起来比较繁琐,会写很多getXXExtra()的代码,而借助Kotlin的属性代理,可以更优雅的实现,将变量声明和参数获取合二为一。
我们新建一个Argument类(代理类),我们可以为Activity、Fragment提供拓展方法bindArgument,给属性声明代理给Argument,getValue()和setValue()分别从Activity、Fragment中获取Bundle实例,获取对应的数据即可。
/**
* 拓展Activity获取方法
*/
fun Activity.bindArgument(name: String, default: T): Argument {
return Argument(name, default) {
this.intent?.extras ?: Bundle()
}
}
/**
* 拓展Fragment获取方法
*/
fun Fragment.bindArgument(name: String, default: T): Argument {
return Argument(name, default) {
this.arguments ?: Bundle()
}
}
/**
* 属性代理,代理到Bundle上
* @param name 属性名
* @param default 默认值
* @param block 获取Bundle对象的闭包
*/
class Argument(private val name: String, private val default: T, private val block: () -> Bundle) : ReadWriteProperty {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = findArgument(name, default)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = putArgument(name, value)
@Suppress("UNCHECKED_CAST")
private fun findArgument(name: String, default: U): U = with(this.block()) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)!!
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
is Serializable -> getSerializable(name) ?: default
is Parcelable -> getParcelable(name) ?: default
else -> throw IllegalArgumentException("This type can be saved into Argument")
}
res as U
}
private fun putArgument(name: String, value: U) = with(this.block()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
is Serializable -> putSerializable(name, value)
is Parcelable -> putParcelable(name, value)
else -> throw IllegalArgumentException("This type can be saved into Argument")
}
}
}
- 代理使用,我们从第一个Activity跳转到第二个Activity,Intent配置了个一个问题Id,key为AskTeacherConstant.Extra.QUESTION_ID,默认值为空字符串,声明的变量为mQuestionId。省去了问题Id在Intent中getStringExtra()的获取,更加简洁。
private val mQuestionId: String by bindArgument(AskTeacherConstant.Extra.QUESTION_ID, "")
标准委托
委托作为Kotlin的一大特性,提供了一些标准委托给予我们使用。
lazy懒加载委托
使用lazy委托,需要我们提供一个Block闭包,Block闭包内写我们变量的初始化逻辑,这个Block只有第一个调用时初始化,后续调用则直接使用第一次的返回值。这样可以很好的节省内存。
- 格式为:
val(var) 变量名 by lazy {
初始化逻辑...
}
下面我们使用一个例子来讲解,例如一个Human类,内部有一个friendList变量,它是一个好友列表,加载比较耗时,main()方法中我们访问friendList变量,希望第一次获取时才初始化,第二次以及以后不会重复加载,复用之前的结果。
fun main(args: Array) {
//懒加载委托,生成一个闭包,包裹获取的值的代码,调用被代理的属性的get方法时调用,然后保存值起来,第二次调用则直接返回
val human = Human()
println("---------------- 第一次加载,才加载数据 ----------------")
println(human.friendList)
println("---------------- 第二次加载,直接输出,不会重复加载 ----------------")
println(human.friendList)
}
class Human {
/**
* 好友列表
*/
//默认模型为LazyThreadSafetyMode.SYNCHRONIZED同步,如果不需要则可以使用NONE标识不不要保证线程安全
val friendList by lazy(mode = LazyThreadSafetyMode.NONE) {
loadFriendList()
}
//这里模拟,加载比较耗时
private fun loadFriendList(): MutableList {
println("---------------- 加载耗时的好友列表 ----------------")
return mutableListOf("Wally", "Barry", "Rose")
}
}
//输出
---------------- 第一次加载,才加载数据 ----------------
---------------- 加载耗时的好友列表 ----------------
[Wally, Barry, Rose]
---------------- 第二次加载,直接输出,会重复加载 ----------------
[Wally, Barry, Rose]
lazy关键字后有一个mode字段,默认不写则为同步,所以是线程安全的,如果确保不会涉及多线程的问题,则可以设置为LazyThreadSafetyMode.NONE,提高性能。
observable监听属性的改变
Kotlin还提供了一个observable委托,让我们能监听到属性值的改变,类似MVVM中的属性值改变,UI组件监听自动改变。同样需要我们提供一个block闭包来回调我们,提供属性值、旧值和新值给我们。
- 格式为:
val(var) 变量名 by Delegates.observable(初始化值) {
property, oldValue, newValue -> ...
}
举一个列子,例如MyPerson类中有一个name属性,初始化值为Wally,我们让它的值改变为Barry,我们的委托Delegates.observable就会回调我们的Block,提供属性值、旧值和新值给我们。
fun main(args: Array) {
//属性观察,可以监听属性的新、旧值,但不能改变
val person = MyPerson()
person.name = "Barry"
}
class MyPerson {
var name: String by Delegates.observable("Wally") { property, oldValue, newValue ->
println("对象属性值发生改变 -> 属性名:${property.name},旧值:$oldValue,新值:$newValue")
}
}
//输出
对象属性值发生改变 -> 属性名:name,旧值:Wally,新值:Barry
vetoable监听属性改变,并且可以拦截
vetoable和observable类似,同样可以监听到属性值的改变,但是vetoable可以拦截值的改变,而observable只能获取不能拦截。也需要我们提供一个block闭包,返回true为拦截,不允许改变,返回false,允许改变。
- 格式为:
val(var) 变量名 by Delegates.observable(初始化值) {
property, oldValue, newValue -> ...
//满足某种条件,返回true代表拦截,不允许改变
if(xxx) {
true
} else {
//返回false为不拦截,允许改变
false
}
举个例子,和上个observable类型,监听Person2类中的name属性,默认值为Wally,我们让它的值改变为Barry,我们的委托Delegates.vetoable就会回调我们提供的block,提供属性值,旧值和新值,要求我们返回一个boolean布尔值,我们判断新值为Barry返回true,不允许改变,其他情况返回false允许改变。
fun main(args: Array) {
//属性改变监听+拦截(保留原来的值)
val person2 = Person2()
person2.name = "Barry"
println("name的值为:${person2.name}")
person2.name = "Wally2"
println("name的值为:${person2.name}")
}
class Person2 {
var name: String by Delegates.vetoable("Wally") { property, oldValue, newValue ->
println("对象属性值发生改变 -> 属性名:${property.name},旧值:$oldValue,新值:$newValue")
//如果设置为Barry不允许设置
if (newValue == "Barry") {
println("!!!!!! <不允许设置为Barry> !!!!!!")
false
} else {
true
}
}
}
//输出
对象属性值发生改变 -> 属性名:name,旧值:Wally,新值:Barry
!!!!!! <不允许设置为Barry> !!!!!!
name的值为:Wally
对象属性值发生改变 -> 属性名:name,旧值:Wally,新值:Wally2
name的值为:Wally2
notNull非空代理
如果我们期望某个属性值使用前必须赋值,那么我们可以对属性值做可空类型,但是可空类型会导致我们使用时必须判空,到处的判空代码,代码会显得十分丑陋,可空属性是用在某几种阶段可用时使用,如果我们的属性赋值一次后就可以一直使用,那么可以使用Delegates.notNull委托。对属性使用Delegates.notNull委托,必须使用前赋值一次,否则会抛出异常。
格式为:
var 变量名 by Delegates.notNull()
举个例子,安卓开发中,一般我们会让Application类作为单例,方便后续直接使用,而App单例的实例在onCreate()中才初始化,那么就需要设置为可空属性,那么App.instant属性就需要每次使用时都判空,而我们知道他不会为空,希望不用判空直接使用,那么我们就可以使用Delegates.notNull委托。
class App :Application() {
companion object {
var instant: App by Delegates.notNull()
}
override fun onCreate() {
super.onCreate()
instant = this;
}
}
class MainActivity: Activity{
override fun onCreate() {
super.onCreate()
val application = App.instant;
}
}
总结
委托在Kotlin是一个很重要的特性,通过委托,我们可以做到很多事情,例如findViewById能委托为一个委托类获取,而可以书写得像声明属性一样。再或者让Sp保存、获取也可以做成变量声明的形式,以及获取Activity、Fragment的Bunlde参数。Kotlin还为我们提供了标准委托,例如lazy懒加载让我们的属性在第一次获取时才输出,但是变量可以书写为变量声明的方式,还有observable、vetoable代理,让我们监听到属性改变,还可以作为MVVM模式下的ViewModel的补充。