即学即用Android Jetpack - ViewModel & LiveData

前言

即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第三篇。

在第二篇《即学即用Android Jetpack - Data Binding》,我们讨论了MVVM模式和Data Binding组件,所以这一章,我们继续学习跟MVVM模式相关的Android Jetpack组件ViewModelLiveData。由于ViewModelLiveData关联性比较强且使用简单(其实LiveData可以和很多组件一起使用),故打算一次性介绍这两个Android Jetpack组件。

学习本文需要掌握Data Binding,如果还没掌握,建议学习:

《即学即用Android Jetpack - Data Binding》

本文实现的效果:


效果

语言:Kotlin
我的Demo:https://github.com/mCyp/Hoo

目录

目录

一、LiveData

友情提醒:
官方文档:LiveData

在讲LiveData之前,我们先看看LiveDataViewModel的作用:

LiveData和ViewModel的作用

从这一张图,我们可以看出ViewModelLiveData在整个MVVM架构中担当数据驱动的职责,这也是MVVM模式中ViewModel层的作用。

1. 介绍

看了上面的图,对于LiveData我们还是感到疑惑,那么我们看看官网是如何定义的:

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services.

从官网的介绍可以看到,LiveData作用跟RxJava类似,是观察数据的类,相比RxJava,它能够在Activity、Fragment和Service之中正确的处理生命周期。那么LiveData有什么优点呢?

  • 数据变更的时候更新UI
  • 没有内存泄漏
  • 不会因为停止Activity崩溃
  • 无需手动处理生命周期
  • 共享资源

乍看之下LiveData挺鸡肋的,事实也确实如此,因为LiveData能够实现的功能RxJava也可以实现,而且与LiveData相比,RxJava拥有着更加丰富的生态,当然,谷歌的官方架构仍然值得我们去学习。

2. 使用方式

LiveData常用的方法也就如下几个:

方法名 作用
observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) 最常用的方法,需要提供Observer处理数据变更后的处理。LifecycleOwner则是我们能够正确处理声明周期的关键!
setValue(T value) 设置数据
getValue():T 获取数据
postValue(T value) 在主线程中更新数据

3. 使用场景

我看见绝大部分的LiveData都是配合其他Android Jetpack组件使用的,具体情况具体分析。

  • ViewModel: 见下文。
  • Room:先参考Demo,文章后续推出。

二、ViewModel

友情提醒:
官方文档:ViewModel
谷歌实验室:教程
谷歌官方Demo地址:https://github.com/googlecodelabs/android-lifecycles

众所周知,MVVM层中ViewModel层用来作逻辑处理的,那么我们Android Jetpack组件中ViewModel的作用是否也一致呢?

1. 介绍

我们先来看官网的介绍:

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

ViewModel同样具有生命周期意识的处理跟UI相关的数据,并且,当设备的一些配置信息改变(例如屏幕旋转)它的数据不会消失。

通常情况下,如果我们不做特殊处理,当屏幕旋转的时候,数据会消失,那ViewModel管理的数据为什么不会消失呢,是因为ViewModel的生命周期:

ViewModel的生命周期

ViewModel的另一个特点就是同一个ActivityFragment之间可以使用ViewModel实现共享数据。

2. 使用方法

继承ViewModel即可。

3. 实战

第一步:添加依赖

添加进module下面的build.gradle

ext.lifecycleVersion = '2.2.0-alpha01'
dependencies {
    //...

    // liveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
    // viewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleVersion"
}
第二步:创建ShoeModel

继承ViewModel类,分别创建对品牌名的观察对象brand:MutableLiveData<String>和对鞋子集合的观察对象shoes: LiveData<List<Shoe>>

class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {

    // 品牌的观察对象 默认观察所有的品牌
    private val brand = MutableLiveData<String>().apply {
        value = ALL
    }

    // 鞋子集合的观察类
    val shoes: LiveData<List<Shoe>> = brand.switchMap {
        // Room数据库查询,只要知道返回的是LiveData<List<Shoe>>即可
        if (it == ALL) {
            shoeRepository.getAllShoes()
        } else {
            shoeRepository.getShoesByBrand(it)
        }
    }

    //... 不重要的函数省略

    companion object {
        private const val ALL = "所有"
    }
}
第三步:获取ViewModel

无构造参数获取:
构造函数没有参数的情况下,获取ShoeModel很简单,ViewModelProviders.of(this).get(ShoeModel::class.java)这样就可以返回一个我们需要的ShoeModel了。
有构造参数获取
不过,上面的ShoeModel中我们在构造函数中需要一个ShoeRepository参数,上述方法是显然行不通的,这种情况下我们需要自定义实现Factory

class ShoeModelFactory(
    private val repository: ShoeRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return ShoeModel(repository) as T
    }
}

为了使用方便,又写了一个工具类CustomViewModelProvider

object CustomViewModelProvider {

    // ...省略无关代码

    fun providerShoeModel(context: Context):ShoeModelFactory{
        val repository:ShoeRepository = RepositoryProvider.providerShoeRepository(context)
        return ShoeModelFactory(repository)
    }
}

最后在ShoeFragment中获取:

    // by viewModels 需要依赖 "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
    private val viewModel: ShoeModel by viewModels {
        CustomViewModelProvider.providerShoeModel(requireContext())
    }
第四步:使用ViewModel

ViewModel的使用需要结合具体的业务,比如我这里的ShoeModel,因为ShoeFragment的代码不多,我直接贴出来:

/**
 * 鞋子集合的Fragment
 *
 */
class ShoeFragment : Fragment() {

    // by viewModels 需要依赖 "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
    private val viewModel: ShoeModel by viewModels {
        CustomViewModelProvider.providerShoeModel(requireContext())
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding: FragmentShoeBinding = FragmentShoeBinding.inflate(inflater, container, false)
        context ?: return binding.root
        ViewModelProviders.of(this).get(ShoeModel::class.java)
        // RecyclerView 的适配器 ShoeAdapter
        val adapter = ShoeAdapter()
        binding.recycler.adapter = adapter
        onSubscribeUi(adapter)
        return binding.root
    }

    /**
     * 鞋子数据更新的通知
     */
    private fun onSubscribeUi(adapter: ShoeAdapter) {
        viewModel.shoes.observe(viewLifecycleOwner, Observer {
            if (it != null) {
                adapter.submitList(it)
            }
        })
    }
}

onSubscribeUi方法中,我们使用ShoeModelLiveData进行了观察通知,当鞋子集合更新的时候,会更新到当前RecyclerView中的适配器。

布局文件fragment_shoe.xml很简单,虽使用了Data Binding,但是没有变量,且只有一个RecyclerView,这里不再赘述。ShoeAdapter的实现同样简单,感兴趣的可以查看源码,这里同样不再赘述。

这样写完之后,本文一开始的图的效果就出现了~

三、更多

一个例子并不能展现所有的关于LiveDataViewModel的内容。LiveDataViewModel仍有一些知识需要我们注意。

1. LiveData数据变换

LiveData中数据变换方法有map()switchMap(),关于switchMap(),我在上面实战的ShoeModel已经实践过了:

// 本地数据仓库
class ShoeRepository private constructor(private val shoeDao: ShoeDao) {

    fun getAllShoes() = shoeDao.getAllShoes()

    /**
     * 通过品牌查询鞋子 返回 LiveData<List<Shoe>>
     */
    fun getShoesByBrand(brand:String) = shoeDao.findShoeByBrand(brand)

    /**
     * 插入鞋子的集合 返回 LiveData<List<Shoe>>
     */
    fun insertShoes(shoes: List<Shoe>) = shoeDao.insertShoes(shoes)

    // ... 单例省略
}

class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {

    // 品牌的观察对象 默认观察所有的品牌
    private val brand = MutableLiveData<String>().apply {
        value = ALL
    }

    // 鞋子集合的观察类
    val shoes: LiveData<List<Shoe>> = brand.switchMap {
        // Room数据库查询,只要知道返回的是LiveData<List<Shoe>>即可
        if (it == ALL) {
            shoeRepository.getAllShoes()
        } else {
            shoeRepository.getShoesByBrand(it)
        }
    }
}

map()的使用我们借用官方的例子:

val userLiveData: LiveData<User> = UserLiveData()
val userName: LiveData<String> = Transformations.map(userLiveData) {
    user -> "${user.name} ${user.lastName}"
}

可以看到,map()同样可以实现将A变成B,那么switchMap()map()的区别是什么?map()中只有一个LiveData<A>,他是在LiveData<A>发送数据的时候把A变成B,而switchMap()中同时存在LiveData<A>LiveData<B>LiveData<A>更新之后通知LiveData<B>更新。

2. LiveData如何共享数据

假设我们有这样的需求:注册页需要记录信息,注册完成跳转到登录页,并将账号和密码显示在登录页。这种情况下,我们可以定义一个类然后继承LiveData,并使用单例模式即可:

// 登录信息
data class LoginInfo constructor(val account:String, val pwd:String, val email:String)

/**
 * 自定义单例LiveData
 */
class LoginLiveData:LiveData<LoginInfo>() {

    companion object {
        private lateinit var sInstance: LoginLiveData

        @MainThread
        fun get(): LoginLiveData {
            sInstance = if (::sInstance.isInitialized) sInstance else LoginLiveData()
            return sInstance
        }
    }
}

需要实例的时候用单例创建即可。

3. 使用ViewModel在同一个Activity中的Fragment之间共享数据

想要利用ViewModel实现Fragment之间数据共享,前提是Fragment中的FragmentActivity得相同,这里直接贴上官方的代码:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class MasterFragment : Fragment() {

    private lateinit var itemSelector: Selector

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        model.selected.observe(this, Observer<Item> { item ->
            // Update the UI
        })
    }
}

四、总结

总结

本文到此就结束了,本人水平有限,难免有误,欢迎指正哟~
Over~

🚀如果觉得本文不错,可以查看Android Jetpack系列的其他文章:

第一篇:《即学即用Android Jetpack - Navigation》
第二篇:《即学即用Android Jetpack - Data Binding》
第四篇:《即学即用Android Jetpack - Room》
第五篇:《即学即用Android Jetpack - Paging》
第六篇:《即学即用Android Jetpack - WorkManger》
第七篇:《即学即用Android Jetpack - Startup》
第八篇:《即学即用Android Jetpack - Paging 3》
项目总结篇:《学习Android Jetpack? 实战和教程这里全都有!》

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

推荐阅读更多精彩内容