Android Architecture Components Part4:ViewModel

在Android Architecture Components(AAC)中ViewMode是为界面组件提供数据并可在界面配置更改后继续存在的对象。例如界面的旋转导致界面配置信息改变。

对于为界面提供数据,我们所知道的也有其他的一些模式,例如MVP的Presenter与MVVM中的ViewModel。那么我们进行一个假设,如果Activity发生界面旋转,此时上述的提供数据的模式会发生什么呢?

  1. 对于Activity的重建,为了提供ui所需的数据,我们必须重新获取数据(网络或者本地数据库),如果需要保存数据,也要重新进行保存操作。
  2. 在对数据进行操作时,你必须要处理一些可能造成的内存泄露问题。

对于以上问题,ViewModel都能够帮我们解决。只要Activity没有彻底被销毁,使用的都是同一个ViewModel,同时对于它的创建与销毁我们无需进行维护管理,能很好的保证资源的释放。

下面的这张图能够帮助我们更好的观察它在Activity中的生命状态

ViewModel贯穿Activity的整个生命周期,只有当Activity彻底释放时才会将其销毁。所以它能够更好的帮助我们实现持久化数据,防止不必要的数据请求,提高App的性能。

是不是有点好奇了呢,下面我们来简单介绍它的使用,为什么说简单呢?因为真的很简单...

依赖

如果你已经有了解过上篇关于Lifecycle的文章(Android Architecture Components Part3:Lifecycle),那么可以直接跳过依赖部分,没有的我们继续。

在使用ViewModel之前,我们需要在App或者Module的build.gradle中添加如下代码

dependencies {
    def lifecycle_version = "1.1.1"
 
    // ViewModel and LiveData
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"   
    annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version"
}

使用

依赖已经准备完毕,我们可以直接通过如下代码使用

class ContactsViewModel(application: Application, private val defTitle: String = "Contacts") : AndroidViewModel(application) {
    val message: MutableLiveData<String> by lazy { MutableLiveData<String>() }
    val contactsList: MutableLiveData<List<ContactsModel>> = MutableLiveData()
    ....
    ....
    
    fun getContacts(refresh: Boolean): LiveData<List<ContactsModel>> {
        message.value = ""
        if (refresh) {
            getDataFromRemote()
        } else if (contactsList.value == null || contactsList.value?.size ?: 0 <= 0) {
            message.value = "数据请求中,请稍后!"
            if (mLocalData.isEmpty()) {
                getDataFromLocal()
            }
        }
        return contactsList
    }
    ...
    ...
}

我们创建ContactsViewModel,让它继承于AndroidViewModel,它是对抽象ViewModel的扩展,使用它时需要传入Application对象,方便一些资源的获取。由于ViewModel的特性是对数据进行持久化,所以它不能持有与Activity相关的引用(Context),防止内存泄露,因此这里使用与应用生命周期绑定的Application。

在ContactsViewModel中我们结合MutableLiveData来更好的管理数据的变化更新。

ViewModel创建好了,接下来只剩下在Activity中进行使用。

class ContactsActivity : AppCompatActivity() {
 
    private lateinit var mViewModel: ContactsViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_contacts_layout)
        setupViewModel()
    }
    
    private fun setupViewModel() {
        mViewModel = ViewModelProviders.of(this)[ContactsViewModel::class.java]
        //active STARTED、RESUMED
        mViewModel.getContacts(true).observe(this,
                Observer {
                    //todo ...
                })
        mViewModel.message.observe(this,
                Observer {
                    //todo ...
                })
    }   
}

实际上我们只需一行代码就可以获取到ViewModel的实例,通过ViewModelProviders.of()方法传入Activity对象,它会返回一个ViewModelProvider对象,然后我们再使用它的get()来根据不同的ViewModel的Class对象来获取到相应的ViewModel实例。

只要Activity对象没有改变,同时都是同一个ViewModel的Class对象,那么我们无论何时获取的都是同一个ViewModel实例。这就实现了在Activity中的ViewModel持久化特性。由于ViewModel是同一个,自然它里面的数据也是同一份。

得到ViewModel后,剩下的就是对数据的操作与响应。这里结合了LiveData所以方便了许多。

LiveData之前已经有详细介绍,如需了解可以查看文章末的链接。

ViewModelProvider

到这里我想你心中可能会有如下几个疑问

  1. ViewModel它是如何初始化的,对象是如何实例化的
  2. 如何向ViewModel中传递初始化的参数

这两个疑问都将由ViewModelProvider来解决。

我们回到获取ViewModelProvider的ViewModelProviders.of()方法,进入源码查看。

    @NonNull
    @MainThread
    public static ViewModelProvider of(@NonNull FragmentActivity activity,
            @Nullable Factory factory) {
        Application application = checkApplication(activity);
        if (factory == null) {
            factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
        }
        return new ViewModelProvider(ViewModelStores.of(activity), factory);
    }

我们也没有发现我们想要的代码,但我们发现factory。我们在获取ViewModel时并没有传入factory,所以它会走空判断里面的代码,创建一个默认的factory。那么我们再进入ViewModelProvider中查看静态内部类AndroidViewModelFactory

    public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {

        ...
        ...
 
        @NonNull
        @Override
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
                //noinspection TryWithIdenticalCatches
                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);
        }
    }

我们只看关键代码,它继承于NewInstanceFactory,其中只有一个方法create()。AndroidViewModelFactory重新实现了create(),在重写的方法中我们找到了我们想要的方法调用。在这里它通过Class的getConstructor()方法获取匹配的Constructor对象,然后再通过newInstance()方法来获取匹配的对象实例。这样我们所需要的ViewModel实例就创建了,第一个疑问就此解决。

至于第二个疑问,细心的话不难发现,上面在调用newInstance()方法时已经传了一个初始化的参数mApplication。所以如果我们要再传入其它自定义的初始化参数,只需实现我们自己的create()方法。要自定义create()方法,我们就要自定义一个factory,继承NewInstanceFactory类。

下面是实现ContactsViewModel在初始化时传入特定的title值

class ContactsFactory(private val application: Application) : ViewModelProvider.NewInstanceFactory() {
 
    companion object {
        @SuppressLint("StaticFieldLeak")
        private var instance: ContactsFactory? = null
 
        fun getInstance(application: Application): ContactsFactory {
            if (instance == null) {
                instance = ContactsFactory(application)
            }
            return instance as ContactsFactory
        }
    }
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ContactsViewModel::class.java)) {
            return ContactsViewModel(application, "Factory Contacts") as T
        }
        return super.create(modelClass)
    }
}

重点自然是create()方法,通过isAssignableFrom()方法判断需要实例化的类型,由于我们能够明确到具体的类,所以可以直接使用正常的类实例化操作。已经有了factory,最后在获取ViewModel时传入即可

mViewModel = ViewModelProviders.of(this, ContactsFactory.getInstance(application))[ContactsViewModel::class.java]

ViewModel就是这么简单,但它对数据的持久化却异常突出。是否已经迫不及待了呢?赶紧来试试它的特性吧。

总结

最后Android Architecture Components(AAC)系列到此就告一段落了,让我们来回顾一下AAC。我们通过Room可以快速方便的实现本地数据存储;结合LiveData来观测数据的更新变化与及时反映到UI层;同时使用Lifecycle来让我们的组件或数据容器的具备生命感知能力,帮助我们的减少生命状态的处理与异常错误的发生;最后将界面数据存储到ViewModel中,使得数据达到持久化,减少不必要的数据请求与资源消耗。

下面的能够初步体现使用AAC后的App项目架构形态

最后感谢大家对AAC架构系列的支持!同时文章中的代码都可以在Github中获取到。使用时请将分支切换到feat_architecture_components

相关文章

Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle

关注

私人博客

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

推荐阅读更多精彩内容