Android应用架构设计—MVP模式
一、前期基础知识储备
随着Android这一移动开发技术的成熟,Android应用架构设计得到了越来越多企业以及开发者的重视,并因此衍生出了Android架构师这一职位。好的架构设计会带来很多好处,比如更易维护、扩展,等等;而差的架构设计或者没有架构设计,则会使得应用在后期的维护和扩展中产生很多严重的问题。目前Android的框架模式主要有MVC、MVP和MVVM,虽说最近流行MVP和MVVM,但是MVC也没有过时之说,我们主要还是根据业务选择来选择合适的架构。
MVP模式
MVP(Model-View-Presenter)是MVC的演化版本,MVP的角色定义如下:
- Model:主要提供数据的存取功能。Presenter需要通过Model层来存储、获取数据。
- View:负责处理用户事件和视图部分的展示。在Android中,它可能是Activity、Fragment类或者某个View控件。
- Presenter:作为View和Model之间沟通的桥梁,它从Model层检索数据后返回给View层,使得View和Model之间没有耦合。
在MVP里,Presenter完全将Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时可以保持Presenter的不变,这点符合面向接口编程的特点。View只应该有简单的Set/Get方法,以及用户输入和设置界面显示的内容,除此之外就不应该有更多的内容,决不允许View直接访问Model,这就是其与MVC的很大不同之处。
- View向Presenter暴露更新UI的方法 —— 视图逻辑;
- Presenter向View暴露执行一些特定业务的方法,比如初始化页面,提交等 —— 业务逻辑。
如果使用MVC模式,一个主流项目的Activity里就会有很多的逻辑判断,那Activity代码的行数就很大了,即便写了注释,维护起来也是比较麻烦的。而且Activity本来是用来呈现界面的一个组件,而在Android的应用开发中又无不肩负着界面跳转和数据访问的职责,Activity到底是View还是Controller还是二者兼具?
- Presenter,处于View和Model之间,控制View的行为同时调度业务逻辑层的行为。这样View和Model不用直接交互;
- View,Activity的视图职责变得更为纯粹,定义一个View接口,让具体的Activity来实现,然后Presenter持有这个View的引用从而能调用View的行为;
- Model,只负责应用数据相关的业务逻辑,例如数据请求和数据处理。
总结一下:从MVC到MVP的一个转变,就是减少了Activity的职责,减轻了它的负担,将逻辑处理代码提取到了Presenter中进行处理,降低了其耦合度。
二、上代码,具体实现
举例实现MVPDemo,这个例子用来访问淘宝IP库。访问一个IP地址,并在界面上显示该IP所对应的国家、地区和城市。在这个例子中要访问网络,为了实现方便,这里采用了OkHttp的封装库OkHttpFinal。
1)添加OkHttpFinal的依赖:
compile 'cn.finalteam:okhttpfinal:2.0.7
为了能使用OkHttpFinal,我们需要在自定义的Application中实现如下初始化代码:
public class MvpApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
OkHttpFinalConfiguration.Builder builder = new OkHttpFinalConfiguration.Builder();
OkHttpFinal.getInstance().init(builder.build());
}
}
2)实现Model,创建实体类:
① 首先创建Model实体IpInfo:
public class IpInfo {
private int code;
private IpData data;
public int getCode() {
return code;
}
... ...
}
② IpData的部分代码如下所示:
public class IpData {
private String country;
private String country_id;
private String area;
... ...
public String getCountry() {
return country;
}
... ...
}
③ 随后自定义获取网络数据的接口类:
public interface NetTask<T> {
void execute(T data , LoadTasksCallBack callBack);
}
④这里再写入一个回调监听接口LoadTasksCallBack,它定义了网络访问回调的各种状态:
public interface LoadTasksCallBack<T> {
void onSuccess(T t);
void onStart();
void onFailed();
void onFinish();
}
⑤ 接下来我们编写NetTask的实现类,以获取数据,如下:
public class IpInfoTask implements NetTask<String> {
private static IpInfoTask INSTANCE = null;
private static final String HOST = "http://ip.taobao.com/service/getIpInfo.php";
private LoadTasksCallBack loadTasksCallBack;
private IpInfoTask() {
}
public static IpInfoTask getInstance() {
if (INSTANCE == null) {
INSTANCE = new IpInfoTask();
}
return INSTANCE;
}
@Override
public void execute(String ip, final LoadTasksCallBack loadTasksCallBack) {
RequestParams requestParams = new RequestParams();
requestParams.addFormDataPart("ip", ip);
HttpRequest.post(HOST, requestParams, new BaseHttpRequestCallback<IpInfo>() {
@Override
public void onStart() {
super.onStart();
loadTasksCallBack.onStart();
}
@Override
protected void onSuccess(IpInfo ipInfo) {
super.onSuccess(ipInfo);
loadTasksCallBack.onSuccess(ipInfo);
}
@Override
public void onFinish() {
super.onFinish();
loadTasksCallBack.onFinish();
}
@Override
public void onFailure(int errorCode, String msg) {
super.onFailure(errorCode, msg);
loadTasksCallBack.onFailed();
}
});
}
}
IpInfoTask是一个单例类,在execute方法中通过OkHttpFinal来获取数据,同时在OkHttpFinal的回调的函数中调用自己定义的回调函数loadTasksCallBack。
2)实现Presenter:
① 首先定义一个契约接口IpInfoContract,契约接口主要用来存放相同业务的Presenter和View的接口,便于查找维护,如下:
Contract,契约,将Model、View、Presenter进行约束管理,方便后期的查找、维护。
public interface IpInfoContract {
/**
* Presenter 控制View的行为,同时调度业务逻辑层的行为
*
* 以下接口方法:
* 实现:在IpInfoPresenter中;
* 调用:在Activity/Fragement中;
*/
interface Presenter {
void getIpInfo(String ip);
}
/**
* View接口
* 只负责显示视图,我们不希望Activity和model有直接的联系,我们可以定义一个View接口,
* 在这个View接口中定义视图行为的抽象,让具体的Activity来实现。
* 然后Presenter持有这个View的引用从而能调用View的行为。
*
* 以下接口方法:
* 实现:在Activity/Fragement中;
* 调用:在IpInfoPresenter中;
*/
interface View extends BaseView<Presenter> {
void setIpInfo(IpInfo ipInfo);
void showLoading();
void hideLoading();
void showError();
boolean isActive();
}
}
上述代码中,可以知道Presenter接口定义了获取数据的方法,而View定义了与外界交互的方法。其中,isActive方法用于判断Fragment是否添加到了Activity中。另外,View接口继承自BaseView接口。
② BaseView接口:
/**
* BaseView中有一个setPresenter()方法,通过该方法,在P的构造函数中将V关联起来。
*/
public interface BaseView<T> {
void setPresenter(T presenter);
}
BaseView接口的目的就是给View绑定Presenter。
③ 接着实现Presenter接口:
public class IpInfoPresenter implements IpInfoContract.Presenter, LoadTasksCallBack<IpInfo> {
private NetTask netTask;
private IpInfoContract.View addTaskView;
public IpInfoPresenter(IpInfoContract.View addTaskView, NetTask netTask) {
this.netTask = netTask;
this.addTaskView=addTaskView;
}
//请求网络的execute方法 在Presenter的接口方法中进行回调 — 业务层面的逻辑
@Override
public void getIpInfo(String ip) {
// 1
netTask.execute(ip,this);
}
//View接口方法的调用 在网络请求回调的CallBack方法中 — 视图层面的逻辑
@Override
public void onSuccess(IpInfo ipInfo) {
if(addTaskView.isActive()){
addTaskView.setIpInfo(ipInfo);
}
}
@Override
public void onStart() {
if(addTaskView.isActive()){
addTaskView.showLoading();
}
}
@Override
public void onFailed() {
if(addTaskView.isActive()){
addTaskView.showError();
addTaskView.hideLoading();
}
}
@Override
public void onFinish() {
if(addTaskView.isActive()){
addTaskView.hideLoading();
}
}
}
IpInfoPresenter中含有NetTask和IpInfoContract.View的实例对象(接口的实例对象),并且实现了LoadTasksCallBack接口。在上面的注释1处,将自身传入NetTask的execute方法中来获取数据,并回调给IpInfoPresenter,最后通过addTaskView来和View进行交互,并且更改界面。Presenter就是一个中间人的角色,其通过NetTask,也就是Model层来获得和保存数据,然后再通过View更新界面,这期间通过定义接口使得View和Model没有任何交互。
3)实现View:
在上面的契约接口IpInfoContract中,我们已经定义了View接口,实现它的是IpInfoFragment。如下:
public class IpInfoFragment extends Fragment implements IpInfoContract.View {
private TextView tv_country;
private TextView tv_area;
private TextView tv_city;
private Button bt_ipinfo;
private Dialog mDialog;
private IpInfoContract.Presenter mPresenter;
public static IpInfoFragment newInstance() {
return new IpInfoFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_ipinfo, container, false);
tv_country= (TextView) root.findViewById(R.id.tv_country);
tv_area= (TextView) root.findViewById(R.id.tv_area);
tv_city= (TextView) root.findViewById(R.id.tv_city);
bt_ipinfo= (Button) root.findViewById(R.id.bt_ipinfo);
return root;
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mDialog=new ProgressDialog(getActivity());
mDialog.setTitle("获取数据中");
bt_ipinfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mPresenter.getIpInfo("39.155.184.147"); // 2
}
});
}
@Override
public void setPresenter(IpInfoContract.Presenter presenter) {
mPresenter=presenter; // 1
}
@Override
public void setIpInfo(IpInfo ipInfo) {
if(ipInfo!=null&&ipInfo.getData()!=null){
IpData ipData=ipInfo.getData();
tv_country.setText(ipData.getCountry());
tv_area.setText(ipData.getArea());
tv_city.setText(ipData.getCity());
}
}
@Override
public void showLoading() {
mDialog.show();
}
@Override
public void hideLoading() {
if(mDialog.isShowing()) {
mDialog.dismiss();
}
}
@Override
public void showError() {
Toast.makeText(getActivity().getApplicationContext(),"网络出错",Toast.LENGTH_SHORT).show();
}
@Override
public boolean isActive() {
return isAdded();
}
}
在上面的代码注释1处,通过实现setPresenter方法来注入IpInfoPresenter。
在注释2处,调用IpInfoPresenter的getIpInfo方法来获取IP地址信息。另外,IpInfoFragment实现了View接口,用来接收IpInfoPresenter的回调并更新界面。
那么IpInfoFragment是在哪里调用setPresenter来注入IpInfoPresenter的呢?答案在Activity中。如下:
public class IpInfoActivity extends AppCompatActivity {
private IpInfoPresenter ipInfoPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ipinfo);
IpInfoFragment ipInfoFragment = (IpInfoFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (ipInfoFragment == null) {
ipInfoFragment = IpInfoFragment.newInstance(); // 1
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
ipInfoFragment, R.id.contentFrame); // 2
}
IpInfoTask ipInfoTask = IpInfoTask.getInstance();
ipInfoPresenter = new IpInfoPresenter(ipInfoFragment, ipInfoTask);
ipInfoFragment.setPresenter(ipInfoPresenter); // 3
}
}
在这个例子中Activity并不是作为View层,而是作为View、Model和Presenter三层的纽带。
在上面代码注释1处,新建IpInfoFragment,接着通过注释2处的代码来将IpInfoFragment添加到Activity中。紧接着创建IpInfoTask,并将它和IpInfoFragment作为参数传入IpInfoPresenter,并在注释3处将IpInfoPresenter注入到IpInfoFragment中。可以看到,IpInfoPresenter与IpInfoFragment是相互注入的。注释2处的代码如下:
public class ActivityUtils {
public static void addFragmentToActivity(FragmentManager fragmentManager,
Fragment fragment, int frameId) {
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(frameId, fragment);
transaction.commit();
}
}
上面的代码负责提交事务,将Fragment添加到Activity中。
整个项目的结构如图:
如图所示,View和Model之间没有联系,View与Presenter之间通过接口进行交互,并在Activity中进行相互注入。Model层的NetTask在Activity中注入Presenter,并等待Presenter调用。
其实还有很多种方式实现MVP,这里是基础的一种,感兴趣的可以查看谷歌官方的MVP示例。