探索MVVM -- 体会DataBinding的魅力

前言

本文用到的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,欢迎大家指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值