美文网首页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