前言
在 Google Android 团队宣布了 Jetpack 的视图模型之后,MVVM 架构已经成为了 Android 开发最流行的架构之一。如下图所示:
不过在 Google 的前期官方文档中,其 Repository 层是直接使用 LiveData 的,而且连 Jetpack Room 也对 LiveData 进行了支持,接口可以直接返回 LiveData 的数据。所以在很长一段时间内,各种开源的 MVVM 框架或者博客中,也是在 Repository 层中直接使用 LiveData。
这里,我们就会有疑问:Repository 层为什么使用 LiveData 呢?(因为通过官方文档介绍,LiveData 应该要跟Acvtivity 、Fragment 这类UI组件有关系,需要依赖 Lifecycle,放在 Repository 层非常奇怪)。
那么正确的做法是什么呢?下面将会演示基于 LiveData 实践的 MVVM框架、其存在的弊端、以及基于 Flow 实践的 MVVM框架,然后通过引入 Flow 来解决 LiveData 存在的问题。
在 Repository 层使用 LiveData 的 MVVM 实践
首先将这张 MVVM 框架图细化,来看看每个层级间的数据类型和数据流向:
然后再进一步细化,来看下设计细节:
1. 数据处理流程:
-
通过基础网络库(类似于:LibNetwork, 一般是业务方对Retrofit的封装)获取网络数据
-
在 Repository 层将请求数据转化为
LiveData<RepositoryData<T>>
,请求场景分多种:- 网络请求
- 本地请求,通常指 Room 数据库
- 网络请求 + 本地请求,用于先显示本地数据,在请求网络数据成功后刷新界面的场景
-
在 ViewModel 层将数据 Transfomer 为 UI层 所能理解的 VO 数据,也就是
LiveData<RepositoryData<S>>
-
在 UI 层监听 LiveData 数据的变化
2. 使用方法
以下是以 请求网络数据 为例:
UI 层:
private val dailyMottoViewModel by viewModels<DailyMottoViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = CommunicationsampleActivityLaunchTargetBinding.inflate(layoutInflater)
setContentView(binding.root)
dailyMottoViewModel.dailyMottoLiveData.observe(this) {
when {
it.isLoading() -> {
// show loading ui
}
it.isSuccess() -> {
// show success ui
}
it.isError() -> {
// show error ui
}
}
}
dailyMottoViewModel.requestDailyMotto()
}
ViewModel 层:
class DailyMottoViewModel : BaseViewModel() {
private val dailyMottoRepository by lazyRepository<DailyMottoRepository>()
private val dailyMottoMutableLiveData = MutableLiveData<RepositoryData<DailyMottoVO>>()
val dailyMottoLiveData
get() = dailyMottoMutableLiveData
fun requestDailyMotto() {
dailyMottoMutableLiveData.observeData(
dataSource = dailyMottoRepository.getDailyMotto(),
transformer = {
it.transformToVO()
}
)
}
}
Repository 层:
class DailyMottoRepository : BaseRepository() {
fun getDailyMotto(): LiveData<RepositoryData<DailyMottoModel>> {
return fetchNetworkData {
WebServiceFactory.instance.fetchDailyMotto()
}
}
}
3. Repository 层实现原理
fun <T> fetchNetworkData(requestFun: suspend () -> WebResponse<T>, saveToLocal: ((T) -> Unit)? = null): LiveData<RepositoryData<T>> {
val liveData = MutableLiveData<RepositoryData<T>>()
repositoryScope.launch {
// Loading 状态
liveData.postValue(RepositoryData.loading())
val result = invokeFunction(requestFun)
if (result.isSuccessful()) {
// 是否需要将数据保存到本地,通常会缓存到 Room 数据库中
saveToLocal?.let { saveDataInLocal ->
withContext(Dispatchers.IO) {
result.data?.let { saveDataInLocal.invoke(it) }
}
}
// 数据请求成功
liveData.postValue(RepositoryData.success(result.data))
} else {
// 数据请求失败
liveData.postValue(RepositoryData.error(RepositoryData.MSG_SERVER_ERROR, statusCode = result.code))
// 处理通用请求异常:例如 token失效、鉴权等
if (RepositoryData.isSpecificErrorToken(result.code)) {
onTokenError(result.code)
}
}
}
return liveData
}
private suspend fun <T> invokeFunction(function: suspend () -> WebResponse<T>): WebResponse<T> = withContext(Dispatchers.IO) {
val response: WebResponse<T> =
try {
function.invoke()
} catch (ex: Exception) {
XLog.e(TAG, "invokeFunction: ${ex.message}")
WebResponse(code = -1)
}
response
}
Repository 中使用 LiveData 的弊端
LiveData API设计得过于简单,难以应对Repository层可能出现的许多复杂的数据处理场景。主要体现在以下三个方面:
- 不支持线程切换
- 不支持背压处理
- 重度依赖 Lifecycle
不支持线程切换
在复杂的业务场景中,往往伴随着线程切换来对数据进行多次处理,类似 RxJava 的 observeOn
以及 Flow 的 flowOn
,而 LiveData 并没有这种能力。所以只能通过 协程 来进行线程切换,而在 Repository 层,就只能自定义 repositoryScope
并处理协程取消的逻辑。即:
private var repositoryScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
get() {
if (field.coroutineContext[Job]?.isActive == true) {
return field
}
val newScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
repositoryScope = newScope
return newScope
}
fun cancel() {
repositoryScope.cancel()
}
不支持背压处理
LiveData 肩负着为 UI 提供数据订阅的能力,所以他的数据订阅只能在主线程,虽然可以在子线程通过 postValue 去发布数据,但短期内调用 postValue 过快,由于没有背压处理,只保留最新的数据,因此可能造成预期之外的数据丢失问题。
而 Flow 则拥有完善的背压策略,可应对 Repository 层可能出现的各种复杂数据场景。
重度依赖 Lifecycle
LiveData 依赖 Lifecycle,具有生命周期感知能力,遵循 activity 和 fragment 等实体的生命周期,在非 UI 的场景中使用要么需要自定义 Lifecycle , 要么使用 LiveData#observerForever
(会造成泄露的风险)。在上面的案例中,ViewModel 需要监听 Repositoy 层的 LiveData,就必须特殊处理,避免内存泄露的问题。例如:
private val tempLiveDataList = mutableMapOf<LiveData<*>, Observer<*>>()
/**
* dataSource -> [transformer] -> LiveData
*/
fun <T, D> MutableLiveData<RepositoryData<T>>.observeData(
dataSource: LiveData<RepositoryData<D>>,
transformer: (D) -> T
) {
val data = MediatorLiveData<RepositoryData<T>>()
data.addSource(dataSource) {
when {
it.isError() -> checkPostErrorValue(it, transformer)
it.isLoading() -> checkPostLoadingValue(it, transformer)
else -> value = if (it.data == null) RepositoryData.success(it.data) else
RepositoryData.success(transformer.invoke(it.data))
}
}
data.observeForever(Observer<RepositoryData<T>> { }.apply { tempLiveDataList[data] = this })
}
/**
* 在 onCleared 中清除 Observer, 避免泄露
*/
override fun onCleared() {
super.onCleared()
tempLiveDataList.forEach {
it.key.removeObserver(it.value as Observer<in Any>)
}
}
在 Repository 层使用 Flow 的 MVVM 实践
使用 Flow 来替换 Repository 层中 LiveData 使用,主要涉及到 ViewModel 层和 Repository 层基础类的修改,而修改后的逻辑更加简洁、易读。而且官方文档也有所更新,对 LiveData 的使用场景有所限制,见:developer.android.com/topic/libra…:
It may be tempting to work
LiveData
objects in your data layer class, butLiveData
is not designed to handle asynchronous streams of data. Even though you can useLiveData
transformations andMediatorLiveData
to achieve this, this approach has drawbacks: the capability to combine streams of data is very limited and allLiveData
objects (including ones created through transformations) are observed on the main thread. The code below is an example of how holding aLiveData
in theRepository
can block the main thread: If you need to use streams of data in other layers of your app, consider using Kotlin Flows and then converting them toLiveData
in theViewModel
usingasLiveData()
. Learn more about using KotlinFlow
withLiveData
in this codelab. For codebases built with Java, consider using Executors in conjuction with callbacks orRxJava
.
1. Repository 层实现原理
protected fun <T> fetchNetworkData(saveToLocal: ((T) -> Unit)? = null, requestFun: suspend () -> WebResponse<T>): Flow<RepositoryData<T>> {
return flow<RepositoryData<T>> {
// Loading 状态
emit(RepositoryData.loading())
val webResponse = requestFun.invoke()
if (webResponse.isSuccessful()) {
// 是否需要将数据保存到本地,通常会缓存到 Room 数据库中
webResponse.data?.let { saveToLocal?.invoke(it) }
// 数据请求成功
emit(RepositoryData.success(webResponse.data))
} else {
// 处理通用请求异常:例如 token失效、鉴权等
if (RepositoryData.isSpecificErrorToken(webResponse.code)) {
onTokenError(webResponse.code)
}
// 数据请求失败
emit(RepositoryData.error(webResponse.msg, webResponse.data, webResponse.code))
}
}.flowOnIOWithCatch()
}
private fun <T> Flow<RepositoryData<T>>.flowOnIOWithCatch(): Flow<RepositoryData<T>> {
return this.catch {
emit(RepositoryData.error("local data error with catch"))
}.flowOn(Dispatchers.IO)
}
2. ViewModel 层实现原理
/**
* dataSource -> [transformer] -> LiveData
*/
fun <T, D> MutableLiveData<RepositoryData<T>>.observeData(
dataSource: Flow<RepositoryData<D>>,
transformer: (D) -> T
) {
dataSource.collectInLaunch {
when {
it.isError() -> checkPostErrorValue(it, transformer)
it.isLoading() -> checkPostLoadingValue(it, transformer)
else -> value = if (it.data == null) RepositoryData.success(it.data) else
RepositoryData.success(transformer.invoke(it.data))
}
}
}
private inline fun <T> Flow<T>.collectInLaunch(crossinline action: suspend (value: T) -> Unit) = viewModelScope.launch {
collect {
action.invoke(it)
}
}
3. 使用方法
由于 API 设计一致,使用方法与之前没有任何更改,因此可以无缝切换。唯一变更点就是 Repository 层的返回数据类型由 LiveData 修改为 Flow:
class DailyMottoRepository : BaseRepository() {
fun getDailyMotto(): Flow<RepositoryData<DailyMottoModel>> {
return fetchNetworkData {
WebServiceFactory.instance.getDailyMotto()
}
}
}
总结
综上,可以在Repository层使用Flow获取数据,并且Retrofit、Room都有自带的Flow扩展支持,使用上基本无缝衔接;ViewModel层collect来自Repository层的Flow,进行数据转换,将Model转到VO,再利用LiveData进行UI更新。
网友评论