Android Jetpack系列之MVVM使用及封装(续)

简介: `MVC`、`MVP`、`MVVM`架构及其对`MVVM`的封装使用,其中`MVVM`的主旨可以理解为数据驱动:`Repository`提供数据,`ViewModel`中发送数据,`UI层`使用的`LiveData`订阅数据,当有数据变化时会主动通知`UI层`进行刷新。接下来继续讨论`LiveData`的局限性以及`google`推荐的`UI`层订阅数据方式

前情提要

在前一篇 Android Jetpack系列之MVVM使用及封装 文章中,介绍了常用的MVCMVPMVVM架构及其对MVVM的封装使用,其中MVVM的主旨可以理解为数据驱动:Repository提供数据,ViewModel中发送数据,UI层使用的LiveData订阅数据,当有数据变化时会主动通知UI层进行刷新。接下来继续讨论LiveData的局限性以及google推荐的UI层订阅数据方式。

LiveData的缺点

在学习LiveData时,我们知道通过LiveData可以让数据被观察,且具备生命周期感知能力,但LiveData的缺点也很明显:

  • LiveData的接收只能在主线程;
  • LiveData发送数据是一次性买卖,不能多次发送;
  • LiveData发送数据的线程是固定的,不能切换线程,setValue/postValue本质上都是在主线程上发送的。当需要来回切换线程时,LiveData就显得无能为力了。

除了使用LiveData,还可以采用Flow替换,Flowgoogle官方提供的一套基于kotlin协程的响应式编程模型。常用的FlowStateFlowSharedFlow,详细使用参见:Android Kotlin之Flow数据流

Lifecycle.repeatOnLifecycle、Flow.flowWithLifecycle订阅数据

StateFlowLiveData 具有相似之处。两者都是可观察的数据容器类,并且在应用架构中使用时,两者都遵循相似模式。但两者还是有不同之处的:StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。

View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,而从 StateFlow 或任何其他数据流收集数据的操作并不会自动停止,即App已经切到后台了,而UI层可能还会继续订阅数据,这样可能会存在隐患。

如需保证App只在前台时订阅数据,需要从 Lifecycle.repeatOnLifecycleFlow.flowWithLifecycle 块收集数据流。google使用更为安全的方式收集 Android UI 数据流中给的例子:

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // 单次配置任务
            val expensiveObject = createExpensiveObject()
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止
                // 对 expensiveObject 进行操作
            }
            // 当协程恢复时,`lifecycle` 处于 DESTROY 状态。repeatOnLifecycle 会在
            // 进入 DESTROYED 状态前挂起协程的执行
        }
    }
}

或者

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        locationProvider.locationFlow()
            .flowWithLifecycle(this, Lifecycle.State.STARTED)
            .onEach {
                // 新的位置!更新地图
            }
            .launchIn(lifecycleScope) 
    }
}

其中Flow.flowWithLifecycle内部也是通过Lifecycle.repeatOnLifecycle实现的,上述例子中会在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止操作,如果觉得使用起来写的重复代码太多,可以简单对Flow.flowWithLifecycle封装一下:

inline fun <T> Flow<T>.flowWithLifecycle2(
        lifecycleOwner: LifecycleOwner,
        minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
        crossinline block: suspend CoroutineScope.(T) -> Unit,
) = lifecycleOwner.lifecycleScope.launch {
    //前后台切换时可以重复订阅数据。如:Lifecycle.State是STARTED,那么在生命周期进入 STARTED 状态时开始任务,在 STOPED 状态时停止订阅
    flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect { block(it) }
}

UI层使用如下:

mViewModel.loadingFlow.flowWithLifecycle2(this, Lifecycle.State.STARTED) { isShow ->
    mStatusViewUtil.showLoadingView(isShow)
}

嗯,看上去简洁了一些。

事件分类导致的新问题

UI层订阅的事件通常分成两种:

  • 一种是同样的事件可以多次消费:比如UI的刷新,多次执行没有任何问题;
  • 另一种是同样的事件只能消费一次,多次执行可能会有问题:比如Loading弹窗、跳转、播放音乐等。

针对第二种情况,写一个简单的例子:

  //UI层
   mBtnQuest.setOnClickListener {
       mViewModel.getModelByFlow()
   }
        
   lifecycleScope.launch {
       mViewModel.mIntFlow
               .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
               .collect { value ->
                   log("collect here: $value")
                   //......其他......
               }
   }
 //ViewModel层
 private val _intFlow = MutableStateFlow<Int>(-1)
 val mIntFlow = _intFlow
    
 fun getModelByFlow() {
     viewModelScope.launch {
          intFlow.emit(1)
     }
 }

打开当前页面时,log如下:

2022-05-08 21:34:17.775 3482-3482/org.ninetripods.mq.study E/TTT: collect here: -1

StateFlow的默认值 -1 会先发送到UI层,点击Button之后:

2022-05-08 21:34:22.921 3482-3482/org.ninetripods.mq.study E/TTT: collect here: 1

ViewModel中发送了1并被UI层接收。一切都很正常,此时我们把App切到后台再切回来:

2022-05-08 21:38:01.597 3482-3482/org.ninetripods.mq.study E/TTT: collect here: 1

可以看到UI层又接收了一遍,这是因为不管是Lifecycle.repeatOnLifecycleFlow.flowWithLifecycle ,切换前后台时,当Lifecycle处于STOPED状态,会挂起调用它的协程;并会在进入STARTED状态时重新执行协程。如果此时UI层是播放语音且需求是只播放一次,那么这里就会有问题了,每次切换前后台都会再播一次,不符合需求了,那么怎么办呢?接着往下看。

避免UI层重复订阅

第一种方式:Channel

Flow底层使用的Channel机制实现,StateFlow、SharedFlow都是一对多的关系,如果上游发送者与下游UI层的订阅者是一对一的关系,可以使用Channel来实现,Channel默认是粘性的。

Channel使用场景:一次性消费场景,如上面说的播放背景音乐,需求是在UI层只播一次,即使App切到后台再切回来,也不会重复播放。Channel使用特点:

  • 每个消息只有一个订阅者可以收到,用于一对一的通信
  • 第一个订阅者可以收到collect之前的事件,即粘性事件

Channel使用举例:

//viewModel中
private val _loadingChannel = Channel<Boolean>()
val loadingFlow = _loadingChannel.receiveAsFlow()

private suspend fun loadStart() {
    _loadingChannel.send(true)
}

private suspend fun loadFinish() {
    _loadingChannel.send(false)
}
//UI层接收Loading信息
 lifecycleScope.launch {
      mViewModel.loadingFlow
             .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
             .collect { isShow ->
                 mStatusViewUtil.showLoadingView(isShow)
             }
 }

通过Channel.receiveAsFlow()可以将Channel转化为Flow使用,Channel是一对一的关系,且下游消费完之后事件就没了,切换前后台也不会再重复消费事件了,达到了我们的要求。

第二种方式:改造Flow.flowWithLifecycle

还有一种写法,是对Flow.flowWithLifecycle改造一下,系统默认的实现如下:

@OptIn(ExperimentalCoroutinesApi::class)
public fun <T> Flow<T>.flowWithLifecycle(
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
    lifecycle.repeatOnLifecycle(minActiveState) {
        this@flowWithLifecycle.collect {
            send(it)
        }
    }
    close()
}

改为下面的方式:

/**
 * NOTE: 如果不想对UI层的Lifecycle.repeatOnLifecycle/Flow.flowWithLifecycle在前后台切换时重复订阅,可以使用此方法;
 * 效果类似于Channel,不过Channel是一对一的,而这里是一对多的
 */
fun <T> Flow<T>.flowOnSingleLifecycle(
        lifecycle: Lifecycle,
        minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
        isFirstCollect: Boolean = true,
): Flow<T> = callbackFlow {
    var lastValue: T? = null
    lifecycle.repeatOnLifecycle(minActiveState) {
        this@flowOnSingleLifecycle.collect {
            if ((lastValue != null || isFirstCollect) && (lastValue != it)) {
                send(it)
            }
            lastValue = it
        }
    }
    lastValue = null
    close()
}

本质上是保存了上次的值lastValue,如果再次订阅时会跟上次的值进行对比,只有值不一样时才会继续接收,从而达到跟Channel类似的效果,不过Channel是一对一的,而这里是一对多的。

相关文章
|
2月前
|
前端开发 JavaScript 测试技术
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
127 2
|
2月前
|
存储 前端开发 Java
Android MVVM架构模式下如何避免内存泄漏
Android采用MVVM架构开发项目,如何避免内存泄漏风险?怎样避免内存泄漏?
112 1
|
1月前
|
前端开发 JavaScript 测试技术
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
在 Android 开发中,选择合适的架构模式对于构建中大型项目至关重要。常见的架构模式有 MVVM、MVP、MVI、Clean Architecture 和 Flux/Redux。每种模式都有其优缺点和适用场景,例如 MVVM 适用于复杂 UI 状态和频繁更新,而 Clean Architecture 适合大型项目和多平台开发。选择合适的架构应考虑项目需求、团队熟悉度和可维护性。
57 6
|
2月前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
40 1
|
2月前
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
1月前
|
前端开发 Java 测试技术
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
63 0
|
2月前
|
前端开发 Java 测试技术
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
30 2
|
2月前
|
测试技术 数据库 Android开发
深入解析Android架构组件——Jetpack的使用与实践
本文旨在探讨谷歌推出的Android架构组件——Jetpack,在现代Android开发中的应用。Jetpack作为一系列库和工具的集合,旨在帮助开发者更轻松地编写出健壮、可维护且性能优异的应用。通过详细解析各个组件如Lifecycle、ViewModel、LiveData等,我们将了解其原理和使用场景,并结合实例展示如何在实际项目中应用这些组件,提升开发效率和应用质量。
54 6
|
3月前
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
96 8
|
3月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
88 4