关于kotlin:从-LiveData-迁移到-Kotlin-数据流

9次阅读

共计 8994 个字符,预计需要花费 23 分钟才能阅读完成。

LiveData 的历史要追溯到 2017 年。彼时,观察者模式无效简化了开发,但诸如 RxJava 一类的库对老手而言有些太过简单。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可察看的数据存储器类。LiveData 被无意简化设计,这使得开发者很容易上手;而对于较为简单的交互数据流场景,建议您应用 RxJava,这样两者联合的劣势就施展进去了。

DeadData?

LiveData 对于 Java 开发者、初学者或是一些简略场景而言仍是可行的解决方案。而对于一些其余的场景,更好的抉择是应用 Kotlin 数据流 (Kotlin Flow)。虽说数据流 (相较 LiveData) 有更平缓的学习曲线,但因为它是 JetBrains 力挺的 Kotlin 语言的一部分,且 Jetpack Compose 正式版行将公布,故两者配合更能施展出 Kotlin 数据流中响应式模型的后劲。

此前一段时间,咱们探讨了 如何应用 Kotlin 数据流 来连贯您的利用当中除了视图和 View Model 以外的其余局部。而当初咱们有了 一种更平安的形式来从 Android 的界面中取得数据流,曾经能够创作一份残缺的迁徙指南了。

在这篇文章中,您将学到如何把数据流裸露给视图、如何收集数据流,以及如何通过调优来适应不同的需要。

数据流: 把简略复杂化,又把简单变简略

LiveData 就做了一件事并且做得不错: 它在 缓存最新的数据 和感知 Android 中的生命周期的同时将数据裸露了进去。稍后咱们会理解到 LiveData 还能够 启动协程 和 创立简单的数据转换,这可能会须要花点工夫。

接下来咱们一起比拟 LiveData 和 Kotlin 数据流中绝对应的写法吧:

#1: 应用可变数据存储器裸露一次性操作的后果

这是一个经典的操作模式,其中您会应用协程的后果来扭转状态容器:

△ 将一次性操作的后果裸露给可变的数据容器 (LiveData)

<!-- Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 -->

class MyViewModel {private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
    val myUiState: LiveData<Result<UiState>> = _myUiState

// 从挂起函数和可变状态中加载数据
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

如果要在 Kotlin 数据流中执行雷同的操作,咱们须要应用 (可变的) StateFlow (状态容器式可察看数据流):

△ 应用可变数据存储器 (StateFlow) 裸露一次性操作的后果

class MyViewModel {private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
    val myUiState: StateFlow<Result<UiState>> = _myUiState

    // 从挂起函数和可变状态中加载数据
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

StateFlowSharedFlow 的一个比拟非凡的变种,而 SharedFlow 又是 Kotlin 数据流当中比拟非凡的一种类型。StateFlow 与 LiveData 是最靠近的,因为:

  • 它始终是有值的。
  • 它的值是惟一的。
  • 它容许被多个观察者共用 (因而是共享的数据流)。
  • 它永远只会把最新的值重现给订阅者,这与沉闷观察者的数量是无关的。

当裸露 UI 的状态给视图时,应该应用 StateFlow。这是一种平安和高效的观察者,专门用于包容 UI 状态。

#2: 把一次性操作的后果裸露进去

这个例子与下面代码片段的成果统一,只是这里裸露协程调用的后果而无需应用可变属性。

如果应用 LiveData,咱们须要应用 LiveData 协程构建器:

△ 把一次性操作的后果裸露进去 (LiveData)

class MyViewModel(...) : ViewModel() {
    val result: LiveData<Result<UiState>> = liveData {emit(Result.Loading)
        emit(repository.fetchItem())
    }
}

因为状态容器总是有值的,那么咱们就能够通过某种 Result 类来把 UI 状态封装起来,比方加载中、胜利、谬误等状态。

与之对应的数据流形式则须要您多做一点 配置:

△ 把一次性操作的后果裸露进去 (StateFlow)

class MyViewModel(...) : ViewModel() {
    val result: StateFlow<Result<UiState>> = flow {emit(repository.fetchItem())
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), // 因为是一次性操作,也能够应用 Lazily 
        initialValue = Result.Loading
    )
}

stateIn 是专门将数据流转换为 StateFlow 的运算符。因为须要通过更简单的示例能力更好地解释它,所以这里暂且把这些参数放在一边。

#3: 带参数的一次性数据加载

比方说您想要加载一些依赖用户 ID 的数据,而信息来自一个提供数据流的 AuthManager:

△ 带参数的一次性数据加载 (LiveData)

应用 LiveData 时,您能够用相似这样的代码:

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> = 
        authManager.observeUser().map { user -> user.id}.asLiveData()

    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
        liveData {emit(repository.fetchItem(newUserId)) }
    }
}

switchMap 是数据变换中的一种,它订阅了 userId 的变动,并且其代码领会在感知到 userId 变动时执行。

如非必须要将 userId 作为 LiveData 应用,那么更好的计划是将流式数据和 Flow 联合,并将最终的后果 (result) 转化为 LiveData。

class MyViewModel(authManager..., repository...) : ViewModel() {private val userId: Flow<UserId> = authManager.observeUser().map {user -> user.id}

    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
       repository.fetchItem(newUserId)
    }.asLiveData()}

如果改用 Kotlin Flow 来编写,代码其实似曾相识:

△ 带参数的一次性数据加载 (StateFlow)

class MyViewModel(authManager..., repository...) : ViewModel() {private val userId: Flow<UserId> = authManager.observeUser().map {user -> user.id}

    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
        repository.fetchItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}

如果说您想要更高的灵活性,能够思考显式调用 transformLatest 和 emit 办法:

val result = userId.transformLatest { newUserId ->
        emit(Result.LoadingData)
        emit(repository.fetchItem(newUserId))
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser // 留神此处不同的加载状态
    )

#4: 察看带参数的数据流

接下来咱们让方才的案例变得更具交互性。数据不再被读取,而是 被察看,因而咱们对数据源的改变会间接被传递到 UI 界面中。

持续方才的例子: 咱们不再对源数据调用 fetchItem 办法,而是通过假设的 observeItem 办法获取一个 Kotlin 数据流。

若应用 LiveData,能够将数据流转换为 LiveData 实例,而后通过 emitSource 传递数据的变动。

△ 察看带参数的数据流 (LiveData)

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> = 
        authManager.observeUser().map { user -> user.id}.asLiveData()

    val result = userId.switchMap { newUserId ->
        repository.observeItem(newUserId).asLiveData()}
}

或者采纳更举荐的形式,把两个流通过 flatMapLatest 联合起来,并且仅将最初的输入转换为 LiveData:

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> = 
        authManager.observeUser().map { user -> user?.id}

    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.asLiveData()}

应用 Kotlin 数据流的实现形式十分类似,然而省下了 LiveData 的转换过程:

△ 察看带参数的数据流 (StateFlow)

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> = 
        authManager.observeUser().map { user -> user?.id}

    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser
    )
}

每当用户实例变动,或者是存储区 (repository) 中用户的数据发生变化时,下面代码中裸露进去的 StateFlow 都会收到相应的更新信息。

#5: 联合多种源: MediatorLiveData -> Flow.combine

MediatorLiveData 容许您察看一个或多个数据源的变动状况,并依据失去的新数据进行相应的操作。通常能够依照上面的形式更新 MediatorLiveData 的值:

val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...

val result = MediatorLiveData<Int>()

result.addSource(liveData1) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}

同样的性能应用 Kotlin 数据流来操作会更加间接:

val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...

val result = combine(flow1, flow2) {a, b -> a + b}

此处也能够应用 combineTransform 或者 zip 函数。

通过 stateIn 配置对外裸露的 StateFlow

早前咱们应用 stateIn 两头运算符来把一般的流转换成 StateFlow,但转换之后还须要一些配置工作。如果当初不想理解太多细节,只是想晓得怎么用,那么能够应用上面的举荐配置:

val result: StateFlow<Result<UiState>> = someFlow
    .stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )

不过,如果您想晓得为什么会应用这个看似随机的 5 秒的 started 参数,请持续往下读。

依据文档,stateIn 有三个参数:‍

@param scope 共享开始时所在的协程作用域范畴

@param started 管制共享的开始和完结的策略

@param initialValue 状态流的初始值

当应用 [SharingStarted.WhileSubscribed] 并带有 `replayExpirationMillis` 参数重置状态流时,也会用到 initialValue。

started 承受以下的三个值:

  • Lazily: 当首个订阅者呈现时开始,在 scope 指定的作用域被完结时终止。
  • Eagerly: 立刻开始,而在 scope 指定的作用域被完结时终止。
  • WhileSubscribed: 这种状况有些简单 (后文详聊)。

对于那些只执行一次的操作,您能够应用 Lazily 或者 Eagerly。然而,如果您须要察看其余的流,就应该应用 WhileSubscribed 来实现轻微但又重要的优化工作,参见后文的解答。

WhileSubscribed 策略

WhileSubscribed 策略会在没有收集器的状况下勾销 上游数据流 。通过 stateIn 运算符创立的 StateFlow 会把数据裸露给视图 (View),同时也会察看来自其余层级或者是上游利用的数据流。让这些流继续沉闷可能会引起不必要的资源节约,例如始终通过从数据库连贯、硬件传感器中读取数据等等。 当您的利用转而在后盾运行时,您该当放弃克服并停止这些协程

WhileSubscribed 承受两个参数:

public fun WhileSubscribed(
   stopTimeoutMillis: Long = 0,
   replayExpirationMillis: Long = Long.MAX_VALUE
)

超时进行

依据其文档:

stopTimeoutMillis 管制一个以毫秒为单位的提早值,指的是最初一个订阅者完结订阅与进行上游流的时间差。默认值是 0 (立刻进行)。

这个值十分有用,因为您可能并不想因为视图有几秒钟不再监听就完结上游流。这种状况十分常见——比方当用户旋转设施时,原来的视图会先被销毁,而后数秒钟内重建。

liveData 协程构建器所应用的办法是 增加一个 5 秒钟的提早,即如果期待 5 秒后依然没有订阅者存在就终止协程。前文代码中的 WhileSubscribed (5000) 正是实现这样的性能:

class MyViewModel(...) : ViewModel() {
    val result = userId.mapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}

这种办法会在以下场景失去体现:

  • 用户将您的利用转至后盾运行,5 秒钟后所有来自其余层的数据更新会进行,这样能够节俭电量。
  • 最新的数据依然会被缓存,所以当用户切换回利用时,视图立刻就能够失去数据进行渲染。
  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。

数据重现的过期工夫

如果用户来到利用太久,此时您不想让用户看到古老的数据,并且心愿显示数据正在加载中,那么就应该在 WhileSubscribed 策略中应用 replayExpirationMillis 参数。在这种状况下此参数非常适合,因为缓存的数据都复原成了 stateIn 中定义的初始值,因而能够无效节俭内存。尽管用户切回利用时可能没那么快显示无效数据,但至多不会把过期的信息显示进去。

replayExpirationMillis 配置了以毫秒为单位的延迟时间,定义了从进行共享协程到重置缓存 (复原到 stateIn 运算符中定义的初始值 initialValue) 所须要期待的工夫。它的默认值是长整型的最大值 Long.MAX_VALUE (示意永远不将其重置)。如果设置为 0,能够在符合条件时立刻重置缓存的数据。

从视图中察看 StateFlow

咱们此前曾经谈到,ViewModel 中的 StateFlow 须要晓得它们曾经不再须要监听。然而,当所有的这些内容都与生命周期 (lifecycle) 联合起来,事件就没那么简略了。

要收集一个数据流,就须要用到协程。Activity 和 Fragment 提供了若干协程构建器:

  • Activity.lifecycleScope.launch : 立刻启动协程,并且在本 Activity 销毁时完结协程。
  • Fragment.lifecycleScope.launch : 立刻启动协程,并且在本 Fragment 销毁时完结协程。
  • Fragment.viewLifecycleOwner.lifecycleScope.launch : 立刻启动协程,并且在本 Fragment 中的视图生命周期完结时勾销协程。

LaunchWhenStarted 和 LaunchWhenResumed

对于一个状态 X,有专门的 launch 办法称为 launchWhenX。它会在 lifecycleOwner 进入 X 状态之前始终期待,又在来到 X 状态时挂起协程。对此,须要留神 对应的协程只有在它们的生命周期所有者被销毁时才会被勾销

△ 应用 launch/launchWhenX 来收集数据流是不平安的

当利用在后盾运行时接收数据更新可能会引起利用解体,但这种状况能够通过将视图的数据流收集操作挂起来解决。然而,上游数据流会在利用后盾运行期间放弃沉闷,因而可能节约肯定的资源。

这么说来,目前咱们对 StateFlow 所进行的配置都是无用功;不过,当初有了一个新的 API。

lifecycle.repeatOnLifecycle 前来救场

这个新的协程构建器 (自 lifecycle-runtime-ktx 2.4.0-alpha01 后可用) 恰好能满足咱们的须要: 在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时进行协程。

△ 不同数据流收集办法的比拟

比方在某个 Fragment 的代码中:

onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {myViewModel.myUiState.collect { ...}
        }
    }
}

当这个 Fragment 处于 STARTED 状态时会开始收集流,并且在 RESUMED 状态时放弃收集,最终在 Fragment 进入 STOPPED 状态时完结收集过程。如需获取更多信息,请参阅: 应用更为平安的形式收集 Android UI 数据流。

联合应用 repeatOnLifecycle API 和下面的 StateFlow 示例能够帮忙您的利用妥善利用设备资源的同时,施展最佳性能。

△ 该 StateFlow 通过 WhileSubscribed(5000) 裸露并通过 repeatOnLifecycle(STARTED) 收集

留神 : 近期在 Data Binding 中退出的 StateFlow 反对 应用了 launchWhenCreated 来形容收集数据更新,并且它会在进入稳定版后转而应用 repeatOnLifecyle

对于 数据绑定,您应该在各处都应用 Kotlin 数据流并简略地加上 asLiveData() 来把数据裸露给视图。数据绑定会在 lifecycle-runtime-ktx 2.4.0 进入稳定版后更新。

总结

通过 ViewModel 裸露数据,并在视图中获取的最佳形式是:

  • ✔️ 应用带超时参数的 WhileSubscribed 策略裸露 StateFlow。[示例 1]
  • ✔️ 应用 repeatOnLifecycle 来收集数据更新。[示例 2]

如果采纳其余形式,上游数据流会被始终放弃沉闷,导致资源节约:

  • ❌ 通过 WhileSubscribed 裸露 StateFlow,而后在 lifecycleScope.launch/launchWhenX 中收集数据更新。
  • ❌ 通过 Lazily/Eagerly 策略裸露 StateFlow,并在 repeatOnLifecycle 中收集数据更新。

当然,如果您并不需要应用到 Kotlin 数据流的弱小性能,就用 LiveData 好了 :)

向 Manuel、Wojtek、Yigit、Alex Cook、Florina 和 Chris 致谢!

正文完
 0