ViewModel

ViewModel 负责根据生命周期来存储和管理 UI 相关(Activity 或 Fragment)的数据。当配置发生变化时,如:屏幕方向旋转,它可以保证数据不被销毁。

你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。https://github.com/CodeXiaoMai/EnvironmentSwitcher

为什么要用 ViewModel

Android Framework 层管理 UI 控制器(Activity/Fragment)的生命周期,它可以决定销毁或者重新创建一个 UI 控制器,以响应某些用户操作或完全无法控制的设备事件。如果系统销毁或重新创建 UI 控制器,那么存储在其中的任何与 UI 相关的临时数据都将丢失。例如,应用程序可能在一个 Activity 中包含一个用户列表,当由于配置更改导致 Activity 重新创建时,新的 Activity 必须重新获取用户列表。对于简单的数据,Activity 可以使用 onSaveInstanceState() 方法在 onCreate() 中恢复数据,但这种方法只适用于少量的、可以序列化和反序列化的数据,不适用于像用户列表或位图这样的大量数据。

另一个问题是 UI 控制器经常需要异步调用,这可能需要一些时间来返回结果。UI 控制器需要管理这些调用,并确保系统在销毁后清除它们,以避免潜在的内存泄漏。这种管理需要大量的维护,并且在配置改变导致对象重新创建的情况下,会导致资源的浪费,因为对象可能不得不重新做它已经做过的事情。

UI控制器(如 Activity/ Fragment)主要用于显示 UI 数据、响应用户操作或处理操作系统通信(如权限请求)。如果界面控制器也负责从数据库或网络加载数据,就会导致这个类变的臃肿。将过度的责任分配给 UI 控制器可能导致一个单独的类试图处理所有应用程序的工作,而不是将工作委托给其他类,以这种方式将过度的责任分配给UI控制器也会使测试变得更加困难。

从UI控制器逻辑中分离出视图数据所有权,使开发更容易、更高效。

实现一个ViewModel

ViewModel 对象会在配置发生变化时自动保留,所以他们持有的数据对于新创建的 Activity 或 Fragment 实例是立即可用的。例如,如果需要在应用程序中显示一个用户列表,确保将获取和保存用户列表的职责分配给一个 ViewModel,而不是一个 Activity 或 Fragment,通过下面的示例代码说明:

public class UserListViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;

    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        new Thread(){
            @Override
            public void run() {
                super.run();
                List<User> list = new ArrayList<>(20);
                for (int i = 0; i < 20; i++) {
                    SystemClock.sleep(1000);
                    User user = new User(String.valueOf(i));
                    list.add(user);
                }
                users.postValue(list);
            }
        }.start();
    }
}

然后可以从一个 Activity 中访问该列表,如下:

public class UserListActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ......
        UserListViewModel userListViewModel = ViewModelProviders.of(this).get(UserListViewModel.class);
        userListViewModel.getUsers().observe(this, new Observer<List<User>>() {
            @Override
            public void onChanged(@Nullable List<User> users) {
                adapter.setUsers(users);
            }
        });
    }
}

如果 Activity 被重新创建,它会接收到被第一个 Activity 创建的相同的 UserListViewModel 实例。当宿主 Activity 结束运行时,Framework 层会调用 ViewModel 对象的 onCleared() 方法从而清理资源。

注意:ViewModel 唯一的责任是管理 UI 数据。一个 ViewModel 绝不能引用视图,有生命周期的,或持有 Activity 的 context 引用的类。

ViewModel 是比 View 或 LifecycleOwner 更具体的实例。这样的设计也意味着可以更容易的测试 ViewModel ,因为它不引用 View 和具有生命周期的对象。ViewModel 对象可以包含 LifecycleObserver 对象(如 LiveData)。然而ViewModel 对象禁止观察具有生命周期的可观察者的变化(如 LiveData)。如果ViewModel 需要 Application context,例如创建一个系统的服务,因为 Application 是 Context 的子类,所以可以继承 AndroidViewModel 类,并创建一个构造函数接收构造函数中的 Application。

ViewModel 的生命周期

ViewModel 对象通过 ViewModelProvider 获取,并且作用域在一个具有生命周期的类上。ViewModel 在其具有生命周期的类存活时,会一直保留在内存中:在一个 Activity 中,会保存到 finish 之前;而在一个 Fragment,会保存到 detached 之前。

下图展示了一个 Activity 经过旋转然后到结束运行的各种生命周期状态。该插图还在 Activity 的生命周期旁边显示了 ViewModel 的生命周期。虽然这个图描述的是 Activity 的状态,但是 Fragment 是和它类似的。

viewmodel-lifecycle.png

从图中可以看出通常只需要在系统第一次调用 Activity 对象的 onCreate() 方法时获取一个 ViewModel。系统可能在一个 Activity 的运行过程中,会多次调用 onCreate() 方法(比如当设备屏幕旋转时)。我们第一次请求的 ViewModel 会一直存在,直到 Activity 结束并销毁。这意味着一个 ViewModel 将不会因为它的创建者的一个配置变化而被销毁(例如:屏幕旋转)。Activity 的新实例将与现有的 ViewModel 重新连接。

Fragment 之间共享数据

一个 Activity 中的两个或多个 Fragment 需要相互通信是很常见的。设想有两个 Fragment,其中有一个用来展示一个列表,用户可以从列表中选择一个条目,另一个 Fragment 显示所选项目的内容。这种情况绝不是微不足道的,因为这两个Fragment 都需要定义一些接口描述,而宿主 Activity 必须将两者结合起来。此外,这两个 Fragment 必须处理另一个 Fragment 尚未创建或可见时的场景。

现在,可以通过使用 ViewModel 对象处理这个难点。可以通过在这些 Fragment 的宿主 Activity 中共享一个 ViewModel,来处理这种通信,通过下面的示例代码说明:

public class SharedViewModel extends ViewModel {

    private final MutableLiveData<String> selected = new MutableLiveData<>();

    private final MutableLiveData<List<User>> liveData = new MutableLiveData<>();

    public void select(String string) {
        selected.setValue(string);
    }

    public LiveData<String> getSelected() {
        return selected;
    }

    public LiveData<List<User>> getLiveData() {
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(new User(String.valueOf(i)));
        }
        liveData.setValue(list);
        select(list.get(0).getFirstName());
        return liveData;
    }
}

public class MasterFragment extends Fragment {

    private SharedViewModel model;
    private MyAdapter mAdapter;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getLiveData().observe(this, new Observer<List<User>>() {
            @Override
            public void onChanged(@Nullable List<User> users) {
                mAdapter.setUsers(users);
            }
        });
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.master_fragment, container, false);
        RecyclerView recyclerView = root.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        mAdapter = new MyAdapter();
        recyclerView.setAdapter(mAdapter);
        mAdapter.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(String string) {
                model.select(string);
            }
        });
        return root;
    }
}

public class DetailFragment extends Fragment {

    private TextView textView;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
                textView.setText(s);
            }
        });
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.detail_fragment, container, false);
        textView = root.findViewById(R.id.text);
        return root;
    }
}

注意: 这两个 Fragment 使用 getActivity() 获取 ViewModelProvider。这样,两个 Fragment 得到宿主 Activity 中的同一个 SharedViewModel 实例。

这种方法的好处:

  • 这个 Activity 不需要做任何事情,也不需要知道任何关于这个通信的事情。
  • Fragment 之间不需要知道除了 SharedViewModel 之外的逻辑。如果其中一个 Fragment 消失了,不会影响另一个 Fragment 继续工作。
  • 每个 Fragment 都有自己的生命周期,不受另一个 Fragment 生命周期的影响。如果一个 Fragment 替换另一个 Fragment,UI 会继续工作,没有任何影响。

使用 ViewModel 替换数据加载器

像 CursorLoader 这样的加载类,经常被用来在一个应用程序的 UI 与数据库之间保持数据同步。现在可以使用 ViewModel 与其他几个类来取代这个加载类。使用 ViewModel 可以将 UI 控制器与数据加载操作分离,这意味着有更少的类之间的强引用。

使用加载器的一种通用方法是,一个应用程序可能使用 CursorLoader 观察数据库的内容。当数据库中的值发生变化时,加载器会自动触发重新加载数据并更新UI:

loader.png

使用 ViewModel、Room、LiveData 一起取代加载器。ViewModel 确保数据在设备配置变化时不被销毁。Room 会在数据库变化时通知 LiveData,反过来,Livedata 会在数据修改后更新UI。

viewmodel-replace-loader

这篇文章介绍了如何使用一个 ViewModel 与 LiveData 取代 AsyncTaskLoader。
https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4

当数据变得更复杂时,可以选择单独的类来加载数据。ViewModel 的目的是封装UI 控制器的数据,保证数据在设备配置变化时不被销毁。

参考文章

https://developer.android.com/topic/libraries/architecture/viewmodel.html

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

推荐阅读更多精彩内容