Android Jetpack - ViewModel

◾︎简介

ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。

举个例子:在 MVP 中,Activity/Fragment 调用 P 层的异步请求。由于 P 层要持有 Activity/Fragment 的引用用于回调接口,当 Activity/Fragment 销毁后,造成内存泄露。为此,必须做大量的工作去管理异步请求。

而 ViewModel 不持有 Activity/Fragment 的引用,不存在上述问题。ViewModel 的 UI 更新是通过观察者模式(LiveData)实现的。

ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

当屏幕旋转时,为了避免数据丢失,通常会在 onSaveInstanceState() 中保存数据,然后在 onCreate() 的 Bundle 中恢复数据。但是这种方法有限制,数据必须支持序列化和反序列化,而且数据不能太大。

而 ViewModel 在 Activity 销毁重建时,会关联到新的 Activity,不需要手动保存再恢复。ViewModel 保存的数据格式和大小也没有像 Bundle 的限制。

本文代码使用 Kotlin 讲解,若需查看 Java 代码写法,请参考文末 Sample

◾︎添加依赖

ViewModel 一般和 LiveData 一起使用。

def lifecycle_version = "2.3.1"

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

◾︎使用

定义 ViewModel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {

    private val news: MutableLiveData<String> by lazy {
        MutableLiveData<String>().also {
            fetchNews()
        }
    }

    fun getNews(): LiveData<String> {
        return news
    }

    private fun fetchNews() {
        Thread {
            Thread.sleep(3000)
            news.postValue("Completed!")
        }.start()
    }
}

使用 ViewModel

import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val model = ViewModelProvider(this)[MyViewModel::class.java]
        model.getNews().observe(this) { msg ->
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
        }
    }
}

以上代码得到的 MyViewModel 实例是默认调用 MyViewModel 的无参构造方法。
我们知道,Android 中经常要用到 Context,如果需要在 ViewModel 中使用 Context,那么就需要调用带 Context 参数的构造方法了。

class MyViewModel(private val context: Context) : ViewModel() {
    ......
}

要调用有参构造方法,需要自定义 ViewModelProvider.Factory。

class MyViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        try {
            // 遍历所有构造方法
            for (constructor in modelClass.constructors) {
                // 找出只有一个 Context 参数的构造方法
                if (arrayOf(Context::class.java).contentEquals(constructor.parameterTypes)) {
                    // 利用此构造方法创建实例
                    return (constructor as Constructor<T>).newInstance(context)
                }
            }
            return modelClass.newInstance()
        } catch (e: InstantiationException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        } catch (e: IllegalAccessException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        }
    }
}

最后在获取 ViewModel 实例的时候,传入自定义的 ViewModelProvider.Factory 即可。

val model = ViewModelProvider(this, MyViewModelFactory(applicationContext))[MyViewModel::class.java]

仔细观察,会发现 ViewModelProvider 中有定义好的几种 Factory,其中就有 AndroidViewModelFactory,和我们上面自定义的 Factory 一样,可以直接拿来用。

◾︎ViewModel 的生命周期

ViewModel 的生命周期取决于传入 ViewModelProvider 的 ViewModelStoreOwner,也就是 Activity/Fragment。ViewModel 一直存在内存中,直到 Activity 完成时,Fragment 分离时。

所以,屏幕旋转后,ViewModel 依然存在在内存中。屏幕旋转时,ViewModel 的生命周期如下:


◾︎在 Fragment 之间共享数据

比如一个界面左边是列表 Fragment,右边是详细信息 Fragment。点了列表 Fragment 某一项需要把该项的信息传递给详细信息 Fragment。
先定义一个用于共享数据的 ViewModel

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

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

然后在两个 Fragment 中分别获取 ViewModel

class ListFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            model.select(item)
        }
    }
}

class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

这两个 Fragment 都会检索包含它们的 Activity。这样,当这两个 Fragment 各自获取 ViewModelProvider 时,它们会收到相同的 SharedViewModel 实例。
有了 ViewModel,Activity 不需要执行任何操作就可以实现 Fragment 间的数据共享。而且 Fragment 之间也相互独立,就算其中一个 Fragment 消失了,另一个 Fragment 也照样正常工作。

◾︎Factory

创建 ViewModel 的时候会用到 Factory。
你可能有疑问,上面的例子中哪里用到了?

val model = ViewModelProvider(this)[MyViewModel::class.java]

别急,我们慢慢看源码。
首先看 ViewModelProvider 的构造方法:

public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
    this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
            ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
            : NewInstanceFactory.getInstance());
}

可以看到指定了默认的 Factory

owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance())

当 owner 也就是例子中的 Activity 实现了 HasDefaultViewModelProviderFactory 接口并覆写了 getDefaultViewModelProviderFactory 方法的时候,返回它。否则返回一个 NewInstanceFactory 实例。
看到没有,虽然没有显示指定 Factory,但是内部使用了默认的 Factory。

Android 默认给开发者提供了两种 Factory。NewInstanceFactory 和 AndroidViewModelFactory

先来看 NewInstanceFactory:

public static class NewInstanceFactory implements Factory {

    private static NewInstanceFactory sInstance;

    static NewInstanceFactory getInstance() {
        if (sInstance == null) {
            sInstance = new NewInstanceFactory();
        }
        return sInstance;
    }

    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        try {
            return modelClass.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Cannot create an instance of " + modelClass, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Cannot create an instance of " + modelClass, e);
        }
    }
}

重点是 create 方法,它是用来指定创建 ViewModel 的时候使用 ViewModel 的哪个构造方法的。这里的 modelClass.newInstance() 会调用无参构造方法。

再来看 AndroidViewModelFactory:

public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {

    private static AndroidViewModelFactory sInstance;

    public static AndroidViewModelFactory getInstance(@NonNull Application application) {
        if (sInstance == null) {
            sInstance = new AndroidViewModelFactory(application);
        }
        return sInstance;
    }

    private Application mApplication;

    public AndroidViewModelFactory(@NonNull Application application) {
        mApplication = application;
    }

    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
            try {
                return modelClass.getConstructor(Application.class).newInstance(mApplication);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (InstantiationException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            }
        }
        return super.create(modelClass);
    }
}

重点还是来看 create 方法。
当 AndroidViewModel.class.isAssignableFrom(modelClass) 也就是要创建的 ViewModel 继承自 AndroidViewModel 的话,会调用参数是 Application 的构造方法,也就是 AndroidViewModel 中定义的构造方法。

public class AndroidViewModel extends ViewModel {
    @SuppressLint("StaticFieldLeak")
    private Application mApplication;

    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }

    @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
    public <T extends Application> T getApplication() {
        return (T) mApplication;
    }
}

当 ViewModel 需要用到资源文件的时候,Application 就很有用了。注意:ViewModel 中不能传 Activity,因为 ViewModel 的生命周期比较长。

AndroidViewModelFactory 使用例:

val factory = ViewModelProvider.AndroidViewModelFactory(application)
val model = ViewModelProvider(this, factory)[MyViewModel::class.java]

当然,如果系统的 Factory 不能满足的话,我们也可以自定义 Factory。
比如:

public class MyViewModelFactory(val age: Int): ViewModelProvider.Factory {

    override fun <T : ViewModel> create(viewModelClass: Class<T>): T {
        return viewModelClass.getConstructor(Int::class.java).newInstance(age)
    }
}

这个 Factory 会调用 public MyViewModel(age :Int) 构造方法。
具体使用如下:

val factory = MyViewModelFactory(10)
val model = ViewModelProvider(this, factory)[MyViewModel::class.java]

◾︎Sample

Kotlin 版
Java 版

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

推荐阅读更多精彩内容