Android Architecture Components(3) - ViewModel

上一篇文章中我们介绍了Architecture Components中的LifeCycle,LifeCycleOwner及LifeCycleObserver,不知道大家掌握的怎么样?在学习编码的路上,还是要多多实践才可以呢。
接下来我们要介绍的是ViewModel。


ViewModel简介

ViewModel是用来存储和管理生命周期过程敏感的界面数据的一个类,用ViewModel存储的数据可以在应用设置项发生改变时保存下来例如当屏幕旋转。

Android框架层管理UI控件的生命周期比如Activity和Fragment。框架层需要决定在面对用户交互时何时销毁或者重建UI控件,这一过程不是由开发者控制的。

如果系统销毁活着重建UI控件,那么用户的输入数据活着你已缓存的UI数据都会丢失,例如,在你的应用中有一个Activity展示着一个用户列表,当屏幕发生旋转时,Activity被重建,那么新创建的Activity需要再去请求一次用户列表数据。对于一些简单的数据,我们可以使用onSavedInstanceState()方法存储在Bundle中,然后在onCreate()函数中恢复,但是这种情况只适用于少量并且可以被序列化的数据,并不适用于其他数据,例如说一个用户列表或者很多图片。

另一个问题是UI控件需要频繁的发起异步请求并等待返回结果。UI控件需要去管理这些异步请求并保证在它完成后被系统清理掉以避免内存泄漏。这种管理需要大量的耐心和细心,并且在这些对象因为设置改变而重建的情形下,造成了一种资源浪费。

Activity和Fragment这种UI空间只是去展示UI数据,响应用户交互或者处理系统交互,例如说请求权限,UI控件同时也需要负责从数据库或者网络上加载数据,我们应该进行责任分摊,不要为UI控件添加过多的操作,这样会导致一个类去处理一个应用所要处理的工作,让我们的测试工作变得更加艰难。

综合以上几点,Google推出了ViewModel,用于帮助UI控件准备数据,ViewModel会在设置发生变化时自动存储数据,他们所持有的数据可以立刻被新建的Fragment或者Activity复用。

ViewModel的使用

假设我们需要在一个 应用页面内展示一个用户列表,那么我们可以将数据请求操作托管给ViewModel,代码如下:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }
    private void loadUsers() {
        // Do an asyncronous operation to fetch users.
    }
}

实现一个ViewModel只需要继承自ViewModel即可,随后我们可以在Activity中访问数据,方式如下:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

如果这个Activity被重建,那么他会接收到上一个Activity所创建的MyViewModel对象,当持有该ViewModel对象的Activity销毁时,系统会调用ViewModel.onCleared()方法释放资源。

注意ViewModel对象绝不能持有View,LifeCycle或者任何持有Activity 引用的对象

在设计理念上,ViewModel的生命周期独立于View或者LifeCycleOwner,这种设计也意味着你可以更简单的为ViewModel编写测试用例以覆盖ViewModel中的操作。ViewModel可以持有LifeCycleObservers比如说LiveData,如果ViewModel需要使用Application的引用,例如说去获取一个系统服务,此时可以继承自AndroidViewModel,该类有一个构造函数,可以接受Application的引用。

这里我再举一个简单的ViewModel的例子,以便大家更好的理解ViewModel。这个例子是界面上有一个Button和一个TextView,TextView用于记录Button 的点击次数,在屏幕旋转时保持TextView上的次数不变。

首先编写布局文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.code.archicomponentssmaples.MainActivity">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="32dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:text="Click Me"
        app:layout_constraintBottom_toTopOf="@+id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>

这里我试用了约束布局,不懂得同学可以自行百度,或者使用LinearLayout/RelativeLayout自己实现即可。

编写ViewModel类,用于持有Button的点击次数,如下:

public class ClickCounterViewModal extends ViewModel {
    private int count = 0;
    public int getCount() {
        return count;
    }
    public void setCount(int count) {
        this.count = count;
    }
}

随后在Activity中使用该ViewModel缓存Button点击次数,代码如下:

public class LifeOwnerActivity extends AppCompatActivity {

  private TextView mTextView;
  private Button mButton;
  private ClickCounterViewModal mClickCounterViewModal;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_life_owner);
    mClickCounterViewModal = ViewModelProviders.of(this).get(ClickCounterViewModal.class);
    initView();
    initData();
  }

  private void initView(){
    mTextView = (TextView)findViewById(R.id.owner_textView);
    mButton = findViewById(R.id.owner_Button);
  }

  private void initData(){
    displayClickCount();

  mButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
      mClickCounterViewModal.setCount(mClickCounterViewModal.getCount()+1);
        displayClickCount();
      }
    });
  }

  private void displayClickCount(){
    mTextView.setText(mClickCounterViewModal.getCount()+"");
  }
}

如上述代码ViewModel对象需要使用ViewModelProviders.of(Context)进行初始化。

另外多说一点ViewModel的提出只是为了处理UI相关数据的缓存问题,并不代表它能完全替代onSavedInstanceState()的作用。

在这个例子中ViewModel的工作流程如下图:

这里写图片描述

ViewModel生命周期

ViewModel对象的生命周期依赖于初始化时ViewModelProvider传入的LifeCycle对象,ViewModel对象会常驻在内存中直到与其对应的LifeCycle被销毁,对于Activity而言,就是当其被finish时,对于Fragment而言就是当它被detach的时候。
下图说明了一个Activity在发生屏幕旋转时自身的生命周期变化以及与其对应的ViewModel的生命周期。

这里写图片描述

通常情况下,在System调起Activity时,我们在Activity的onCreate()函数内初始化ViewModel对象,随后系统可能多次调用该Activity的onCreate()函数,例如说发生多次屏幕旋转。这种清醒下ViewModel来源于第一次onCreate(),直到调用Activity.finish()时,该ViewModel对象才会被销毁并释放资源。

使用ViewModel在Fragment间共享数据

在我们日常变成生活中,Activity需要与其内部的一个或多个Fragment交互信息的需求比比皆是,假设又这样一种情形,我们有一个Activity页面,左右各一个Fragment,左侧展示列表,右侧展示列表中某一项的详情。这种情形下Fragment中需要定义接口,持有Fragment的Activity需要同时绑定这两个Fragment,并且一个Fragment要处理另一个Fragment没有创建或显示的问题。

这种痛点可以通过ViewModel解决,这两个Fragment可以公用一个ViewModel对象,在Activity内处理,示例代码如下:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
    public void select(Item item) {
        selected.setValue(item);
    }
    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

如上所示,两个Fragment只需要监听ViewModel中值的变化更新UI即可。
需要注意的是这两个Fragment在ViewModelProvider内传入了getActivity(), 此时该ViewModel对象的生命周期完全依赖于持有两个Fragment的Activity。

这种模式有如下几点好处:

  • Activity不需要关注交互中的任何事;
  • Fragments彼此不需要知道对方的状态,一个Fragment消失了,另一个仍然可以正常工作;
  • 每个Fragment有其独立的生命周期,不会彼此影响,如果一个Fragment被另一个Fragment替换了,UI仍然能正常显示;

使用ViewModel代替Loaders

类似于CursorLoader的加载类经常被频繁的用于异步维护界面和数据库的数据一致,现在你也可以用ViewModel和一些其他的辅助类来实现这种功能了,使用ViewModel可以使我们的界面控制与数据加载解耦。
一种常见的使用CursorLoader监听数据库变化的结构图如下,当一个数据库值发生改变时,加载器会自动触发一个重新加载事件,完成后更新UI。


这里写图片描述

当ViewModel与Room以及LiveData结合使用时,就可以完全替代加载器的功能,ViewModel保证数据在设置发生改变的过程中不清空,当数据发生改变时,Room通知LiveData,随后使用新的数据更新UI。


这里写图片描述

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

推荐阅读更多精彩内容