Android ViewModel 作为 LifecycleOwner 落地的思考

Context

很早之前在项目中实现了一套从 ViewModel 获得生命周期 LifecycleOwner 的机制,最近引入到新项目中,也正好抽空整理分享一下以供其他同学参考,一起讨论一下 :P

先说一下背景,LifecycleOwner/Lifecycle 作为 Google Android Jetpack 的核心设计底座,不得不说真的是个非常牛逼和先进的 Idea. 从我个人理解来说,一切具有生命周期特征的对象理论上都可以抽象出 Lifecycle,于是有了下面的内容。

Why do this?

在之前的项目中,我负责的业务中基本上是比较严格地按照 MVVM 的模式编写代码(即在 VM 中只负责接受用户事件并转换成业务意图分发给 biz model,同时维护业务场景数据和 UI 状态)。

而在实际开发的过程中会遇到这种场景:在 ViewModel 中调用某些业务 modelAPI 时需要提供 Lifecycle/LifecycleOwner.

(其实这个也算是比较常见的 case 了,比如调用某个业务 service 接口需要透传生命周期相关的参数)

由于个人原因我很讨厌在有更优选择的情况下增加代码耦合(像是给 ViewModel 增加方法 注入来自 Fragment/ActivityLifecycleOwner 之类的操作)。例如下面这种写法:

Kotlin 复制代码
class XXFragment {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        viewModel.bindLifecycleOwner(this)
    }
}

class XXViewModel {
    ...
    override fun onCleared() {
        ...
        this.hostLifecycleOwner = null
    }
    
    fun bindLifecycleOwner(lifecycleOwner: LifecycleOwner) {
        this.hostLifecycleOwner = lifecycleOwner
        lifecycleOwner.doOnDestroy { this.hostLifecycleOwner = null }
    }
}

为了满足业务场景需求,同时又不想增加这种很丑陋且没必要的函数,我就开动脑筋灵感一闪:ViewModel 这个东西,按照我对生命周期的理解其实也是一个具有生命周期的对象,为什么不能抽象成 LifecycleOwner 呢?

在经过了仔细思考之后,我确定这是一个完全可行的方向和做法。

开搞...

How to do it?

在确定了方向之后做法就比较明确了:我希望 ViewModel 可以作为 LifecycleOwner 相对单独存在,即可以从 ViewModel 实例直接获得生命周期相关的对象。

换句话说,其实需要做的事情就只有一个:当我有一个 ViewModel 的实例时,可以通过类似 viewModel.getLifecycleOwner() 或者 viewModel.getLifecycle() 的方式获得非空、可靠的生命周期对象。

(很简单对吧 :)

带生命周期的 ViewModel

首先 ViewModel 作为 Jetpack-lifecycle 套件提供给应用开发的基础组件,它只是一个普通的 Generic Java Class. 我们在业务开发中是不太能够对它作出修改的。

这里防杠一下,如果说通过 ASM/KCP 之类的方式修改编译产物当然是可以

怎么说,可以但没必要,而且有大炮打蚊子的嫌疑,阿巴阿巴...

so... 那么既然不能直接修改 ViewModel,那就换个思路,在参考了 Jetpack 套件的部分实现后,很快发现了两个比较快速的方法:

  1. 自定义 lazy 实现,完成生命周期的注入
  2. 自定义 ViewModelProvider.Factory 注入 ([注 1](#注 1 "#%E8%A1%A5%E5%85%85%E8%AF%B4%E6%98%8E"))

通过生产者注入生命周期

先看一下现在比较常见和广泛使用的官方推荐 的创建 ViewModel 方法:

Kotlin 复制代码
class XXActivity {

    val viewModel: XXViewModel by viewModels()

    override fun onCreate(...) {
        ...
        viewModel.doSomething()
    }
}

Google 为了让我们使用 ViewModel 更便捷,不再推荐开发者使用 ViewModelProvider(this).get(XXViewModel::class.java) 这种稍显啰嗦的写法来创建了,取而代之的是通过 Activity/Fragment 的扩展方法来一键获取实例。

热知识:众所周知viewModels() 函数其实很简单,只是通过 inline 提供了范型信息、同时依赖 receiver 提供 ViewModelStoreOwnerfactory 来创建了一个 ViewModelLazy 对象并返回。跟手写 val viewModel: XXViewModel by lazy { ... } 没有本质区别,只是把一些模版代码封在了 ViewModelLazy 内部更加便捷罢了。

那么接下来就很清楚了,参考 ViewModelLazy 我们自己实现一个 LifecycleViewModelLazy 就好了,只需要给我们创建的 ViewModel 自动注入一个 LifecycleOwner,ok 很简单

Kotlin 复制代码
/**
 * Customized `lazy` delegate for [ViewModel] with lifecycle
 */
class LifecycleViewModelLazy<VM : ViewModel>(
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory,
    private val lifecycleOwnerProducer: () -> LifecycleOwner,
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
                    cached = it
                    it.acquireLifecycleOwner().also { lifecycleOwner ->
                        lifecycleOwner.attachToHost(lifecycleOwnerProducer())
                        /* hook viewModel.viewModelScope, use lifecycle.coroutineScope instead
                         * lifecycle.coroutineScope 是 lifecycle-runtime-ktx 的 internal class LifecycleCoroutineScopeImpl, 会进行类型校验无法替换
                         */
                        it.hookViewModelScope(lifecycleOwner.lifecycle)
                    }
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}

这里要解决的主要是在 lazy 内部创建 ViewModel 实例的时候,如何注入 LifecycleOwner 的问题。

实际上就是自定义了一个 ViewModelLifecycleOwner 类型实现 LifecycleOwner 接口,内部维护一个 LifecycleRegistry.

此外,我们还需要做的就是把自定义的 LifecycleOwner 跟当前 ViewModel 实例关联起来,并提供一个 ViewModel 的扩展方法来取回相关联的 LifecycleOwner 即可。

然后提供一个 Activity/Fragment 可使用的 lazy 函数快速使用:

Kotlin 复制代码
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.lifecycleViewModels(
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory }
    return LifecycleViewModelLazy(VM::class, { viewModelStore }, factoryPromise, { this })
}

@MainThread
inline fun <reified VM : ViewModel> Fragment.lifecycleViewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this }, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory }
    return LifecycleViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryPromise, { this })
}

通过 ViewModel 取得 LifecycleOwner

关于如何建立自定义 LifecycleOwnerViewModel 实例的 1-1 关联其实有很多办法,这里有一个我认为最简单的法子:直接复用 ViewModelgetTag(String) 函数和其内部的 mBagOfTags Map 容器。

由于 ViewModel::getTag(String)<T> ViewModel::setTagIfAbsent(String, T) 的访问级别是 package 访问,所以只需要在相同包名下提供两个桥接方法即可顺畅的调用了:

Kotlin 复制代码
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <T> ViewModel.setTagIfAbsentX(key: String, value: T): T = this.setTagIfAbsent(key, value)

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <T> ViewModel.getTagX(key: String): T? = this.getTag(key)

直接将创建实例时生成的 LifecycleOwner 存入 ViewModel::mBagOfTags 中,然后提供一个公开的扩展方法用于获取 LifecycleOwner

Kotlin 复制代码
@Throws(IllegalStateException::class)
fun ViewModel.asLifecycleOwner(): LifecycleOwner = acquireLifecycleOwner().also {
    check(it.isAttached) { "Invalid: require `by lifecycleViewModels()`, and must called after instantiation complete." }
}

Enjoy

打完收工,实现过程并不复杂。从个人实际使用的感受上来说,雀食很香 :)

举个例子:

ViewModel 中需要调用业务 model 中的一个功能,而这个功能需要调用到其他业务 service 的方法,需要传递 LifecycleOwner, 这个时候就在 ViewModel 中直接提供无需依赖 Fragment 的传递,编码上更干净便捷。

Kotlin 复制代码
// 某业务 Fragment
@AndroidEntryPoint
class XxxFragment : Fragment() {
    private val viewModel: XxxViewModel by lifecycleViewModel()
    ...
}

// 业务 ViewModel
@HiltViewModel
class XxxViewModel @Inject constructor(private val bizModel: XxxBizModel) : ViewModel() {
    ...
    fun doSometing() {
        // 业务 model 某些方法需要传递一个 LifecycleOwner, 直接从 ViewModel 获取无需任何依赖
        bizModel.someBizMethod(this.asLifecycleOwner(), ...)
    }
}

// 业务 biz model
class XxxBizModel @Inject constructor() {
    ...
    fun someBizMethod(lifecycleOwner: LifecycleOwner, ...) {
        // 比如某些业务 service 的接口需要 LifecycleOwner, 就需要调用处传入,这算是一种很常见的场景
        getService<XxBizService>.someFunc(lifecycleOwner, ...)
    }
}

争议和意见

虽然通过这种小的技巧实现了 ViewModel 的独立生命周期访问,但是之前在项目中给其他同学介绍了这种用法和思路的时候被 argue 了。

总的来说,关于 ViewModel 作为 LifecycleOwner 的用法主要有两个争议点:

  • 这种设计没有必要:ViewModel 正常来说在应用中就是作为 view-model 和胶水来使用的,官方也没有类似的实践和建议,不需要;
  • 属于强行设计,没有道理:ViewModel 没有生命周期的概念,强行认定为 LifecycleOwner 没有意义。

我的想法

对于不同的意见我们当然是认真倾听和思考了,当然作为开发者来说,保持开放的心态和学习态度非常重要。而且每个人有不同的思考和理解也很正常,这里再阐述一下我的个人理解,希望有其他的同学提供更多的角度来让大家一起思考。

对于我个人而言,这个 case 的出发点始于项目中实际应用的场景诉求。

但是回头再来思考整个设计,我依然坚持 ViewModel 有作为独立 LifecycleOwner 的合理性,理由有几点:

  1. 首先,从 ViewModel 的角色和作用来说,在一个完整的 MVVM 场景中作为 View 的协作者,它的实例生命周期理应是跟相关的 View (Activity/Fragment) 保持同步的,即随着 View 创建而创建,随着 View 的销毁而消亡。

  2. ViewModel 的代码实践中也能看出,ViewModel 实例存储于 ViewModelStore([注 2:](#注 2: "#%E8%A1%A5%E5%85%85%E8%AF%B4%E6%98%8E") 这个基本上可以认为就是 Activity/Fragment),ViewModel::onCleared() 方法在所属的 host (Activity/Fragment) 销毁时被调用,即认为是消亡并释放资源。

  3. 论证 ViewModel 具备生命周期:
    a. 从上述两点来看,ViewModel 其实是具备生命周期特征的(这一点我觉得应该是没什么疑问的);
    b. 从另一个场外信息 分析(从代码实现角度论证其独立性 ,仅作侧面参考),在分析了 Hilt (Dagger) 生成的 Components (Injector) 实现可以看到:

    • Activity -> Fragment 的注入链是存在嵌套依赖关系的(即 FragmentCI 作为 ActivityCImpl 的内部类且构造器依赖 outer class 实例,可以注入来自 activity scope 的内容);
    • 而在 ViewModel 注入的实现中可以看到,ActivityCImplViewModelCImpl 同为 ActivityRetainedCImpl 内部类各自独立没有任何依赖和耦合关系,因此我认为从这个角度来看,ViewModel 在设计设计为度来看也是完全可以作为完整独立的组件来看待的。

结合上述三个角度来看:ViewModel 具有生命周期特征、ViewModel 有生命周期相关行为,且 ViewModel 完整独立。

所以我认为 ViewModel 是完全可以认定为具有独立生命周期的 LifecycleOwner 的。

补充说明

  • 注 1: 自定义 lazy 实现与 ViewModelProvider.Factory 可以结合在一起使用,将生命周期相关的注入逻辑沉入自定义的 Factory, 再改造一下 LifecycleViewModelLazy 内部的创建过程使用我们的自定义 Factory:

    Kotlin 复制代码
    open class LifecycleViewModelLazy<VM : ViewModel>(
        private val viewModelClass: KClass<VM>,
        private val storeProducer: () -> ViewModelStore,
        private val factoryProducer: () -> ViewModelProvider.Factory,
        private val lifecycleOwnerProducer: () -> LifecycleOwner,
    ) : Lazy<VM> {
        private var cached: VM? = null
    
        override val value: VM
            get() {
                val viewModel = cached
                return if (viewModel == null) {
                    val factory = factoryProducer()
                    val realFactory = if (factory is LifecycleViewModelFactory) factory else LifecycleViewModelFactory(factory, lifecycleOwnerProducer)
                    ViewModelProvider(storeProducer(), realFactory).get(viewModelClass.java).also { cached = it }
                } else {
                    viewModel
                }
            }
    
        final override fun isInitialized(): Boolean = cached != null
    }

    这样修改之后,在业务侧使用就不再限制需要使用特定的 lazy 函数了,可以通过重写 host getDefaultViewModelProviderFactory() 方法来保证创建的 ViewModel 是符合了预期的

  • 注 2: 对于 ViewModel 来说不论在 Activity 还是 Fragment 中使用,其存储与状态(如 ViewModel::onCleared)仅依赖 ViewModelStoreActivty/Fragment 则是通过实现 ViewModelStoreOwner 接口并提供一个 ViewModelStore 实例来管理 ViewModel 实例。

    理解了这一点那么对于 ViewModel 的使用就不限于 Activity/Fragment 了,任意业务组件其实都可以通过同样的方式来管理自己的 ViewModel 和依赖注入达到更彻底的解耦。比如目前尚在草稿阶段的某统一组件抽象方案(此处不展开)

相关推荐
阿巴斯甜18 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker19 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952720 小时前
Andorid Google 登录接入文档
android
黄林晴21 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android