探究Google力推的JetPack库<一>---------Lifecycles、LiveData、ViewModel、手写LiveDataBus
上一次https://www.cnblogs.com/webor2006/p/12463543.html对于MVVM架构的进行了一个学习,其中提到了Google官方其实推出了一个MVVM的工具框架:Data Binding,本来计划着这次是来研究一下利用官方的MVVM的这个框架的,但其实它是属于JetPack库中的一员,所以这里有必要先对JetPack进行一个了解。
什么是JetPack库?
上Android开发者官网https://developer.android.com/jetpack来了解一下:
而这套库总的来说包含以下几类东东:
而每一个类别里面又包含非常丰富的库,下面看下官网对它的详情:
从图中可以看到,基本上把我们实际开发APP的各个点都覆盖全了,是不是能感觉以后开发Android APP的成本越来越低,所有的Google都帮你考虑到了,而且Google还会不断对这段库进行更新维护,等于如果APP使用Jetpack来搭建就相当于是遵行了Google的最佳开发实践了,那可想比我们传统纯自己没有规范的篇写出来的APP质量要高得多,所以对于这门技术非常有必要了解一下,不管是项目中有没有使用它,在未来我猜肯定是一个趋势,所以提前了解这门技术对于未来的职场之路肯定是有利无害的。
这里如图上所示,只学习跟架构相关的组件,所以这里先对要学的组件进行一个总览,看都是干嘛用的:
好,下面则来一个个进行学习,下面还是基于之前学习MVP时的那个商品列表加载的案例为例,不过这里还是用的简单的MVC的方式,用最精简的代码来学习是最好的,所以这里简单再把目前代码的结构贴一下:
package com.android.jetpackstudy; import android.os.Bundle; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; import com.android.jetpackstudy.adapter.MyAdapter; import com.android.jetpackstudy.bean.Shoe; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView listView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.listview); loadData(); } private void loadData() { List<Shoe> data = new ArrayList<>(); data.add(new Shoe(R.drawable.shop1, 1000, 10000)); data.add(new Shoe(R.drawable.shop2, 111, 90)); data.add(new Shoe(R.drawable.shop3, 2222, 800)); data.add(new Shoe(R.drawable.shop4, 333, 110)); data.add(new Shoe(R.drawable.shop5, 4444, 220)); data.add(new Shoe(R.drawable.shop6, 100, 330)); data.add(new Shoe(R.drawable.shop7, 20, 0)); data.add(new Shoe(R.drawable.shop8, 10000, 20)); data.add(new Shoe(R.drawable.shop9, 500, 120)); data.add(new Shoe(R.drawable.shop10, 30, 400)); listView.setAdapter(new MyAdapter(this, data)); } }
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/listview" android:layout_width="fill_parent" android:layout_height="fill_parent" tools:ignore="MissingConstraints" /> </LinearLayout>
item.xml:
<?xml version="1.0" encoding="UTF-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="8dip" android:layout_marginTop="20dip" android:layout_toRightOf="@id/iv_icon" android:orientation="vertical"> <TextView android:id="@+id/tv_style" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#666" android:textSize="18sp" /> <TextView android:id="@+id/tv_like" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:textColor="#666" android:textSize="15sp" /> </LinearLayout> </RelativeLayout>
package com.android.jetpackstudy.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import com.android.jetpackstudy.R; import com.android.jetpackstudy.bean.Shoe; import java.util.List; public class MyAdapter extends BaseAdapter { private LayoutInflater inflater; private List<Shoe> shoes; public MyAdapter(Context context, List<Shoe> girs) { inflater = LayoutInflater.from(context); this.shoes = girs; } @Override public int getCount() { return shoes.size(); } @Override public Object getItem(int position) { return shoes.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = inflater.inflate(R.layout.item, null); Shoe g = shoes.get(position); ImageView iv_icon = (ImageView) view.findViewById(R.id.iv_icon); iv_icon.setImageResource(g.icon); TextView tv_like = (TextView) view.findViewById(R.id.tv_like); tv_like.setText("销量:" + g.sales); TextView tv_style = (TextView) view.findViewById(R.id.tv_style); tv_style.setText("库存:" + g.inventory); return view; } }
package com.android.jetpackstudy.bean; public class ResponceData { /** * result : null * reason : 当前可请求的次数不足 * error_code : 10012 * resultcode : 112 */ private String result; private String reason; private int error_code; private String resultcode; public void setResult(String result) { this.result = result; } public void setReason(String reason) { this.reason = reason; } public void setError_code(int error_code) { this.error_code = error_code; } public void setResultcode(String resultcode) { this.resultcode = resultcode; } public String getResult() { return result; } public String getReason() { return reason; } public int getError_code() { return error_code; } public String getResultcode() { return resultcode; } }
package com.android.jetpackstudy.bean; /** * 实体 */ public class Shoe { public int icon; public int sales;//销量 public int inventory;//库存 public Shoe(int icon, int sales, int inventory) { this.icon = icon; this.sales = sales; this.inventory = inventory; } public Shoe() { } public int getIcon() { return icon; } public void setIcon(int icon) { this.icon = icon; } public int getSales() { return sales; } public void setSales(int sales) { this.sales = sales; } public int getInventory() { return inventory; } public void setInventory(int inventory) { this.inventory = inventory; } }
Lifecycles:
在前面也看下这块是用来帮我们管理生命周期的,那正式学习之前还是上官网对它进行一个更细的了解:
哦,总之感觉很高大上的样子,简单猜一下它的作用就是:比如之前我们要管理某个组件的一些生命周期则会将代码散步在各个Activity和Fragment的生命周期回调函数里面,这个对于大家来说是再熟悉不过了,写个伪代码:
而要管理这个组件的生命周期,则需要这样做:
很显然这种方式很容易出错,为啥?
而这些生命周期的管理其实可以由这个组件来统一管理,那接下来咱们就来看一下它的用法:
先来新建一个生命周期的观察者:
目前咱们只来观察这个onStart()试验一下既可,那怎么知道在onStart()生命周期能够调用到我们所写的onStart()呢,此时需要用上注解了,如下:
接下来则来绑定一下既可,如下:
就这么简单,此时咱们运行一下看一下效果:
好,这就是它的简单使用,至于它的原理在之后再来剖析,可以看到对于依赖组件的生命周期的管理就从Activity中的模板代码中解放了。
LiveData&ViewModel:
啥意思的呢?咱们直接使用一下便知:
其中MutableLiveData的结构为:
接下来则提供一个获取仓库的方法:
好,接下来则可以来调用一下,注意这里不是直接去new这个MyShoe类哈,此时需要先增加一个依赖包:
然后调用之:
package com.android.jetpackstudy; import android.os.Bundle; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; import com.android.jetpackstudy.adapter.MyAdapter; import com.android.jetpackstudy.bean.Shoe; import com.android.jetpackstudy.viewmodel.MyShoe; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView listView; private MyShoe myShoe; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.listview); loadData(); getLifecycle().addObserver(new MyLifeObserver()); //调用系统API初始化这个对象 myShoe = ViewModelProviders.of(this).get(MyShoe.class); myShoe.getDataBean().observe(this, new Observer<List<Shoe>>() { @Override public void onChanged(List<Shoe> shoes) { //当我们的数据发生变化的时候,我们可以在这个onChanged中进行处理 listView.setAdapter(new MyAdapter(MainActivity.this, shoes)); } }); } private void loadData() { List<Shoe> data = new ArrayList<>(); data.add(new Shoe(R.drawable.shop1, 1000, 10000)); data.add(new Shoe(R.drawable.shop2, 111, 90)); data.add(new Shoe(R.drawable.shop3, 2222, 800)); data.add(new Shoe(R.drawable.shop4, 333, 110)); data.add(new Shoe(R.drawable.shop5, 4444, 220)); data.add(new Shoe(R.drawable.shop6, 100, 330)); data.add(new Shoe(R.drawable.shop7, 20, 0)); data.add(new Shoe(R.drawable.shop8, 10000, 20)); data.add(new Shoe(R.drawable.shop9, 500, 120)); data.add(new Shoe(R.drawable.shop10, 30, 400)); listView.setAdapter(new MyAdapter(this, data)); } }
那怎么来让咱们的数据源发生变化呢?这里这样来模拟,就是点击ListView项时会跳到第二个界面,然后我们在第二个界面来改变LiveData的数据源,看是否在主界面能收到通知既可,所以先来给ListView增加点击事件,然后看能否在当前界面改了数据源收到改变的结果:
运行瞅一下:
嗯,很方便,那如果在不同的界面来更新数据源,那在主界面的ListView还能收到变更么?试一下:
再来运行:
另外这里有一个细节,就是对于LiveData的这个字段必须定义成一个static的:
为啥?此时将它去掉运行就晓得了:
崩溃了,看一下日志:
所以,此时还是将static还原,这样就在整个APP之间数据都只有一份共享了。这种数据源实时变更的效果如果是用传统的方式还是比较麻烦的,要么是通过回调,要么通过EventBus之类的来通知更新,用了Google的这套框架一切变得是那么的简单。
LiveDataBus
概念:
平常事件总线这块我们通常会使用EventBus或RxBus,而有了Jetpack的LiveData这个组件,咱们就可以来构建一条类似的总线框架了,从上面的实验也可以看出效果跟EventBus真的很像,也就是对于消息变化的感知非常灵敏,在网上有人说它可以代替EventBus和RxBus,如博主https://www.cnblogs.com/meituantech/p/9376449.html的说明:
代不代替这就不是我所关心的事,但是对于新的技术的研究透才是核心,知道它是干嘛的以及使用,要换也是分分钟的事,接下来咱们手写一个LiveDataBus框架,写完之后你就自然而然的知道它跟EventBus有啥优劣了。
架构思想:
从网上贴一张关于LiveDataBus的架构图,其实就是一个典型的观察者模型:
这里以卖华为手机这个场景为例,华为公司有新品发布时则会通知三方电商平台(天猫、京东)进行上架销售,而消费者则会提前先到三方平台进行订阅,当平台一发布则订阅了的消费者则会第一时间收到通知,其中华为则为发布者,消费者则为订阅者,而京东天猫平台则是消息总线。
具体实现:
这里新建一个module,所有LiveDataBus都写在其中:
定义消息总线(天猫京东平台):
接下来则来定义消息订阅方法:
至此,消息总线代码就写完了,简不简单,而且完全没有导三方依赖包,都是androidx里面的,从这点看就优于EventBus了。
消费者订阅:
接下来咱们就可以进行消息订阅了,先来弄个实体:
然后来进行消息订阅:
package com.android.livedatabus; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; import android.os.Bundle; import android.widget.Toast; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //消费者订阅消息 LiveDataBus.getInstance().with("huawei", Huawei.class) .observe(this, new Observer<Huawei>() { @Override public void onChanged(Huawei huawei) { if (huawei != null) { Toast.makeText(MainActivity.this, huawei.getType(), Toast.LENGTH_SHORT).show(); } } }); } }
上面就是典型LiveData的使用。
发布者发送:
这里增加一个发送按钮:
咱们来运行一下:
使用方式跟EventBus基本差不多的。
进一步完善:
其实目前框架还是有些问题的,这里新建第二个Activity,然后也来注册,再来运行看一下:
package com.android.livedatabus; import android.os.Bundle; import android.view.View; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; public class SecActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sec); //消费者订阅消息 LiveDataBus.getInstance().with("huawei", Huawei.class).observe( this, new Observer<Huawei>() {//观查者 @Override public void onChanged(Huawei huawei) { if (huawei != null) { Toast.makeText(SecActivity.this, huawei.getType(), Toast.LENGTH_SHORT).show(); } } } ); } /** * 发布者 * * @param view */ public void sendMessage(View view) { Huawei huawei = new Huawei("META-20"); //厂家发布消息 LiveDataBus.getInstance().with("huawei", Huawei.class).postValue(huawei); } }
然后清单中注册一个这个Activity,再来运行看一下此时问题就来了:
看到木有,跳到第二个界面都还没点击“发送消息”按钮居然也收到消息了。。所以接下来要来修复这个问题,而要修复之前得要明确产生问题的原因,所以此时需要来查阅一下它的源码的实现,这里总共就是一发一收,先来看收的这块处理逻辑,目前的现象就是在接收处进行了多余的打印了,如下:
此时就需要转到MutableLiveData这个对象来瞅一下:
父类打开之后代码比较多,直接搜一下我们要看到onChanged():
而从这个方法可以看到,有三处可以拦截onChanged()方法的回调:
那思路初步构成:假如我们在跳到第二个界面时让以上三个条件有一个能满足要求不就能解决我们目前面临的问题了么?嗯,接一下则来决定要用哪个条件下手了,大致瞅一下这三处的条件:
接下来再来看第二个条件:
貌似也有生命周期,它会调activeStateChanged(),所以也先放弃,最后发现用第三个条件来处理是最合适的:
而这个mVersion是在我们每次发消息时会自增的:
所以造成咱们目前这个问题的原因就是这个条件出问题了,因为在MainActivity点击发送时,mVersion变为1了,然后此时再跳到第二个界面,由于在执行在这个方法时:
所以还没发送就又调了onChanged()方法了,所以咱们来想办法让这个条件能够成立,接下来则来看怎么做到:
这个字段好拿,通过反射既可,接下来就是看observer.mLastVersion怎么拿呢?
而它其实是我们传递进来的:
此时咱们就可以对它里面的observe()方法进行一个hook,将里面的version给改了,具体如下:
接下来则集中来处理hook既可,其思想就是将这两version赋成相等既可,这里就直接贴一下代码了:
package com.android.livedatabus; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * 这个类就是我们的总线(天猫,京东) */ public class LiveDataBus { //存放订阅者 private Map<String, BusMutableLiveData<Object>> bus; private static LiveDataBus liveDataBus = new LiveDataBus(); private LiveDataBus() { bus = new HashMap<>(); } public static LiveDataBus getInstance() { return liveDataBus; } /** * 用来给用户进行订阅(存入map) */ public synchronized <T> BusMutableLiveData<T> with(String key, Class<T> type) { if (!bus.containsKey(key)) { bus.put(key, new BusMutableLiveData<Object>()); } return (BusMutableLiveData<T>) bus.get(key); } public static class BusMutableLiveData<T> extends MutableLiveData<T> { @Override public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { super.observe(owner, observer); hook(observer); } private void hook(Observer<? super T> observer) { try { //1.得到mLastVersion Class<LiveData> liveDataClass = LiveData.class; Field mObserversField = liveDataClass.getDeclaredField("mObservers"); mObserversField.setAccessible(true); //获取到这个成员变量对应的对象 Object mObserversObject = mObserversField.get(this); //得到map Class<?> mObserversObjectClass = mObserversObject.getClass(); //获取到mObservers对象的get方法 Method get = mObserversObjectClass.getDeclaredMethod("get", Object.class); get.setAccessible(true); //执行get方法 Object invokeEntry = get.invoke(mObserversObject, observer); //取到map中的value Object observerWraper = null; if (invokeEntry != null && invokeEntry instanceof Map.Entry) { observerWraper = ((Map.Entry) invokeEntry).getValue(); } if (observerWraper == null) { throw new NullPointerException("observerWraper is null"); } //得到ObserverWrapper的类对象 Class<?> superclass = observerWraper.getClass().getSuperclass(); Field mLastVersion = superclass.getDeclaredField("mLastVersion"); mLastVersion.setAccessible(true); //2.得到mVersion Field mVersion = liveDataClass.getDeclaredField("mVersion"); mVersion.setAccessible(true); //3.把mVersion的值填入到mLastVersion中 Object mVersionValue = mVersion.get(this); mLastVersion.set(observerWraper, mVersionValue); } catch (Exception e) { e.printStackTrace(); } } } }
其中要注意有的问题,注意这块的代码:
debug看一下就晓得了,字节码看到的是具体实现类:
很明显mLastVersion是在它的父类中定义的,所以需要获得它的父类型。
好,运行看一下:
嗯,完美解决跳第二个界面时的问题了,但是为啥从第二个界面返回到主界面时自动又弹了呢?其实是由于本身第一个界面就已经订阅了当然是能收到了,它并不是每一次初始化LiveData,而我们的问题是在刚进第二个界面才初始化LiveData时就收到消息了,根据用户的使用来讲是不符合常理的,当然这块是不是bug得根据实际业务来判断,如果产品就是要求默认的LiveDataBus的行为,那就没必要进行hook了。