本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。
本文的示例代码主要是基于作者的经验来编写的,若你有其他的技巧和方法可以参与进来一起完善这篇文章。文章中大量参考和引用了DBinding权威使用指南的内容,如果你想了解更多建议深入阅读一下DBinding权威使用指南。
说明:下文中vm是view model的缩写
本文固定连接:https://github.com/tianzhijiexian/Android-Best-Practices
一、需求背景
开发者都希望可以更快更简单地编写代码,并且还希望代码的可维护性和健壮性能符合团队的期望。很多初创团队在发展多年后逐渐认识到了早期代码模式的弊端,并且在代码的组织结构上有了很多思考。
在模式方面,2015年大家开始争相讨论mvc,mvp,mvvm,期间谷歌也推出了自家的数据绑定框架databinding,借此来简化代码的编写。在这一片百家争鸣中,开发者十分希望能找到一个满足项目需求并且稳定可靠的框架来简化开发工作。
二、需求
开发者对于一个框架最看重的是下面几点:
- 能加快开发速度,屏蔽底层细节
- 代码可读性好,易维护
- 代码量越少越好,易阅读
- bug少,有不错的健壮性
三、实现
指定明确的分层
如果一个项目有了明确的分层结构,那么代码的可读性和可维护性会上升很多,它也是一个架构的基础。分层良好的的优点有很多,而且即使某天要更换框架,也不会伤筋动骨。
需要格外注意的是:只有当一个项目的成员都能明确项目的层级后才可以谈框架和模式,否则一个框架再优秀也无法在混乱中发挥出优势。
层名 | 内容 |
---|---|
view层 | 具体的view,activity,fragment等,做ui展示、ui逻辑、ui动画 |
vm层 | 具体的视图模型类,是view展示的数据的java映射,能被model层直接操作 |
model层 | 非ui层面的业务逻辑的实现。包含网络请求,数据遍历等操作,是很多具体类的抽象载体 |
DBinding是一个databinding的扩展类,它提供了快速绑定vm和通过vm维持多个页面之间数据同步等功能,并且它还有强大的as插件来做支持,因此本文将选择它作为mvvm框架。
通过数据来更新UI
目前流行的做法都是通过数据来驱动UI,其优点在于方便做单元测试和多人协作,对bug的定位也有比较好的帮助。mvvm是一个抽象的概念,它目前最稳定可靠的实现就是databinding,在用databinding之后,我已经很少到view层定位bug了。databinding的代码由xml代码和java代码构成。
layout:
Activity:
private UserViewModel mUserVm = new UserViewModel();
@Override
protected void onCreate(Bundle savedInstanceState) {
DBinding.bindViewModel(this, R.layout.activity_main, mUserVm);
mUserVm.setName("kale"); // textview中就会自动渲染出文字了
}
layout文件中的vm取名应该和layout文件名字有关联,layout文件的名字也应该和activity的名字有关,这样可以方便定位问题和查找逻辑。layout中vm的参数完全可以模仿之前取id名字的思路,只不过千万不要加view的缩写,出现tv_username或username_tv就闹笑话了。layout文件中强烈不建议写import语句,vm类名强制写全称。至于java代码就十分简单了,没有过多的要求,只要对vm操作即可更新ui。
通过代码模板快速生成layout文件
为了快速产生mvvm的layout文件,我利用了as提供的代码模板功能。
下面就是创建好的代码块:
通过插件自动生成ViewModel
DBinding提供了强大的as插件来生成vm,这样就强制你不能随意修改vm的内容,将问题屏蔽在了vm之外,这样既加快了代码的编写速度又方便定位问题。
目前Dbinding的插件不能也永远不可能支持所有view的属性的绑定,但是你可通过配置的方式来让其支持更多属性,下面会演示如何给SimpleDraweeView
增加的url的属性。
在代码中编写适配器:
public class NetWorkImageViewAdapter {
@BindingAdapter({"url"})
public static void setUrl(SimpleDraweeView view, String url) {
view.setImageURI(url);
}
}
在value/dbinding_config.xml中进行配置:
android.graphics.Bitmap
java.lang.String
这样插件便会知道url对应的类型,然后进行生成对应的vm的field。
欢迎你给DBinding库提交代码来让库原生支持你想要的属性
利用ide来对vm进行重构操作
因为目前as对于layout中的vm的补全和重构的支持力度不足,所以推荐用下列方式进行vm的重构工作。
1.改名和改包名
如果要改vm的包名或改vm的类名的时候,最快捷的方式是进入到这个类的实体中,通过ide的重构工具进行修改。这样所有的改动会自动同步到使用了这个类的xml文件中去。当然,你也可以在这个类被调用的地方通过重构工具进行改名。
2.删除
删除某个vm也是一样的,仍旧是对java类进行操作。删除的时候注意排查下用到的地方,以免出错,这个排查工作真必须是手工做的。
3.给vm中的字段改名
我们先来看下插件会通过我们的xml生成什么东西:
package org.kale.vm;
public class UserviewModel extends BaseviewModel {
private java.lang.CharSequence name;
public final void setName(java.lang.CharSequence name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public final java.lang.CharSequence getName() {
return this.name;
}
}
这里有我们定义的name字段和其get和set方法。如果我们突然想把这个“name”改名为“nickname”,或者是删除这个name字段呢?最好的做法就是直接重构name这个字段。
下面为了演示方便,减少干扰选项,我把name这个过于通用的字母先改成了nickname,现在我将演示如何将nickname改为name。
4.从vm中删除字段
因为as对于databinding的支持力度很低(未来或许就可以通过重构工具来做了),所以在重构字段的时候只能我们自己去排查了。我的排查方案是通过检索当前类使用到的地方,来看下使用当前类的xml中有没有使用过我准备删除的字段,如果有就进行处理,如果没有就直接删除,以此来避免删除后出现程序出错的问题。
禁止在layout中写复杂逻辑
databinding原生提供了在xml中写java语句的能力,也就是它允许你再xml中写逻辑。这点在DBinding中是强烈禁止的,如果你是通过dbinding的插件来生成vm的,那么你会发现你几乎找不到在xml中写java逻辑的需求。
至于这么做的原因是为了方便定位问题,一旦你将逻辑写的四分五裂,那么出现了bug后开发者能否在第一时间知道具体逻辑这个先不谈,就说引起bug的可能性就有多个,试错和排查都会花很多的时间。
如果你的团队协作,你把一些逻辑写到了java中,一些写到了xml中,阅读代码的人必须要能理解这些才能真正的了解你的意图,此外layout文件是具备复用能力的,一旦你要复用layout,那么这些xml中的逻辑便成了其无法复用的根源,因此我强烈禁止在xml中写java逻辑。
在实际使用中我会发现我们经常会根据字段来判断是否要让view显示或隐藏,如果都在java代码中写感觉会比较重一些。于是我尝试在xml中写了判断是否显示的逻辑,后来发现即使layout被复用了,这种逻辑也是必然存在的,即使遇到不存在的情况转为java代码实现也是很简单的。在定位问题方面,如果知道xml中有这个逻辑的话也还好,所以我目前唯一能允许的就是在xml中写控制view是否显示的逻辑代码,其余的逻辑代码一律禁止。如果你也准备这么写,请务必让你的团队接受并了解这种机制,否则会给别人带来困扰的。这里我仍旧是通过代码模板的方式进行快速编写:
利用b代替findViewById
在mvvm时代,我们是否需要id呢?其实,我们仍旧需要id,只是不再需要findViewById了! 这在DBinding的demo中就有这样的体现:
public abstract class BaseActivity extends AppCompatActivity{
protected EventViewModel viewEvents = new EventViewModel();
protected T b;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bindViews();
beforeSetViews();
setViews();
doTransaction();
}
protected Activity getActivity() {
return this;
}
@LayoutRes
protected abstract int getLayoutResId();
protected void bindViews(){
b = DBinding.bind(this, getLayoutResId());
}
protected abstract void beforeSetViews();
protected abstract void setViews();
protected abstract void doTransaction();
}
在子类中,只需要写好泛型就行:
public class MainActivity extends BaseActivity {}
利用ViewEvent类做事件的统一管理
一个页面中会有多个view,Button和EditText肯定是会产生事件的,在mvvm中我采用的是事件设置的代码在xml中,事件处理代码在java中的思路。
viewEvents.setOnClick(v -> {
if (v == b.userInfoInclude.headPicIv) {
// do something
}
});
定位问题的思路是这样的,首先你肯定不会怀疑button不会产生事件(如果真的有,那么你的开发真的开小差了),一般都是按钮被点击后的事件触发的代码产生了问题,所以大多数情况下只需要在触发的那段java代码中下断点就行了。
利用vm做全局的数据同步
两个页面间
vm自身的自动绑定特性会让两个页面共用数据变得十分简单,可以通过viewModel.toSerializable()来将其序列化,然后在接收的地方通过:
NewsviewModel vm = NewsviewModel.toviewModel(getIntent().getSerializableExtra(KEY));
得到它,现在你就可以方便的利用上个页面传来的vm进行layout层面的绑定了。
注意:
虽然这种方式十分简单,但不要滥用,它仅仅针对于两页面有有共同vm的情况,其他情况我还是推荐通过回调、广播、事件总线等方式去做。要记得vm虽好,但它不是万能的。
多个页面间
我们经常会有一些全局的数据,比如红点消息和用户信息,这些数据我们通常会产生一个静态的对象进行存储,以用户信息举例,我们完全可以让所有用到当前用户信息的页面用同一个vm,这样就再也不用考虑多个页面用户信息不同步的情况了。至于什么东西可以用这种方式做全局同步,什么不可以,这个就只能看业务和团队成员的把控能力了。
通过注册数据监听来解耦view层
在mvvm中我们应该把所有数据同步的事情交给框架,而不是自己去维护。将view层的逻辑(如:动画,控件A文字的改变引起的控件B改变等)独立写出,在model中独立写出数据对vm产生影响的逻辑,下面举个例子:
/**
* 数据改变后ui会做一些改变。
* 应该利用对vm的字段监听的方式做处理,不应该在数据改变时,通过开发者做ui层面的更新。
*
* @param bind 为什么不是单一监听器,而是观察者模式?
* 因为会有多个东西对同一个数据进行监听,如果是单一的就没办法实现这个功能。
*/
public void notifyData(final NewsItemBinding bind) {
mviewModel.addOnPropertyChangedCallback((sender, propertyId)-> {
// 监听title的改变,然后设置文字
if (propertyId == kale.db.BR.title) {
// do change view
}
}
});
}
在数据来的时候,数据仅仅对vm进行绑定,不用考虑ui层面的逻辑:
///////////////////////////////////////////////////////////////////////////
// 这里就仅仅做数据和ui的绑定工作了,不用想ui层面的任何逻辑
///////////////////////////////////////////////////////////////////////////
/**
* 将ViewModel和model的数据进行同步
* model模型可能很复杂,但viewModel的模型很简单,这里就是做二者的转换。
*/
@Override
public void handleData(NewsInfo data, int pos) {
mviewModel.setTitle(String.format(data.title,"kale"));
}
禁止一切容易出错的操作
强类型语言和弱类型语言的一个差异(仅仅是差异)就是在于IDE可以帮你做很多限制,databinding本身是相当灵活的,支持双向绑定,支持xml中写逻辑等操作,但是我这里利用插件或者是其他的方式强烈禁止在xml中写方法和特殊逻辑,对于import我只允许了View这一个类的import。对于双向绑定,我建议你在编码的时候就应该有所警惕,最好能有注释,方便你的同伴进行定位问题。
如果你是一人开发一个不需要维护的应用,那么xml中随便你怎么写,但如果你是团队开发,你会发现那些在xml中的逻辑很可能是团队合作的灾难。当然了,如果你已经通过某种文档或者是其他的标准化方式来限制和规定xml中的逻辑格式,那么我倒是觉得是可行的。
自由是在限制之中的,如果没有限制那么就没有社会。
四、总结
我经历了项目从mvc到mvp,然后变成mvvm,最后到mvpvm的各个阶段,在每个阶段中我也花了大量的时间去发现问题解决问题,为后续的扩展和灵活性做了很多的工作。在做这些事情的时候我渐渐发现,无论你采用什么模式,你都必须有明确的分层的概念,其实大到分层小到单一职责概念,都是在提升代码可维护性。在现在这个时期,我的建议是中小型公司可以放心尝试databinding,大型公司的话因为体量和人员的问题很难会改变模式。当然了,如果目前你的代码本身就有很好的可维护性,我也不建议因为技术的新颖而动项目,因为我们的目的不是尝鲜和炫技,而是为了解决问题!
话说,你写了多少年的findViewById?