玩转Android Jetpack系列之ViewMode

1. 什么是Android Jetpack?

Android Jetpack是谷歌在2018年I/O开发者大会上推出的新一代组件、工具和架构指导,旨在加快开发者的 Android 应用开发速度。 ——[官方介绍网站]1

Google爸爸老司机在2018年的I/O大会上发车了,推出了新一代的开发组件、工具和架构指导,并打包在一起,取名为“Jetpack”,顾名思义,Jetpack直译过来的意思就是喷气背包,Google也使用了一个很形象的穿戴喷气背包的android机器人来做形象代言,如下图。

通过图片可以很明确的感受到谷歌爸爸的意图:带你飞!。简单的概况,Jetpack推出的主要目的是为了能够让开发者更加快速、方便以及高质量的完成产品开发,Jetpack的主要作用可以概况为以下几点:

  • 加速开发速度,Jetpack包含的一系列组件可以分开使用,也可以相互组合使用,同时还完美支持Kotlin语言的功能特点进一步提升开发效率
  • 聚焦开发的核心,Jetpack提供的组件可以协助管理日常繁琐且容易出错的地方,比如生命周期的管理,后台任务的管理,导航的处理等等,这样开发可以将开发的注意力放到更加核心的地方上。
  • 提升应用开发的质量,利用Jetpack组件进行开发可以有效减少内存溢出、崩溃的概率,提升应用开发的质量,并提供向后的兼容性

Jetpack组件主要分为四个方向:基础,架构,行为和UI。详情见下表:

基础 架构 行为 UI
AppCompat Data Binding Download Manager Animation & transitions
Android KTX Lifecycles Media & playback Auto
Multidex LiveData Notifications Emoji
Test Navigation Permissions Fragment
- Paging Sharing Layout
- Room Slices Palette
- ViewMode - TV
- WorkManager - Wear OS by Google

如上表格,有些内容是很早就有的,有些是最近推出的,Jetpack将这些内容打包在一起,共同组成Jetpack,本系列主要对Jetpack新推出的架构这一块内容进行叙述。

2. ViewMode概述

ViewMode主要用来管理和存储与UI绑定的数据,同时ViewMode还与UI的生命周期相关联。例如ViewMode的一大特色在于:与ViewMode相关联的UI界面如果因为某些原因需要重新绘制创建时,例如横竖屏切换,导致Activity销毁并重新创建时,ViewMode仍然可以保留之前读取到的数据不会因为Activity的销毁而丢失,这样我们无需额外再浪费资源去再次请求数据。

有时候Activity或者Fragment的生命周期的变动是不受控制的,经常会因为各种事件的调度导致界面需要重新创建。当Activity需要重新创建的时候,之前与之绑定的数据也会丢失。比如你的应用界面通过list来展示用户名单,如果界面重新创建时,之前获取的用户名单数据需要再次重新获取,但是用户名单数据相对来说是一个比较稳定的静态数据,再次获取一次数据显然浪费了系统资源。

有的同学看到这里后可能会有疑问:不对呀,Android中不是提供了onSaveInstanceState()方法来保存数据吗,然后重新执行OnCreate的时候,通过Bundle参数来再次获取保存的数据?这种方式也是可行的,但是有个限制,即这种方式只能保存数据量较小的情况,并且数据被序列化才行。如果遇到数据量较大的时候,比如图片数据,这种方案显示力不从心了。

我们在日常请求数据后对UI进行绑定还有一个常见的问题,有时候我们会把数据的的请求放到异步去操作,这样不会因为长时间获取数据导致UI进程的堵塞。但是随之带来的问题也挺多,例如我们需要管理和维护好获取到数据后的回调,另外在销毁当前UI的时候,我们需要确保异步任务中的资源有效的得到了清理,防止出现内存溢出。一旦我们的界面需要重新绘制的时候,我们上述所有的异步操作需要重新创建和执行,这样显然浪费了系统的开销。

我们日常使用的Activity或者Fragment,他们的主要职责就是展示UI,以及与用户的操作行为进行交互,或者与系统的一些事件进行通信,例如权限管理对话框。如果还要求Activity对数据请求的事件进行管理和维护,这个已经超出了Activity本来的意图,并且随着事务的增多,Activity会越来越臃肿,一旦出了问题,需要花费大量精力去维护。

ViewMode的推出正是基于上述问题给出的解决方案,完美高效的将UI控制器和数据业务进行分离,UI控制器只负责UI展示相关的工作,数据业务只负责获取数据的相关工作。

3. ViewMode的使用方法

通过上文的铺垫,那么如何使用ViewMode呢,为了方便说明,本文采用一个简单的例子,通过Activity中的list展示一组用户的姓名,下面详细进行说明。ViewMode是一个抽象类,所以我们需要通过extends集成它才能够使用,代码如下所示:

public class MyViewMode extends ViewModel {

    private MutableLiveData<List<User>> users;

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

    private void loadUsers(){
        String[] names = {"张三","李四","王五","John","小明","Leo","Wang","Li","Ha","Yun"};
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < names.length; i++){
            User user = new User();
            user.setName(names[i]);
            userList.add(user);
        }
        users.setValue(userList);
    }
}

上述例子较为简单,代码行数没有多少,主要定义了一个类型为MutableLiveData变量:users与两个方法。这里的users就是我们用来存放数据的变量容器,可能有的同学注意到了,它的类型是MutableLiveData,这个类型是什么呢?看起来很眼熟。如果各位有印象的话,我们在上文中介绍Jetpack的时候,在介绍Jetpack构成的时候,架构中有一个LiveData。恩对,这个变量类型也是Jetpack的一员。可不要小看它,它的本领很多,我们会在下一文中单独对它进行讨论。大家只要这里记住,他是用来存储数据的容器即可,而MutableLiveData是对LiveData的扩展,主要实现了set和post方法来方便更改LiveData的值。

loadUser的方法很容易理解,为了方便测试,我们这里定义了一个字符串数组,然后通过for循环进行遍历存储到list中,最后通过LiveData提供的setValue方法将数据存放到users中。getUser是针对数据users的get方法,为了防止每次读取user的时候都要创建一次数据,对数据进行判空处理,只有为空的情况下才调用loadUser去加载数据。

ViewMode的实现方式就是这么简单,它不需要关心UI是如何呈现的,它只关心数据如何获取。完全与UI无关。
下面看下Activity的实现方式,主要代码如下:

public class MainActivity extends AppCompatActivity {

    private ListView mListView;

    private MyAdapter adapater;

    private List<User> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        MyViewMode model = ViewModelProviders.of(this).get(MyViewMode.class);
        model.getUsers().observe(this, new Observer<List<User>>() {
            @Override
            public void onChanged(@Nullable List<User> users) {
                mDatas = users;
                adapater.notifyDataSetChanged();
            }
        });
    }
    .
    .
    .

看完代码是不是发现太简单了,没有什么多余需要说明的,主要是在OnCreate方法中,首先获取我们定义的MyViewMode,获取的方式是通过ViewModelProviders的of方法绑定activity的实例进行初始化,然后通过get方法来获取到MyViewMode的实例。
接着第二句通过调用我们在MyViewMode已经定义过的getUser方法来获取LiveData数据,LiveData数据可以再通过observe方法进行数据回调的返回,如上代码中的onChanged回调。所以我们只要在onChange方法中做好数据刷新UI的操作即可。

注意:ViewMode中不能引用任何View的实例,也不能引用任何持有Activity或者Context的实例。如果有些请求数据的情况必须用到Context,在继承ViewMode的时候,可以改为继承AndroidViewMode,这个类会返回一个带有Context的构造函数。

4. ViewMode的生命周期

ViewMode在其生命周期的范围内会一直保存在内存中,当依附的Activity被finish后才会销毁,或者当依附的Fragment detached后进行销毁。为了验证,我们在上述的例子中,每个生命周期中打出相应的log,然后在MyViewMode的loadUser和getUser也打出相应的log。然后程序运行后执行横竖屏切换来让生命周期重新Oncreate,然后查看得到的log如下:

2018-07-31 10:34:48.573 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onCreate
2018-07-31 10:34:48.607 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel getUsers
2018-07-31 10:34:48.607 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel loadUsers
2018-07-31 10:34:48.612 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onResume
2018-07-31 10:34:51.044 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onStop
2018-07-31 10:34:51.045 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onDestroy
2018-07-31 10:34:51.102 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onCreate
2018-07-31 10:34:51.142 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel getUsers
2018-07-31 10:34:51.148 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onResume
2018-07-31 10:35:01.437 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onStop
2018-07-31 10:35:01.438 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onDestroy
2018-07-31 10:35:01.485 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onCreate
2018-07-31 10:35:01.509 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel getUsers
2018-07-31 10:35:01.519 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onResume
2018-07-31 10:35:04.550 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onStop
2018-07-31 10:35:04.551 30315-30315/jinfeng.myapplication E/wangjinfeng: ViewModel onCleared
2018-07-31 10:35:04.551 30315-30315/jinfeng.myapplication E/wangjinfeng: MainActivity onDestroy

当打开应用后,执行onCreate的时候,ViewMode会通过调用getUsers和loadUsers来获取数据,当切换手机横竖屏后,MainActivity会destroy并重新onCreate来重构当前界面,所以我们在log中会看到生命周期再次重新触发onCreate,但是ViewMode仅仅调用了getUsers来返回数据,并没有调用loadUsers,我们再回头看上面getUsers的逻辑,当判断users数据为空的情况下,才会去执行loadUsers,这样也就意味着,我们切换横竖屏后,activity被销毁并重建后,user的数据并没有丢失,所以并没有重新执行获取数据的操作。

细心的读者可能发现了,在日志的最后,ViewMode会执行onCleared操作,这个是ViewMode的一个回调,表明当前Activity要彻底关闭,ViewMode需要做一些回收清理的操作,如下代码:

@Override
    protected void onCleared() {
        super.onCleared();
        /**
         * 这里可以执行一些资源释放、数据清理的操作
         */
    }

下面用一张图来标注ViewMode的生命周期与Activity的生命周期的关联,如下图所示:

5. 应用举例:利用ViewMode进行Fragment之间的数据交互

我们在平日里会遇到这样的场景,比如文件管理器这个应用,文件管理器的主界面通过一个MainFragment进行封装实现,主要呈现各个文件类别的入口,比如有音乐、视频、图片、文档等的入口,点击对应的入口项,会进入对应的详情页,而详情页是由另外一个DetailFragment实现。我们传统的实现方案是在DetailFragment中抽象暴露出一些回调接口,然后当在MainFragment进行点击不同的入口时,执行DetailFragment中的回调接口来展示不同的Detail详情内容。

同样,我们也可以利用ViewMode来实现上述场景,关键代码如下所示:

public class SharedViewModel extends ViewModel {
    //selected保存的是被选中的item的状态或者数据
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    //主要通过masterFragment进行调用交互,用来更新selected中的值
    public void select(Item item) {
        selected.setValue(item);
    }

    //主要给detailFragment进行回调,用来通知selected的值的更新情况
    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 -> {
            //当点击某一个item的时候,更新viewmode中的selected的值
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //在onCreate中绑定ViewMode的selected的值,当有更新时通知DetailFragment
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, item -> {
           // Update the UI.
        });
    }
}

上述代码的逻辑很简单,MasterFragment与DetailFragment并不直接进行交互,而是各自与ViewMode进行交互,MasterFragment用来更新维护ViewMode中的数据,DetailFragment可以收到来自ViewMode中数据更新的通知。这样便达到了两个frangment之间的数据通信。

6. 后记

ViewMode其实还有很多应用场景,但是需要和LiveData、Room等其他的JetPack构件一起使用效果更佳,所以这里先卖个关子,等后续内容将LiveData、Room等内容都涉及到后,我们统一利用这些构件来尝试做一些更牛叉的事情。

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

推荐阅读更多精彩内容