前几天接触公司一Android项目,刚看代码时,不知道这么多层级的代码都是干嘛的,看着有点儿懵。只有清楚了结构和流程,才能够在浩瀚的代码里游刃有余。
先不管局部是什么,从全局上去看才能把一件事情看清楚。从宏观把握,由整体到局部,这是一种哲学和做事的方法论。就好比盲人摸象,即便再摸也不知道他摸的是一头大象。即使不是盲人,把一常人眼贴上去去摸,也未必分得清那就是大象。只有把他带远点儿从远处看,才看清了,哦,那是一头大象。
古人有句诗:“不识庐山真面目,只缘身在此山中。”,一样的道理,只有从全局把事情理清了,才能思路清晰的把一件事情看清楚。如果一下扎进某个点儿去看,往往容易一叶障目,不见森林。
比如linux操作系统代码,几千万行也有了,如果一头扎进去,盲目的看,敢说几年也看不清头绪。假如从整体去把握,了解整体结构,各个模块的作用和功能,有针对的去看,才能理清思路。相信那些精通linux内核的,不是把源码看够个遍了,而是对结构,对功能有更深入的了解。
以下为按照此方法论对一项目结构做一分析。由于零零散散的业余时间看了点儿,有哪里不对的地方请指正。
何为MVVM?懂web开发的都知道有一种风格叫MVC ,模型,视图,控制器。一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
它强制性的使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型、视图、控制器。它们各自处理自己的任务。最典型的MVC就是JSP + servlet + javabean的模式。
MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。
View绑定到ViewModel,然后执行一些命令在向它请求一个动作。而反过来,ViewModel跟Model通讯,告诉它更新来响应UI。这样便使得为应用构建UI非常的容易。往一个应用程序上贴一个界面越容易,外观设计师就越容易使用Blend来创建一个漂亮的界面。同时,当UI和功能越来越松耦合的时候,功能的可测试性就越来越强。
搞懂一件事情前不妨先问个为什么,为什么要MVVM?
只有这样才有搞下去的动力。
我们基于MVC开发完第一版本,然后企业需要迭代2.0版本,并且UI界面变化比较大,业务变动较小,怎么办呢?
当2.0的所有东西都已经评审过后。这个时候,新建布局,然后开始按照新的效果图,进行UI布局。然后还要新建Activity、Fragment把相关逻辑和数据填充到新的View上。
如果业务逻辑比较复杂,需要从Activity、Fragment中提取上个版本的所有逻辑,这个时候自己可能就要晕倒了,因为一个复杂的业务,一个Activity几千行代码也是很常见的。千辛万苦做完提取完,可能还会出现很多bug。
这个时候MVVM就闪亮登场了。
可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
在Android中,布局里可以进行一个视图逻辑,并且Model发生变化,View也随着发生变化。
低耦合。以前Activity、Fragment中需要把数据填充到View,还要进行一些视图逻辑。现在这些都可在布局中完成(具体代码请看后面)
甚至都不需要再Activity、Fragment去findViewById。这时候Activity、Fragment只需要做好的逻辑处理就可以了。
接下来分析下这套代码结构,看看它怎么就是MVVM。
先看下它都用了哪些库,涉及哪些知识。看app目录下的build.gradle文件中的包引用以及Adnroidmanifest文件,
对它用到库和知识点先做到心里有数。比如里面用到了
io.reactivex.rxjava2:rxandroid:2.0.2
compile('com.squareup.okhttp3:logging-interceptor:3.7.0')
compile('com.squareup.retrofit2:retrofit:2.4.0')
android { dataBinding { enabled = true }
大致从这几个就看出,使用了Andoid自带的databing技术,使用了很火的异步框架RxJava,使用了网络库retrofit等等
请看以下代码结构:
它咋就是MVVM的风格呢? 从MainActivity中,看不到findID和 控件事件响应的方法以及界面更新的方法。
在哪实现界面的操作和更新呢?这期中是怎样的一种逻辑?
接着看MainActivity中的OnCreate方法,
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); writeLinuxParams(); }
看不出来什么,但是往上层看,因为它是继承自自定义的BaseActivity,那就往BaseActivity找,
public class MainActivity extends BaseActivity implements MainView,IDecoderAcquirer
果然,在这里看到了
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); context = this; if (getLayResId() > 0) { setContentView(getLayResId()); } initView(); registerRxbus(); initData(); initEvent(); }
而initView里,进行了Databinding.
@Override protected void initView() { super.initView(); binding = DataBindingUtil.setContentView(this,R.layout.activity_main); contentlayout=binding.contentlayout; faceswiping=binding.includeMainQrcode.faceswiping;
接着浏览各个文件夹,大致翻看了下,
activity文件夹,放置各个activity
adpter文件夹,各种适配器类,因为有些控件 listView之类的需要传参为adpter
base文件夹,放一些基础类,供其他类继承,如BaseActivity,BaseView之类的。一般的MainAcity不直接继承系统的Activity,
而是多继承一层,如自己定义的BaseActivity,有好处的。更为灵活和便于一些控制和全局之类的操作。
Bean文件夹,放置一些可以服用的Bean。如MVVM上就需要一些Bean和界面layout上的一一对应。
Bean下面的MVVM就是和界面绑定相关的字段定义。bussiness是和业务相关的一些Bean.
controller文件夹,这个应该是跟控制相关的,放置到这里面了。
dilaog文件夹,用到的各种对话框界面。
setting文件夹,跟设置相关的一些界面(Acticvity)
service文件夹,后台服务线程的一些业务操作。
wige,文件夹,一些自定义或第三方控件
那么跟MVVM相关的,在结构上是如何体现的呢?
涉及以下几个文件夹,mode文件夹,viewmode文件夹,bean下的MVVM文件夹。Ilistenner文件夹。
他们之间的关系是什么样的?如何实现MVVM的?
翻开看代码,
public class MainViewModel implements MainListener, View.OnClickListener, VitualRequestResultListener { private String TAG = "MainViewModel"; public AuxScreenController asController; private SysParManager sysParManager;
MainViewModel继承了MainLister接口,做了哪些事情呢?
/** * 支付成功后界面刷新 */ public void refreshPaySucScreen(String name, String outID, long balance, long mainWalletFare, long subWalletFare, long consumeMoney, long mngfare) { if (Constant.modelType == 2) { inputMoney = opfareFinal; } mainSimpleBean.cardinfo_name.set(name); mainSimpleBean.cardinfo_outid.set(outID); mainSimpleBean.cardinfo_pay_state.set("消费明细"); mainSimpleBean.cardinfo_rl_jiaoyi.set(true);
从中可以大致了解到,MainViewModel这个类,负责把需要显示的内容 传递给 View(layout里绑定的控件,界面显示),且实现了View。OnClieckListener接口,负责接收界面响应。
而项目中的Ilistener文件夹,里面定义了一些接口如MainLister接口提供给MainViewModel继承。这样就相当于 解耦了一层。作为一个桥梁,中间层。把MVVM 分割为 M +V +(桥梁)+VM
V层完成界面绑定,VM层继承了Ilistener,实现了更新界面的接口,M层类里面组合使用了这些接口,把响应的数据传过去。
往下看,
要显示的数据从哪里来呢?又是怎么来的呢?
接下来看model文件夹。模型层。
public class MainModelImpl implements MainModel { private String TAG = "MainModelImpl"; private static String setTime = ""; private static String setDay = ""; private static String setWeek = ""; private boolean timecount = true; private StartTime startTime; private MainListener listener; private WelcomeTimeBean welcomeTimeBean = new WelcomeTimeBean(); private Observable<String> badRequestObservable; //收到BAD_REQUUEST报文 private Observable<WSResponseData> setTimeObservable; private Observable<String> netChangeObserver;
从他里面看到了RxBus的身影。且该类组合使用了MainListener的方法,
里面注册了RxBus的消息接收响应。收到订阅的事件后,调用MainListener接口中的方法,去把数据填进去,最终实现了界面上更新的效果。
@Override public void initRxbus(ArrayMap<Object, Observable> observables) { badRequestObservable = RxBus.get().register(CommonConstant.NOTIFY_REQUEST_FAILED, String.class); observables.put(CommonConstant.NOTIFY_REQUEST_FAILED,badRequestObservable); badRequestObservable .subscribe(new Consumer<String>() { @Override public void accept(String arg0) { if (arg0 != null && !arg0.trim().equals("")) { Log.d(TAG, "收到BAD_REQUEST SEQ=" + com.newcapec.commontools.GsonUtil.GsonString(arg0)); // CardConstant.isEnableTheSweep = true; listener.badRequest(arg0); listener.initPayStatus(); } } }); setTimeObservable = RxBus.get().register(CommonConstant.NOTIFY_TIME, WSResponseData.class);//获取系统时间 observables.put(CommonConstant.NOTIFY_TIME,setTimeObservable); setTimeObservable.observeOn(AndroidSchedulers.mainThread(), false, 100).subscribe(new Consumer<WSResponseData>() { @Override public void accept(WSResponseData result) { Log.i(TAG, "===校验时间==="); if (result != null) {
由此可以理出,何为MVVM ? 即 M (model)+ V(视图) + VM (ViewModel)
从上述工程结构上看,model文件夹即充当了M (model)的角色。里面注册了RxBus,获取数据并对收到的事件消息进行响应。调用VM(ViewModel)中的接口方法,完成对界面数据的更新操作。
viewModel文件夹,充当了VM (ViewModel)层
V层呢?这个应该是在 Bean文件夹的MVVM文件夹中定义的Bean,以及在BaseActivity中完成的DataBanding充当了View层。
至此,MVVM 各个层已经介绍完了。
总结下就是 V层完成界面绑定,VM层继承了Ilistener,实现了更新界面的接口,且负责和界面交互的业务逻辑,M层类里面组合使用了这些接口,注册了RxBus事件总线,把数据源和响应的数据传过去。
理清了各个文件夹的功能和MVVM的结构,代码看起来就清楚多了。
拿以上结构举例,如果界面发生了很大变化,有哪些组件是可以复用的?只需改下跟界面绑定的Bean以及Bean和界面的绑定,
model和viewmodel基本是可以复用的。
大致就这些了,不过发现项目里分层也不是很清晰。
以上仅是该工程的分析,并不一定就是完整意义上的MVVM,关于MVVM,不同人有不同的理解。
总而言之,言而总之,谁能把业务和界面分清楚了,做到逻辑清晰,条理清晰,方便复用,方便维护就是最好的。。
知乎上有这个问题的大讨论,https://www.zhihu.com/question/30976423
我觉得虽然业务复杂多变,但是界面可能更复杂多变。界面耦合在业务里,会给业务功能的复用带来很大的麻烦。
既然要努力的把界面和业务逻辑分开,那么,把业务放在model层里,里面不涉及任何界面更新的东西。且留出供viewmodel层调用获取数据的接口 。而viewmodel层也留出 供model业务层涉及显示需求的接口。让model 层可以调用他来做到更新界面。
做个假设,如果界面更改了,增了些控件,减了几个Button,你有哪些地方要改的?
如果换了个项目,业务差不多,但界面无一丝相似之处,你有哪些要改的?
如果,viewmodel层不涉及任何业务,model层不涉及任何界面。那么,需要改动的地方有:Activity和相应的layout,以及layout对应绑定的Bean。还有viewmodel层的负责跟界面交互的地方。业务model层可以全部照搬过来,
model层留出供viewmodel层获取数据的接口,viewmodel层留出供model层调用的显示。两者相互留出彼此使用的接口。
这样,如果调试界面的人员和业务人员分工,那么,只需根据需要,把 model层留出的获取数据的接口用模拟数据实现了,先供测试。最后,由业务人员再把这些接口用真实获取数据的方法给替换掉。。。