探索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即Model-View-ViewModel的缩写,特点是ViewModel与View进行双向绑定,View的更新可以实时反映到ViewModel中,ViewModel里面数据的变动,也可以实时渲染到View上,而数据的提供则是来自Model,这样的架构设计出来无疑是将视图与数据隔离开来的,带到一定的解耦目的。为了实现ViewModel和View的双向绑定,DataBinding这个库就起到了关键作用,他会更具布局自动生成一个Binding 类,通过这个Binding 类来包装整个View。当然本文内容并不怎么涉及到DataBinding的API用法,只是以整体架构为主,来做一些思考。
2. 怎么用?
2.1先来看一个主界面:
这个界面可以左右滑动,每个界面对应于一个日期,每个界面中有一个列表,用于显示不同的ViewType的数据,针对于上面这个界面特点,我采用了如下的思路:
(在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(); }); }}
结构非常简单,通过构造方法注入OneListBaseDatail
,OneListBaseDatail
是一个接口,定义了这个界面获取数据两个接口:
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。
2.还是从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,欢迎大家指正。
- 探索MVVM -- 体会DataBinding的魅力
- MVVM + dataBinding
- android MVVM DataBinding
- DataBinding实现MVVM
- MVVM之DataBinding
- MvvM 之databinding
- Android DataBinding && MVVM [U04]
- MVVM.DataBinding学习总结
- Android DataBinding & MVVM
- MVVM之DataBinding入门
- Android MVVM框架 DataBinding
- 对MVP、MVVM、DataBinding、ButterKnife、Dagger2的初步学习
- Android之MVVM开发模式和DataBinding的简单用法
- 高效开发 MVVM 和 databinding 你需要使用的工具
- 算法的魅力——读《算法帝国》体会
- 终于体会到数学对于程序员的魅力
- 求最大子序列和--体会算法的魅力
- 1 第一次在工作中体会到数据结构的魅力
- 对象
- C语言学习的第三天
- Android开发之运行客户的Demo拿不到数据
- [LOJ6031][雅礼集训2017Day1]字符串-后缀自动机-莫队算法-倍增LCA
- Codeforces Round #446(Div.2)Problem E Envy(并查集)
- 探索MVVM -- 体会DataBinding的魅力
- 浅谈字符串常量
- Linux grep命令使用大全
- java 关于begdecimal类的简单说明
- 【耀阳的读书笔记】算法导论(1)_一切始于排序
- 解决Spyder3 安装Tensorflow后代码补全失效问题
- 712. Minimum ASCII Delete Sum for Two Strings
- vue-cli项目引入assets里的css样式出错
- MNIST.PKL.GZ加载问题,TypeError: object of type 'zip' ,training_data zip at 0x58ac4c8>