最近一直在思考一个看上去很容易的问题,就是我们应该在哪里触发ViewModel的数据加载。看过一些源码,有很多种方式,也对比了一下自己使用的姿势,所以就想着罗列其中的一些,看看哪个姿势比较好。
17年的时候,为了让我们开发APP更加快捷,解耦更加彻底,Google将Architecture Components引入Android开发中来。作为这些组件的核心部分的ViewModel用来代替Presenter来加载数据,LiveData作为一个生命周期自我感知的组件用来连接Activity和ViewModel,ViewModel输出数据Activity使用这些数据,这一点是明确的,也没什么好纠结的.值得思考的问题是ViewModel必须在某个点上加载、订阅或触发数据的加载,那到底什么时候加载数据呢?
用例代码
使用一个简单的用例,在我们的ViewModel中加载一个书籍列表,并使用LiveData传递数据。
class Books(val names: List<String>)
data class Parameters(val namePrefix: String = "")/*只为示范*/
class GetBooksCase {
fun loadBooks(parameters: Parameters, onLoad: (Books) -> Unit) { /* Implementation detail */
}
}
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
// TODO When to call getBooksCase.loadBooks?
fun getBooks(parameters: Parameters): LiveData<Books> {
TODO("What to return here?")
}
}
达到什么效果
为了有据可依,我们首先要定一下加载数据些要满足的一些条件:
利用ViewModel按需加载,在生命周期旋转和配置更改分离.
易于理解和实现,使用干净少量的代码.
减少使用ViewModel所需的小型API知识.
提供参数的可能性(ViewModel 经常需要接受参数来加载数据).
❌ Bad: Activity/Fragment中调用方法
这种方式被广泛使用,在Google Blueprints example中也得到了推广,但存在着严重的问题。方法需要从某个地方调用,这通常会在活动或片段的生命周期方法中结束。
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
private val booksLiveData = MutableLiveData<Books>()
fun loadBooks(parameters: Parameters) {
getBooksCase.loadBooks(parameters) {
booksLiveData.value = it
}
}
fun books(): LiveData<Books> = booksLiveData
}
➖我们每次旋转都重新加载,没利用与Activity/Fragment生命周期解耦的特点,因为每次旋转都会从onCreate()
或其他生命周期方法调用该方法.
➕容易实现和理解.
➖需要两次调用方法.
➖引入隐式条件,即对于同一实例,参数始终相同。loadBooks()
和books()
方法是耦合的 .
➕易于提供参数.
❌ Bad: ViewModel 构造函数中调用
通过在ViewModel的构造函数中触发数据加载,可以轻松地确保只加载一次数据。这种方法在官方文档中也有。
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
private val booksLiveData = MutableLiveData<Books>()
init {
getBooksCase.loadBooks(Parameters()) {
booksLiveData.value = it
}
}
fun books(): LiveData<Books> = booksLiveData
}
➕数据只加载一次.
➕易于实现.
➕公有方法只有 books()
.
➖不容易提供参数.
➖在构造方法中做一些工作.
✔️ Better: 懒加载
使用kotlin的lazy委托属性特性:
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
private val booksLiveData by lazy {
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(Parameters()) {
liveData.value = it
}
return@lazy liveData
}
fun books(): LiveData<Books> = booksLiveData
}
➕只在第一次使用LiveData的时候加载数据.
➕易于实现.
➕公有方法只有 books()
.
➖除了booksLiveData
被访问之前添加参数之外,无法为加载函数提供参数.
✔️ Good: Lazy Map
我们可以根据提供的参数使用lazyMap或类似的lazy init。当参数是字符串或其他不可变类时,很容易将它们用作映射的键,以获取与提供的参数相对应的LiveData。
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
private val booksLiveData: Map<Parameters, LiveData<Books>> = lazyMap { parameters ->
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(parameters) {
liveData.value = it
}
return@lazyMap liveData
}
fun books(parameters: Parameters): LiveData<Books> = booksLiveData.getValue(parameters)
}
fun <K, V> lazyMap(initializer: (K) -> V): Map<K, V> {
val map = mutableMapOf<K, V>()
return map.withDefault { key ->
val newValue = initializer(key)
map[key] = newValue
return@withDefault newValue
}
}
➕只在第一次使用LiveData的时候加载数据.
➕比较容易实现和理解.
➕公有方法只有 books()
.
➕可以提供参数, ViewModel 甚至可以同时处理多个参数.
➖仍然在ViewModel中有一些可变状态.
✔️ Good: 通过构造方法传递参数
在上面使用lazy map的时候,我们只使用map来传递参数,但在许多情况下,ViewModel的一个实例将始终具有相同的参数。这时候最好将参数传递给构造函数,并在构造函数中使用lazy load或start load。我们可以使用ViewModelProvider.Factory来实现这一点,但它会有一些问题。
class BooksViewModel(val getBooksCase: GetBooksCase, parameters: Parameters) : ViewModel() {
private val booksLiveData: LiveData<Books> by lazy {
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(parameters) {
liveData.value = it
}
return@lazy liveData
}
fun books(parameters: Parameters): LiveData<Books> = booksLiveData
}
class BooksViewModelFactory(val getBooksCase: GetBooksCase, val parameters: Parameters) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return BooksViewModel(getBooksCase, parameters) as T
}
}
➕只加载一次.
➖实现和理解并不容易,会有很多boilerplate
➕公有方法只有 books()
.
➕ViewModel 在构造方法中接收参数,不可变且可测试.
从上面的代码可以看出这种方式需要额外的代码即ViewModel.Factory来传递动态参数。同时,我们开始有其他依赖的问题,如果有多个页面使用了这个ViewModel那我们需要分清如何将它们与参数一起实际传递到Fatory,这样可能会创建更多的模板代码。
到底选哪种
Architecture Components的引入大大简化了android的开发,解决了很多问题。尽管如此,仍然存在一些问题,这里列举了ViewModel加载数据的各种方式,并比较了优劣。
我的项目中使用的是lazy map这种方式,因为我发现这种方式利弊比较平衡,而且非常容易上手。如下代码是项目中使用到的:
class ListViewModel : ViewModel() {
private val liveDataMap: Map<String, LiveData<List<String>>> = lazyMap(this::getList)
fun getLiveData(fullRepoName: String): LiveData<List<String>> {
return liveDataMap.getValue(fullRepoName)
}
private fun getList(searchName: String): LiveData<List<String>> {
val map = HashMap<String, Any>()
val requestBody = RequestBody.create(null, JSONObject(map as Map<*, *>).toString())
return ListService.getSearchHot(requestBody)
.schedulerHelper()
.compose(handleResult())
.map { it }
.toLiveData()
}
}
千人千面,没有完美的解决方案,只有最适合的方法,在整个项目开发中平衡健壮性、简单性和一致性,让代码充分解耦易读就够了!