在开始讲解各种架构模式时,我们先来看下没有经过设计的代码是如何编写的。为了不分散重点,笔者举的例子会比较简单,初始时从数据库缓存中获取用户信息展示到界面上,点击刷新按钮可以从服务器上拉取最新的用户信息并进行展示。
由于从数据库和服务器上获取数据都属于更底层的逻辑,因此这两个操作一开始就会进行封装,不会列入讨论范围,并且为了使程序更加简单,这两个操作都是使用的测试代码进行模拟。
User.java
// User实体类,再没有封装意识的人,实体类总会有一个吧
public class User {
public String name;
public int age;
}
DbUtils.java
// 数据库工具
public class DbUtils {
// 查询数据库记录并返回cursor,这里使用测试代码直接返回null
public static Cursor query(String sql) {
return null;
}
// 更新数据库记录,这里使用测试代码不进行任何实际处理
public static void update(String sql) {
}
}
HttpUtils.java
// http工具
public class HttpUtils {
private static Handler sHandler = new Handler(Looper.getMainLooper());
public interface ResponseCallback {
void onResponseSuccessed(String json);
void onResponseFailed(int reason);
}
// 发起http请求,这里使用模拟的数据,并有一定机率请求失败
public static void request(Map params, final ResponseCallback callback) {
sHandler.postDelayed(new Runnable() {
@Override
public void run() {
int value = new Random(System.currentTimeMillis()).nextInt(5);
if(callback != null) {
if(value == 2) {
callback.onResponseFailed(1);
} else {
callback.onResponseSuccessed("{\"name\": \"纯爷们\", \"age\": 20}");
}
}
}
}, 500);
}
}
activity_user.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tv_name"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:id="@+id/tv_age"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:text="刷新"
android:id="@+id/btn_refresh"/>
</LinearLayout>
UserActivity.java
public class UserActivity extends Activity {
private TextView mNameView;
private TextView mAgeView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mNameView = (TextView)findViewById(R.id.tv_name);
mAgeView = (TextView)findViewById(R.id.tv_age);
// 加载缓存的用户数据并展示
User user = loadUser();
if(user != null) {
mNameView.setText("昵称:" + user.name);
mAgeView.setText("年龄:" + user.age);
}
findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从服务器拉取最新用户数据并显示
refresh();
}
});
}
private User loadUser() {
// 这里本应从cursor中获取数据,但为求程序尽量简单,我们直接使用模拟数据。之所以要加入query这段代码,是为了尽可能模拟真实的流程。
Cursor cursor = DbUtils.query(null);
if(cursor != null) {
try {
} catch (Exception e) {
} finally {
cursor.close();
}
}
User user = new User();
user.name = "小虾米";
user.age = 19;
return user;
}
private void refresh() {
HttpUtils.request(null, new HttpUtils.ResponseCallback() {
@Override
public void onResponseSuccessed(String json) {
try {
User user = new Gson().fromJson(json, User.class);
// 用户信息更新了,要同步更新数据库中的记录
String updateSql = null;
DbUtils.update(updateSql);
mNameView.setText("昵称:" + user.name);
mAgeView.setText("年龄:" + user.age);
} catch (Exception e) {
}
}
@Override
public void onResponseFailed(int reason) {
Toast.makeText(UserActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
}
});
}
}
点击刷新
前显示如下界面
点击
刷新
后显示如下界面上面的例子请读者务必记劳,后续讲到的几种架构模式全部使用的都是这个例子。
上述例子一个非常突出的问题是,用户信息可能在多个界面上都需要显示,而在这些界面上,从数据库和服务器上获取用户信息的流程都要写一遍,重复编写不仅容易出错也不容易维护。解决该问题的方法就是封装一个可复用的Model,MXX模式也由此产生。
MVC
MVC由Model、View、Controller组成,Android提供的xml布局是View层的主要部分,View本身已经是相对比较独立的了,因此MVC中主要考虑的就是Model的设计。我们来看下MVC中各层在Android中的主要应用:
- Model:表示模型层,模型的一个核心特点就是可复用。包含数据业务实体(entity/bean)本身,以及围绕该实体进行的业务操作(本地或远程增删改查操作)。即Model层主要针对数据,包含数据实体和数据访问,如果非要以模型来称呼和理解的话,前者为数据模型,后者为业务模型。
- View:表示视图层,负责界面数据的展示,以及响应用户操作。
- Controller:表示控制层,负责逻辑处理,由其连接Model和View。Controller通过Model获取数据并传递给View进行展示;通过响应View传递过来的用户事件调用Model的接口进行业务处理。
后续内容都使用简称,M代表Model,V代表View,C代表Controller。
其中,V和C一般又统称为UI层,由于Android已经提供了xml布局,因此在Android中V和C并不需要刻意区分,可以统一以UI层来理解,UI层的核心代码包含xml和Activity(或Fragment,或另外封装的控制器),后者既扮演着部分V的角色,又扮演着全部的C角色。我们来看下使用MVC重构后的例子,增加了UserBusiness
类,修改了UserActivity
的代码,以下只贴出更新的部分代码,其余代码请参考之前的例子。
UserBusiness.java
public class UserBusiness {
private static final UserBusiness INSTANCE = new UserBusiness();
private List<UserListener> mListeners = new LinkedList<>();
public static UserBusiness get() {
return INSTANCE;
}
public void addListener(UserListener listener) {
if(listener == null) {
return;
}
synchronized (mListeners) {
if(!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
}
public void removeListener(UserListener listener) {
if(listener == null) {
return;
}
synchronized (mListeners) {
mListeners.remove(listener);
}
}
public User getUser() {
Cursor cursor = DbUtils.query(null);
if(cursor != null) {
try {
} catch (Exception e) {
} finally {
cursor.close();
}
}
User user = new User();
user.name = "小虾米";
user.age = 19;
return user;
}
public void requestUser() {
HttpUtils.request(null, new HttpUtils.ResponseCallback() {
@Override
public void onResponseSuccessed(String json) {
User user = null;
try {
user = new Gson().fromJson(json, User.class);
} catch (Exception e) {
}
if(user != null) {
String updateSql = null;
DbUtils.update(updateSql);
notifyRequestUser(0, user);
} else {
notifyRequestUser(1, null);
}
}
@Override
public void onResponseFailed(int reason) {
notifyRequestUser(reason, null);
}
});
}
private void notifyRequestUser(int code, User user) {
List<UserListener> listeners = new LinkedList<>();
synchronized (mListeners) {
listeners.addAll(mListeners);
}
for(UserListener listener : listeners) {
listener.onRequestUserResult(code, user);
}
}
public interface UserListener {
void onRequestUserResult(int code, User user);
}
}
UserActivity.java
public class UserActivity extends Activity implements UserBusiness.UserListener {
private TextView mNameView;
private TextView mAgeView;
private UserBusiness mUserBusiness = UserBusiness.get();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mNameView = (TextView)findViewById(R.id.tv_name);
mAgeView = (TextView)findViewById(R.id.tv_age);
// 加载缓存的用户数据并展示
User user = mUserBusiness.getUser();
if(user != null) {
mNameView.setText("昵称:" + user.name);
mAgeView.setText("年龄:" + user.age);
}
findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从服务器拉取最新用户数据并显示
mUserBusiness.requestUser();
}
});
mUserBusiness.addListener(this);
}
@Override
protected void onDestroy() {
mUserBusiness.removeListener(this);
super.onDestroy();
}
@Override
public void onRequestUserResult(int code, User user) {
if(code == 0) {
mNameView.setText("昵称:" + user.name);
mAgeView.setText("年龄:" + user.age);
} else {
Toast.makeText(UserActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
}
}
}
重构后的代码有如下优点:
- Activity的代码变得简单和整洁了,Activity现在只需要处理控制逻辑(UI逻辑),以及作为V和M通信的桥梁。
- 业务代码封装在
UserBusiness
中,一是隐藏了数据操作(业务流程)的具体细节,使得UI层在访问时更简单了;二是可以复用,任何模块都可以轻松访问,且可以通过在UserBusiness
中注册一个监听器来监听用户业务的相关事件。
但MVC仍具有以下缺点:
- V不可复用,然而在Android中V复用没有意义,需要复用的话完全可以封装可复用的控件,然后V组装这些控件。
- C不可复用。
- V和C之间还存在部分耦合,因此除了M外V和C都无法进行单元测试。
为了解决以上缺点,便有了MVP。
MVP
MVP由Model、View和Presenter组成,M和V就不再重复解释了,P和C一样,承担着控制层的责任。MVP相比MVC作了如下改进(或者说变化,是否改进视情况而定):
- P可复用,这意味着不能再使用Activity(或...)作为P了,很简单,Activity不能复用(使用继承达到复用的场景不在这讨论范围之内)。由此,Activity从控制层的角色转向了视图层,即在MVP中V由xml和Activity组成。
也可以另外抽离一个V,然后将Activity作为创建V和P的管理器,并负责将V和P进行绑定。但不建议采用这种方式,额外增加了代码,并且也没带来多少益处,除非想要复用V或者界面异常复杂而拆分了多个V和P。
- MVP三者皆可以独立完成单元测试,为了达到这个目的,P和V需要做到完全解耦,解耦一般使用接口。
我们来看下使用MVP重构过的代码,在mvc的基础上主要是改动了UserActivity.java
,然后增加了几个类。
PresenterContext.java
public interface PresenterContext {
Activity getActivity();
}
UserPresenter.java
public interface UserPresenter {
void onRefresh();
void onInited();
void onDestroyed();
}
UserPresenterImpl.java
public class UserPresenterImpl implements UserPresenter, UserBusiness.UserListener {
private PresenterContext mContext;
private UserView mView;
private UserBusiness mUserBusiness = UserBusiness.get();
public UserPresenterImpl(PresenterContext context, UserView view) {
mContext = context;
mView = view;
}
@Override
public void onRefresh() {
mUserBusiness.requestUser();
}
@Override
public void onInited() {
mUserBusiness.addListener(this);
User user = mUserBusiness.getUser();
if(user != null) {
mView.updateName(user.name);
mView.updateAge(user.age);
}
}
@Override
public void onDestroyed() {
mUserBusiness.removeListener(this);
}
@Override
public void onRequestUserResult(int code, User user) {
if(code == 0) {
mView.updateName(user.name);
mView.updateAge(user.age);
} else {
Toast.makeText(mContext.getActivity(), "刷新失败", Toast.LENGTH_SHORT).show();
}
}
}
UserView.java
public interface UserView {
void updateName(String name);
void updateAge(int age);
}
UserActivity.java
public class UserActivity extends Activity implements UserView, PresenterContext {
private TextView mNameView;
private TextView mAgeView;
private UserPresenter mPresenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mNameView = (TextView)findViewById(R.id.tv_name);
mAgeView = (TextView)findViewById(R.id.tv_age);
mPresenter = new UserPresenterImpl(this, this);
findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.onRefresh();
}
});
mPresenter.onInited();
}
@Override
protected void onDestroy() {
mPresenter.onDestroyed();
super.onDestroy();
}
@Override
public void updateName(String name) {
mNameView.setText("昵称:" + name);
}
@Override
public void updateAge(int age) {
mAgeView.setText("年龄:" + age);
}
@Override
public Activity getActivity() {
return this;
}
}
我们来讲解下:
- 由于Presenter不像Activity一样,它没有上下文信息,因此增加了
PresenterContext
类作为Presenter的上下文信息。这里
PresenterContext
只是用来获取Activity,是因为示例是力求简单,实际项目中它可以获取的信息会更多。 -
UserActivity
的代码被拆分成了两部分,一部分仍然在UserActivity
中,作为View的代码,一部分抽离到UserPresenterImpl
中,作为Presenter的代码。至此,将视图和控制层的代码完全分离了。 - 新建了
UserView
和UserPresenter
两个接口用来表示V和P,V的实现方UserActivity
持有UserPresenter
接口而非具体的实现类,P的实现方持有UserView
接口而非具体的实现类,从而达到V和P解耦。需要对P进行单元测试时,只需要创建一个类简单地实现
UserView
的类,然后和UserPresenterImpl
绑定即可;需要对V进行单元测试时,只需要创建一个简单实现UserPresenter
的类,然后在UserActivity
中将构建P的那行代码修改下即可。
MVP相对MVC具有P复用及方便做单元测试的优点,然而,在实际Android项目中,P复用的场景基本不存在,且多数公司并没有做单元测试。因此,多数情况下,MVC可能比MVP更适合Android项目,毕竟MVP多引入了不入类和代码,且带来解耦的同时也使得代码更加“绕”。
MVVM
不管是MVC还是MVP都存在几下问题:
- 在每个界面都要编写不少的
findViewById
、setOnClickListener
之类的代码。 - 数据更新后,要手动调用
setText
之类的代码刷新视图。
以上几点都不是什么大问题,编写这些代码也不会轻易出错,但总有达人追求极致,MVVM便由此产生了。Android解决第问题1的方法是在xml中直接嵌入代码(类似JSX的写法),解决问题2的方法是提供了DataBinding
方案绑定视图和数据。MVVM真正地将V层完全地体现在xml上,M层还是老样子(模式怎么变它都不变),VM(ViewModel)用来代替C和P,以MVC作为改造,VM包括Activity
和DataBinding
(自动生成)。
本文主要讲解几种架构模式的应用场景和区别,因此不会过多讲解MVVM在Android中如何使用,没接触过MVVM的建议先看下这篇入门文章或查阅官方文档。
使用DataBinding
需要在build.gradle中加入如下代码:
android {
dataBinding {
enabled = true
}
}
配置了之后build时会下载DataBinding
的依赖包以及自动生成部分代码,自动生成的代码后续遇到时会提到。
接着我们来看下在MVC的基础上变更后的MVVM代码。
activity_user.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.sean.mvvm.model.entity.User" />
<variable
name="host"
type="com.sean.mvvm.UserActivity"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text='@{"昵称:" + user.name}' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text='@{"年龄:" + String.valueOf(user.age)}'/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:text="刷新"
android:onClick="@{host.onRefresh}"/>
</LinearLayout>
</layout>
User.java
public class User extends BaseObservable {
@Bindable
public String name;
@Bindable
public int age;
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
}
UserActivity.java
public class UserActivity extends Activity implements UserBusiness.UserListener {
private User mUser;
private UserBusiness mUserBusiness = UserBusiness.get();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
mUser = mUserBusiness.getUser();
if(mUser == null) {
mUser = new User();
}
binding.setUser(mUser);
binding.setHost(this);
mUserBusiness.addListener(this);
}
@Override
protected void onDestroy() {
mUserBusiness.removeListener(this);
super.onDestroy();
}
@Override
public void onRequestUserResult(int code, User user) {
if(code == 0) {
mUser.setName(user.name);
mUser.setAge(user.age);
} else {
Toast.makeText(UserActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
}
}
public void onRefresh(View v) {
mUserBusiness.requestUser();
}
}
分析一下:
- xml布局的顶部标签变成了
layout
,data
标签存储非布局相关代码,variable
定义变量。 - 编译时会根据xml名称和内容自动生成binding类,如例子中的
ActivityUserBinding
,variable
定义的变量在binding中都有对应的set、get方法。 - 数据改变时要做到自动刷新UI,实体类必须继承
BaseObservable
,且需要自动触发的字段必须以Bindable
注解,并在字段值变化时调用notifyPropertyChanged
方法。
PS:强烈建议至少熟读一个自动生成的binding类,绑定的所有原理都在这里,代码很容易理解,也不需要去网络上寻求答案。
上面的例子实现了数据的单向绑定(数据更新触发UI更新)和事件的绑定,而Android是支持数据双向绑定的,现在来看下当UI更新时如何触发数据的更新。使用方式其实很简单,在xml中小小修改下就行:
android:text='@={user.name}'
注意到@
后面多了个=
,同时昵称:
去掉了,=
表示数据双向绑定,但当=
存在时,右侧的表达式只能是个变量,因此昵称:
只能去掉了。当xml改成这样后,TextView的文本发生变化了,user.name
的值也会随之更新。
总结下MVVM的优缺点:
- 新型xml更加成熟,可以独立支撑View层。然而,这可能也是缺点,毕竟这种xml编程方式和Android传统的方式差异较大。
如果不想改变xml的编写方式,又希望使用MVVM,那么可以仿照自动生成的binding类自己编写一个ViewModel,但是有没有这个必要呢。。。
- 只关心数据变化,而不需要关注视图的刷新,刷新由自动生成的binding处理了。
- 数据双向绑定是个伪命题,实际上并没有完全做到自动化,还是需要手动编写额外的代码,并且也有条件限制。
- 在非主module(一般为app)中无法编译新型xml(这个也可能是笔者使用不当,有待确认)。
综上,MVVM相比MVC、MVP并没有多大优势,但可以通知配置减少一些重复的逻辑代码。使用哪种模式根据实际情况而定,没有谁比谁更好。