获取viewModel的新方法
旧方法
有新方法肯定要先介绍一下旧方法。
在传统的viewModel获取中,我们都有这样一个经验——不能在Activity里直接创建viewModel对象。因为ViewModel的生命周期是长于Activity的,如果在Activity的方法内直接创建对象,就失去了viewModel的设计意义了,即独立于activity的生命周期,存放activity数据的仓库。
因此,我们要采用一些外部的方法进行初始化。
class MainActivity : AppCompatActivity() {
lateinit var viewModel:MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ❗ 看下面
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
// ❗ 看上面
findViewById<Button>(R.id.button).setOnClickListener {
viewModel.printSomething(System.currentTimeMillis().toString())
}
}
}
我们把viewModel定义成一个延迟初始化变量,在onCreate里进行初始化。
有人可能要问了,你这不也是每次重新创建activity时都会调用创建viewModel语句吗?重新创建了里面的数据不也没了?
其实并非如此,而且答案正好回答了为什么不用直接的MainViewModel()
来创建对象。
下面我们来看一下这句初始化代码,这个是摘录《第一行代码Android》(第三版)里的创建viewModel方式
ViewModelProviders.of(this).get(MainViewModel::class.java)
但是实际上这种写法已经被废弃了,查看官网链接可以看到已经被ViewModelProvider取代了。
那么新的写法怎么写呢?
答案如下,这种写法能保证每次获取到的viewModel都是第一次初始化的,从而达到保存数据的作用。
viewModel = ViewModelProvider(this,
ViewModelProvider.NewInstanceFactory())
.get(MainViewModel::class.java)
这种写法又是怎么确保viewModel不会被多次初始化的?
这里引入一个知识点:activity会通过一些手段来存储viewModel到其一个ViewModelStore类型的对象里,但是这个存储对象我们一般不直接操作,而是交给ViewModelProvider来操作。如果你愿意,你当然可以直接操作这个ViewModelStore来获取viewModel,因为这就相当于你做了ViewModelProvider的工作了,而且大概率没它做得好(各种错误处理机制的实现)。
OK,回到问题本身。通过传入的参数this,可以获取到当前activity的ViewModelStore,从而获取到上一次初始化的viewModel,避免了多次初始化。想更详细地了解可以参考以下文章:深入AAC架构,彻底理解ViewModel的用法
新方法
???怎么现在才到新方法?
是的,在新的版本中,聪明的开发者发现上一种写法太笨了,要写的代码太多了。我们要声明一个延时初始化变量,然后调用上述方法进行获取。能否对其进行精简?答案当然是可以的。而且很幸运,换汤不换药,只要你基本理解了上述获取ViewModel对象过程,就能理解新写法。为了体现新写法和后面要讲的内容重要性,我把两段代码都列了出来。
// 又长又麻烦
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
.get(MainViewModel::class.java)
}
}
新写法
// 对onCreate代码0侵入
class MainActivity : AppCompatActivity() {
// ❗ 看下面
private val mainViewModel by viewModels<MainViewModel>()
// ❗ 看上面
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
这样的新写法是由kotlin提供的,viewModels函数实际上是一个扩展函数,想要使用这样的新写法,要在导入以下依赖
implementation 'androidx.activity:activity-ktx:1.2.2'
很简洁不是吗?这就是我喜欢kotlin的原因,它真正地站在了开发者的角度,就像一个大神,用很高的代码水平帮你实现复杂的功能。坦白说这样有点偷懒的嫌疑,但四舍五入就是JetBrain帮我打工,这波不是血赚?
言归正传,我们注意到上面这一句核心代码就实现了之前的一大段
private val mainViewModel by viewModels<MainViewModel>()
/*
等价于
lateinit var mainViewModel: MainViewModel
mainViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
.get(MainViewModel::class.java)
*/
下面就来剖析一下这行代码。但是心急不行,先了解一下什么是委托模式。
什么是委托模式?
从词义上看,by就是“通过”,”经由“的意思。这里是委托模式的一个应用。
那么委托模式到底是什么呢?
请求别人替我完成某些工作,这样的思想就是委托。在Kotlin里,委托有两种,分别是属性委托和类委托。
- 类委托:一个类,定义时实现了某个接口,就可以把该接口委托给一个对象(该对象可以通过构造函数传入),委托会自动进行拦截,把调用这个接口的操作转发给该对象。
- 属性委托:把一个变量通过by关键字,委托给别人操作。我们知道,对于一个变量而言,只有两个操作,分别是获取这个变量和修改这个变量,也就是
get()
和set()
。委托就是把这两个操作进行拦截,然后分发给指定的委托对象进行执行。
以上是通俗的理解,下面用几个例子来说明一下。
类委托应用场景
设想有这样的场景:我需要设计一个类,它需要具有MutableMap接口的所有方法和功能,但是比较特别的一点是,这个类有一个支持撤销的功能,即调用recover方法,把上一次put进来的东西删除。
朴素的想法是,自己定义一个MyMap类,实现Map接口,内部维护一个实际的HashMap对象(因为我们实际上也是对其进行一点增强)。记录下每次put进来的key,然后添加一个recover()
函数进行处理,调用recover()
其实就是移除map中lastKey。好,我们来试试看。
可以看到,由于实现了Map接口,编译器会提示我们实现好几个函数。我们当然可以用编译器的自动生成来构建起需要的类,可实际上呢?我们只关心put()
和一个自定义的recover()
函数的执行。硬着头皮去实现就不优雅了,Map接口方法少可以,List接口呢?
于是就有了委托这一实现。
class MyMap(private val realMap: HashMap<Int, Int>) : MutableMap<Int, Int> by realMap {
private var lastKey = 0
override fun put(key: Int, value: Int): Int? {
lastKey = key
return realMap.put(key,value)
}
fun recover() {
realMap.remove(lastKey)
}
}
是不是很优雅?这里实际操作的map通过构造函数传入,通过by关键字把MutableMap的方法都委托给这个realMap,我们这个类中只关心自己的实现就可以了。看到这里也许就有同学说了,这个用扩展函数也可以完成啊!这个在代码里直接用remove也可以啊!我之前也是这么想的,后来发现所有设计模式本质上都可以通过最基础的代码完成,委托更像是一种思想、一种方式,你当然可以另辟蹊径,就像你当然可以自己重写一个map一样,只要最贴切、最完善地完成目标即可,这也是所有编码工作的核心要义。
属性委托的应用场景
理解了类委托,属性委托也就不难了。
先要做这样一个理解:本质上来说,对一个对象只有两个用法,一个是赋值,一个是取值。看以下代码。
var s = "1"
//取s指向的对象,然后调用方法
s.isBlank()
// 赋值给s
s = "2"
可以类比为一个类中的private成员变量,对于这个变量,无论你想调用它的什么函数,都要先调用它的get()方法,然后再操作。你想对它赋值,就要调用set(XX)操作。
Kotlin中的val也是同理,不过变成了只能读取,不能set的变量而已。也就是只有get方法,没有set方法。
有了这个理解,就可以看看属性委托了。
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("代理对象来喽,"+"当前时间:"+System.currentTimeMillis())
return "abcdefg"
}
}
fun main() {
val s: String by Delegate()
s.length
}
试想这样一个情况,我需要每次调用s的函数时都打印当前系统时间,在主函数中,我们没有直接给s赋值或者初始化,我们把这个工作交给了一个Delegate对象。Delegate对象内部重写了getValue
这个方法。当我们每次调用s的时候(注意这里指的调用不仅仅是输出其具体内容,还包括s.length()
等一系列方法的调用,原因见前文),就会被拦截,转而调用getValue方法。所以,以上代码的运行结果为
代理对象来喽,当前时间:1618404268384
Process finished with exit code 0
“by” 关键字到底是啥
说完了两种委托,我们发现其都有一个关键的地方,都是通过by这个关键字实现的。真的是这样吗?Java又加新关键字了?说加就加?我怎么没听说过?
这就是Kotlin的优点之一了,可以快速响应社区需求。by做了什么工作呢,我们尝试将上面的代码进行反编译成Java代码,也称得上是官方(JVM)对这个by关键字的理解了。
public final class TKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property0(new PropertyReference0Impl(TKt.class, "s", "<v#0>", 1))};
public static final void main() {
// 注释1
Delegate var10000 = new Delegate();
KProperty var1 = $$delegatedProperties[0];
Delegate s = var10000;
// 注释2
s.getValue((Object)null, var1).length();
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
前面的Delegate可以理解为一个普通的类(实际上和普通的类也没区别,只是重载了一个getValue方法罢了),就没列出来。
关键在于这个main函数。可以看到,注释1处创建了一个Delegate对象,并且声明了一个s指向这个对象。
这不就是我们前面声明的s吗?它不是被委托给Delegate了吗?对对对,别急,下面就来。
看到注释2,可以发现调用s.length
实质上调用了delegate对象里的getValue()
方法(还传了一系列参数,但是这两个参数在这个例子的影响不大,不详细讨论),执行了方法里的打印时间,返回了“abcdefg”。然后调用字符串的length()
函数。
一切都已经明了~
反编译之后理解了实际过程,那么我们也可以“多此一举”地尝试一下自己实现,就彻底了解“by”关键字的作用了。
class Delegate {
// operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
// println("代理对象来喽,"+"当前时间:"+System.currentTimeMillis())
// return "abcdefg"
// }
fun myDelegate():String{
println("代理对象来喽,"+"当前时间:"+System.currentTimeMillis())
return "abcdefg"
}
}
fun main() {
val delegate = Delegate()
val s = delegate
s.myDelegate().length
}
本质上这个实现和原来的by写法是基本一样的(省略了getValue的传参),自己的写法每次调用s.myDelegate()
,得到返回的结果再操作不是不行,还是那句话,不够优雅。委托让我们能暂时忘了它到底是什么,让我们可以把s直接当成String来处理,剩下的让编译器去想就好了,操心那么多干嘛呢~
总结
委托模式是一种抽象的设计模式,在我看来,它是代理模式的一个超集,某些时候,委托回退化成代理模式,就如本文中的例子一样,逻辑较为简单的时候,我们也可以用代理的方式实现。
委托适用的场景是对所需对象的大部分实现细节不需要了解,仅需要改变一小部分功能的情况。
by关键字是一个面向编译器的声明,做了以下工作:
- 把委托对象(对应上面例子中的
s
)实例化为一个被委托对象(对应例子中的Delegate对象) - 每次对委托对象的方法调用,都会被转换成调用被委托对象的
getValue()
方法,该方法返回实质调用对对象 - 实质调用对对象执行原来的调用(对应例子中的
length
方法)
Kotlin真的太甜了~