当链路用 LiveData 表达时,访问数据库和网络的操作被定义在一个 Repository 的方法中:
class NewsRepository(context: Context) {
fun fetchNewsLiveData(): LiveData<List<News>?> {
// 1.从数据库获取新闻
val localNews = newsDao.queryNews()
// 2.从网络获取新闻
val remoteNews = newsApi.fetchNewsLiveData(mapOf("page" to "1", "count" to "4"))
// 3.将数据库和网络响应的 LiveData 合并
newsLiveData.addSource(localNews) {newsLiveData.value = it}
newsLiveData.addSource(remoteNews) {newsLiveData.value = it}
return newsLiveData
}
}
复制代码
并且它们是串行的,即只有当数据库访问结束后才开始网络请求,最后再将它们通过 MediatorLiveData 合流。
而使用流时,数据库和网络操作被定义在不同的流中,这为它们提供了更灵活的合流方式。
串行合流
串行合流的思路是将多个流组织成“嵌套流”,然后将它们“展平”。
拿 List 举例,List.flat()
提供了在列表上的展平操作,flat 即展平,为啥要展平?因为有嵌套,比如List<List<Int>>
,即 List 中每个元素还是 List:
val lists = listOf(
listOf(1,2,3),
listOf(4,5,6)
)
Log.v("ttaylor","${lists.flatten()}") //[1, 2, 3, 4, 5, 6]
Log.v("ttaylor","${lists.flatMap { it.map { it+1 } }}") //[2, 3, 4, 5, 6, 7]
复制代码
List.flat() 将两层嵌套结构变成单层结构,而List.flatMap()
在展平的同时提供了变换内部 List 的机会。
流也提供了类似的展平方法flattenConcat()
:
flowOf(
flow {
emit(1)
emit(2)
},
flow { emit(3) },
flow { emit(4) },
).flattenConcat().collect {
Log.v("ttaylor", "${it}") // 1,2,3,4
}
复制代码
flattenConcat() 的合流是串行的,即只有消费了前一个流中所有的数据后才会消费后一个流。
在 ViewModel 层对原始数据流进行合流:
// 新闻 ViewModel 持有 repo
class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {
fun newsFlow(type: Int, count: Int) =
flowOf(newsRepo.localNewsFlow, newsRepo.remoteNewsFlow(type, count))
.flattenConcat() // 串行合流
.map { NewsModel(it, false) }
}
// 通过 ViewModelProvider.Factory 定义构建 ViewModel 的细节(注入Repository)
class NewsViewModelFactory(private val newsRepo: NewsRepo) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NewsViewModel(newsRepo) as T
}
}
复制代码
在 Repository + Flow 的加持下,ViewModel 变得异常简单,它持有原始数据流并对其进行合流以及变换。
两个原始数据流分别是数据库流和网络流,使用flowOf()
将它们组织成Flow<Flow<News>>
嵌套结构,然后调用 flattenConcat() 将它们串行合流并展平变成一个流,即先查询数据库,待查询完毕后才请求网络。合流之后还进行了数据变换,以将网络数据转换为界面数据 NewsModel:
data class NewsModel(
val news: List<News>, // 新闻列表
val loading: Boolean, // 是否正在加载
val errorMessage: String = "" // 错误信息
)
复制代码
将新闻列表进行这样包装的目的是实现“唯一可信数据源”,这是 MVI 的关键词之一。