美文网首页Android-Jetpack
ViewModel中加载数据的一些姿势

ViewModel中加载数据的一些姿势

作者: 葛糖糖 | 来源:发表于2019-10-10 15:09 被阅读0次

最近一直在思考一个看上去很容易的问题,就是我们应该在哪里触发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?")
   }
}

达到什么效果

为了有据可依,我们首先要定一下加载数据些要满足的一些条件:

  1. 利用ViewModel按需加载,在生命周期旋转和配置更改分离.

  2. 易于理解和实现,使用干净少量的代码.

  3. 减少使用ViewModel所需的小型API知识.

  4. 提供参数的可能性(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()
    }
}

千人千面,没有完美的解决方案,只有最适合的方法,在整个项目开发中平衡健壮性、简单性和一致性,让代码充分解耦易读就够了!

相关文章

网友评论

    本文标题:ViewModel中加载数据的一些姿势

    本文链接:https://www.haomeiwen.com/subject/mtfapctx.html