ViewModel中加载数据的一些姿势

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342