探索MVVM -- 体会DataBinding的魅力

来源:互联网 发布:qq影音 知乎 编辑:程序博客网 时间:2024/05/10 16:34

前言

本文用到的demo是以“「ONE · 一个」”的API为基础,模仿其功能实践一下MVVM的用法,以感受MVVM架构为主要目的,并未完全模仿其功能,代码结构借鉴了Google的sample。 虽然谷歌今年出了一个架构组件指南,来指导开发者构建App,但对于在实际生产过程中常用到MVP和MVVM的还是不能忽略其用法,毕竟都是架构思想都是一步一步在朝着解耦化,规范化演进。以前写东西用得最多的就是MVP架构,可能是也没接触过大型的用户级项目,我对于网上提到MVP的一些缺点的感受还不是很深刻,相反,我更认为在团队合作中,合理使用MVP更能规范化开发的形式,看队友的代码的时候也能很顺其自然地找到相应功能对应的类。初次接触MVVM还是在一年前帮别人改代码时候发现DataBinding绑定View的炫酷操作,瞬间就去学了学基础用法,后来DataBinding也被google完善了不少,借此我也去学了学MVVM。

1. 是什么?

mvvm基础架构
正如上图所示,MVVM即Model-View-ViewModel的缩写,特点是ViewModel与View进行双向绑定,View的更新可以实时反映到ViewModel中,ViewModel里面数据的变动,也可以实时渲染到View上,而数据的提供则是来自Model,这样的架构设计出来无疑是将视图与数据隔离开来的,带到一定的解耦目的。为了实现ViewModel和View的双向绑定,DataBinding这个库就起到了关键作用,他会更具布局自动生成一个Binding 类,通过这个Binding 类来包装整个View。当然本文内容并不怎么涉及到DataBinding的API用法,只是以整体架构为主,来做一些思考。

2. 怎么用?

2.1先来看一个主界面:

onelist
onelist1

这个界面可以左右滑动,每个界面对应于一个日期,每个界面中有一个列表,用于显示不同的ViewType的数据,针对于上面这个界面特点,我采用了如下的思路:
onelist架构
(在Google的sample中,每个Activity总是对应了一个Fragment,将具体的一些业务逻辑交给了Fragment来处理,在我看来这种处理的意义在于将View的逻辑进一步抽离开来,Activity专注于生命周期的变化以及一些UI的变化,如Toolbar、Drawerlayout,Dialog等,而Fragment则负责具体的逻辑部分,但目前我对与这种写法的优势认识得并不是很深刻,甚至觉得还不如就写在Activity里面来得方便。不过既然抱着学习的心态,还是得认真写,经历点实际场景或许会对我的理解会有更多的帮助。)
最外层是一个Activity,里面又一个Fragment用于承载具体的View与控制相关的业务逻辑,在Fragment中有一个ViwPager用于左右滑动显示最近10天的内容。
从内而外地看,每个ItemView都绑定了一个ItemViewModel,一旦ItemViewModel的内容发生变化,就会立马更新ItemView。最外层的Fragment也绑定了一个单独的FragmentViewModel,用于界面显示的数据。不同的两个ViewModel的数据则是由最右边的Model提供,为了减少网络请求的次数,数据的来源做了一个二级缓存的处理,加载数据的默认顺序为:内存 -> 本地 -> 网络,其中Model依赖的是一个接口,因此,在可以灵活地根据需要向其中注入数据的来源。
整个的一个项目结构如下图:
项目结构
有了项目结构图,先从数据部分入手,直接看到OneListModel这个提供数据的类。

package com.xushuzhan.theonedemo.model.onelist;/** * Created by xushuzhan on 2017/11/27. */public class OneListModel {    private static final String TAG = "OneListModel";    OneListBaseDatail mOneListBaseData;    public OneListModel(OneListBaseDatail oneListBaseData) {            mOneListBaseData = oneListBaseData;    }    public void getData(int idPosition, DataCallBack dataCallBack) {        mOneListBaseData.getIdListBeanObservable()                .flatMap(listJsonWrapper -> mOneListBaseData.getItemBeanObservable(listJsonWrapper.getData().get(idPosition)))                .subscribeOn(Schedulers.io())                .observeOn(AndroidSchedulers.mainThread())                .subscribe(oneListBeanJsonWrapper -> {                    dataCallBack.onLoadData(oneListBeanJsonWrapper.getData());                }, throwable ->                {                    Log.e(TAG, "getData: " + throwable.getMessage());                    throwable.printStackTrace();                });    }}

结构非常简单,通过构造方法注入OneListBaseDatailOneListBaseDatail是一个接口,定义了这个界面获取数据两个接口:

public interface OneListBaseDatail {     //通过id获取到列表的数据    Observable<JsonWrapper<OneListBean>> getItemBeanObservable(String id);    //获取id的列表    Observable<JsonWrapper<List<String>>> getIdListBeanObservable();}

由于这里接口的逻辑是先请求一次文章列表的id,拿到最近10天的文章列表id,再通过这个id去请求具体某天的文章列表,所以接口中定义了两个方法,都直接返回一个Observable,用于完成具体的请求。
OneListModel中,通过调用getIdListBeanObservable()方法,得到id列表,再通过RxJava的flatMap操作符,将结果转化成我们需要的文章列表数据发送给下游,在下游的观察者中通过一个dataCallBack.onLoadData()回调,将数据回调给对应的OneListViewModel,数据部分上层的逻辑大致就是这样。
再来看看下层的逻辑,注入到其中的OneListBaseDatail其实是一个实现了二级缓存的数据获取类,只是这里通过依赖抽象来屏蔽了具体的数据获取逻辑。数据来源有三种:网络,本地,内存。
以网络部分为例:

package com.xushuzhan.theonedemo.model.data.remote.onelist;/** * Created by xushuzhan on 2017/11/27. */public class OneListRemoteData implements OneListBaseData {    OneListService oneListService;    public OneListRemoteData(){         createApiService();    }    final void createApiService(){        oneListService = RetrofitManager.INSTANCE                .getRetrofit()                .create(OneListService.class);    }    @Override    public Observable<JsonWrapper<OneListBean>> getItemBeanObservable(String id) {        return oneListService.getOneList(id);    }    @Override    public Observable<JsonWrapper<List<String>>> getIdListBeanObservable() {        return oneListService.getIdList();    }}

从网络获取数据的逻辑就是构建一个Retrofit对象,再构建一个OneListService,通过它便可以方便地在接口方法中完成网络请求。
由于上层都是依赖的接口,所以这里的底层只需在对应的接口方法中实现自己的逻辑,将结果返回给上层即可。其他的两种获取数据来源的方式都是如此的思路。
之后实现的二级缓存类就使用了一种类似于静态代理模式的形式,将三种数据获取方式综合起来:

package com.xushuzhan.theonedemo.model.onelist;/** * Created by xushuzhan on 2017/11/28. */public class OneListMultiData implements OneListBaseData {    private static final String TAG = "OneListMultiData";    OneListRemoteData oneListRemoteData;    OneListLocalData oneListLocalData;    HashMap<String, Observable<JsonWrapper<OneListBean>>> mItemBeanCache = new HashMap<>();    Observable<JsonWrapper<List<String>>> mIdListBeanCache;    public static class Holder{      static OneListMultiData INSTANCE = new OneListMultiData(new OneListRemoteData(),new OneListLocalData());    }    public static OneListMultiData getInstance(){        return Holder.INSTANCE;    }    private OneListMultiData(@NonNull OneListRemoteData oneListRemoteData, @NonNull OneListLocalData oneListLocalData) {        this.oneListRemoteData = oneListRemoteData;        this.oneListLocalData = oneListLocalData;    }    @Override    public Observable<JsonWrapper<OneListBean>> getItemBeanObservable(String id) {        String s = "来自内存";        Observable<JsonWrapper<OneListBean>> observable = mItemBeanCache.get(id);        if (observable == null) {            observable = oneListLocalData.getItemBeanObservable(id);            mItemBeanCache.put(id, observable);            s = "来自本地";        }        if (observable == null) {            observable = oneListRemoteData.getItemBeanObservable(id);            mItemBeanCache.put(id, observable);            s = "来自网络";        }        Log.d(TAG, "getItemBeanObservable: "+s);        return observable;    }    @Override    public Observable<JsonWrapper<List<String>>> getIdListBeanObservable() {        if (mIdListBeanCache == null){            mIdListBeanCache = oneListLocalData.getIdListBeanObservable();        }        if (mIdListBeanCache == null){            mIdListBeanCache = oneListRemoteData.getIdListBeanObservable();        }        return mIdListBeanCache;    }}

Model看完了,就来看看ViewModle,ViewModle在MVVM中起着中间者的作用,一边要和View进行双向绑定,一边还要和Model进行交互。
OneListViewModel开始看:

package com.xushuzhan.theonedemo.viewmodel.onelist;/** * Created by xushuzhan on 2017/11/30. * OneListViewModel 通过DataCallBack回调与OneListModel进行数据交互 * OneListViewModel 通过DataLoadCallBack回调与Fragment进行数据交互 */public class OneListViewModel implements DataCallBack {    private static final String TAG = "OneListViewModel";    private DataLoadCallBack mDataLoadCallBack ;    private OneListModel mOneListModel;    public OneListViewModel() {        mOneListModel = new OneListModel(OneListMultiData.getInstance());    }    public void getData(int pagePisitionn,DataLoadCallBack dataLoadCallBack){        mDataLoadCallBack = dataLoadCallBack;        mOneListModel.getData(pagePisitionn,this);    }    @Override    public void onLoadData(OneListBean oneListBean) {        mDataLoadCallBack.onComplete(oneListBean);    }    @Override    public void onGetIdList(List<String> idList) {    }}

她的作用无非就是实例化一个OneListModel,然后更具View传进来的参数进行数据的获取,之后通过接口将接口回调给View,比较简单,由于数据都在RecyclerView的Item中,所以这里暂时没涉及到数据绑定。
数据的绑定体现在了OneListItemViewModule中:

package com.xushuzhan.theonedemo.viewmodel.onelist;/** * Created by xushuzhan on 2017/11/29. */public class OneListItemViewModule{    private static final String TAG = "OneListItemViewModule";    public final ObservableField<String> title = new ObservableField<>();    public final ObservableField<String> picInfo = new ObservableField<>();    public final ObservableField<String> content = new ObservableField<>();    public final ObservableField<String> wordsInfo = new ObservableField<>();    public final ObservableField<String> imageUrl = new ObservableField<>();    //normal item's property    public final ObservableField<String> categry = new ObservableField<>();    public final ObservableField<String> author = new ObservableField<>();    public OneListBean.ContentListBean contentListBean;    public OneListItemViewModule(OneListBean.ContentListBean contentListBean) {        this.contentListBean = contentListBean;        update(contentListBean);    }    public void update(OneListBean.ContentListBean contentListBean){        title.set(contentListBean.getTitle());        picInfo.set(contentListBean.getPic_info());        content.set(contentListBean.getForward());        wordsInfo.set(contentListBean.getWords_info());        imageUrl.set(contentListBean.getImg_url());        categry.set(contentListBean.getShare_list().getWx().getTitle().split("\\|")[0]);        author.set(contentListBean.getAuthor().getUser_name());    }    @BindingAdapter({"image_url"})    public static void loadImage(ImageView imageView, String url){        Glide.with(imageView.getContext())                .load(url)                .into(imageView);    }}

一看就是典型的DataBinding的双向绑定的套路,并且还自定义了一个BindingAdapter方法,用来加载图片。在OneListAdapter中的onBindViewHolder中便可以将每个Item对应的数据设置进来。
最后就剩View了,在Model和ViewModel中做了这么多的工作,留给View的工作自然就少了,这里的View主要是由Fragment(OneCommonFragment)来体现,逻辑也十分简单:

package com.xushuzhan.theonedemo.view.onelist;/** * 公共的Fragment * Created by xushuzhan on 2017/11/27. */public class OneCommonFragment extends Fragment implements DataLoadCallBack{    private static final String TAG = "OneCommonFragment";    public static final String LIST_ID = "list_id";    public static final String ITEM_CATEGORY = "item_category";    public static final String ITEM_ID = "item_id";    FragmentOneCommonBinding mFragmentCommonOneBinding;    @Nullable    @Override    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        mFragmentCommonOneBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_one_common,container,false);        return mFragmentCommonOneBinding.getRoot();    }    @Override    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {        super.onViewCreated(view, savedInstanceState);        int listId = getArguments().getInt(LIST_ID);        OneListViewModel oneListViewModel = new OneListViewModel();        oneListViewModel.getData(listId,this);    }    public static Fragment newInstance(int listId){        Fragment fragment = new OneCommonFragment();        Bundle bundle = new Bundle();        bundle.putInt(LIST_ID,listId);        fragment.setArguments(bundle);        return fragment;    }    @Override    public void onComplete(OneListBean oneListBean) {        mFragmentCommonOneBinding.rvOneCommonFragment.setLayoutManager(new LinearLayoutManager(getContext()));        OneListAdapter oneListAdapter = new OneListAdapter(oneListBean.getContent_list());        oneListAdapter.setOnItemClickListener(contentListBean -> {            Intent intent = new Intent(OneCommonFragment.this.getContext(), OneDetailActivity.class);            intent.putExtra(ITEM_CATEGORY,contentListBean.getCategory());            intent.putExtra(ITEM_ID,contentListBean.getItem_id());            startActivity(intent);        });        mFragmentCommonOneBinding.rvOneCommonFragment.setAdapter(oneListAdapter);    }}

由于界面里的ViewPager中的Fragment有10个,并且布局和业务逻辑相同,肯定就是要抽象一个公共的Fragment出来,具体的业务逻辑都在这里实现,他也持有了ViewModel的引用,主要就是做一些使用数据和RecyclerView的初始化工作。

2.2 数据详情部分

基本的界面写好了,就该来写详情界面了,由于接口是来自github,所以对于数据的处理也是一个值得注意的地方。
首先还是上一张OneListDetailActivity的整体设计图:
整体架构

1.首先,对于不同类型的Item,对应了不同布局的详情界面,接口返回的数据的字段也有所不同,正对于这样的不同肯定不可能定义多个Fragment来显示每种类型的界面,这样开销太大了。再者,数据格式不同的花ViewModel也会有所差异,所以,我就考虑自己写几个Converter类来将不同格式的数据统一转化为一个HTML形式的文本,最后用WebView来渲染,以屏蔽数据格式的差异,采用同一个ViewModel和统一的显示方式。由于电影和音乐两个页面的并不单单提供显示功能,所以我也考度将它们单独抽离出来,单独绑定一个ViewModel。

normal question music

2.还是从Converter部分看起吧:
Converter
以MusicConverter为例:

public class MusicConverter {    public static String convert(MusicBean content){        String pic = "<div style=\"position: relative; \">\n" +                "\n" +                "<img src=\""+content.getCover()+"\" style=\"width: 130px;position: absolute;\n" +                "    top: 50%;\n" +                "    left: 50%;\n" +                "    margin-left: -65px;\n" +                "    margin-top: -65px;\n" +                "    z-index: 1;\" >\n" +                "\n" +                " <img src=\""+content.getCover()+"\" style=\" \n" +                "    height: 230px;\n" +                "    display: block;\n" +                "    width: -webkit-fill-available;\n" +                "     -webkit-filter: blur(10px);\n" +                "       -moz-filter: blur(10px);\n" +                "        -ms-filter: blur(10px);    \n" +                "            filter: blur(10px);  \">\n" +                " </div>";        String title = "<p style=\"font-size: 28px;font-weight: bold;\">" + content.getStory_title()+"</p>";        String author = "<p style=\"font-size: 13px;\">文/"+content.getStory_author().getUser_name()+"</p>";        return pic+title+author+content.getStory();    }}

提供了静态的convert方法将接口返回的不同内容拼接成HTML,最后显示在WebView中,其他几个类别的数据处理都是采用了这种策略。
3.依然采用了OneListDetailRemoteData来管理所有分类的请求结果的Observable。

package com.xushuzhan.theonedemo.model.data.remote.onelistdetail;/** * Created by xushuzhan on 2017/12/4. */public class OneListDetailRemoteData implements OneListDetailBaseData {    private static final String TAG = "OneListDetailRemoteData";    OneListDetailService mOneListDetailService;    public OneListDetailRemoteData() {        mOneListDetailService = RetrofitManager.INSTANCE.getRetrofit().create(OneListDetailService.class);    }    @Override    public <T> Observable<T> getContent(String itemId, String category) {        switch (category) {            case Config.ONE_DETAIL_CATEGORY_SERIALIZE:                return (Observable<T>) mOneListDetailService.getSerializedContent(itemId);            case Config.ONE_DETAIL_CATEGORY_ESSAY:                return (Observable<T>) mOneListDetailService.getReadingContent(itemId);            case Config.ONE_DETAIL_CATEGORY_ASK_ANSWER:                return (Observable<T>) mOneListDetailService.getQuestionContent(itemId);            case Config.ONE_DETAIL_CATEGORY__MUSIC:                return (Observable<T>) mOneListDetailService.getMusicContent(itemId);            case Config.ONE_DETAIL_CATEGORY_MOVIE:                Observable<JsonWrapper<MovieDetailBean>> detail = mOneListDetailService.getMovieContent(itemId);                Observable<JsonWrapper<MovieInfoBean>> info = mOneListDetailService.getMovieInfo(itemId);                return (Observable<T>) Observable.zip(detail, info, (movieDetailBeanJsonWrapper, movieInfoBeanJsonWrapper) -> {                    MovieBean movieBean = new MovieBean(movieInfoBeanJsonWrapper.getData().getDetailcover(),                            movieDetailBeanJsonWrapper.getData().getData().get(0).getTitle(),                            movieDetailBeanJsonWrapper.getData().getData().get(0).getContent(),                            movieDetailBeanJsonWrapper.getData().getData().get(0).getUser().getUser_name());                    return movieBean;                });            default:                return (Observable<T>) mOneListDetailService.getReadingContent(itemId);        }    }}

在遇到同一个界面的数据来源是不同接口的情况下由于引入了RxJava也可以用丰富的操作符很好的解决。

3.总体感受

说了这么多,这个Demo的实现思路也就是这样,整个思考的过程对与我来说是最重要的,特别是对与整个架构逻辑的把握,给我也带来了许多收获,对于接口返回不同数据也着实花了一番功夫来想办法处理。就这个MVVM架构而言,Databinding是一个十分核心的库,它解决了数据绑定的核心功能,有了它,实现这种架构思想也变得十分容易实现。不过,任何东西有得必有失,在运用设计模式和架构思想的时候,势必会带来更多的代码量。也就是牺牲了代码量来换取APP的灵活性,所以,在大型的项目中MVVM更有优势,便于团队维护。

完整代码:https://github.com/Solinzon/TheOneDemo,欢迎大家指正。

原创粉丝点击