一、前期基础知识储备
随着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架构的实现。其实真正理解起来,可以做如下概括:
① View层和Presenter层双向绑定,体现在代码中各自持有一个对方的引用;同时Presenter层持有Model层引用;
② Presenter层持有View的引用,能够通知View层做相应的更新,也有了Model层的引用,可以从Model层获取想要的数据;
③ 每一层定义一套接口,然后面向接口编程 —— 实现接口和接口方法的地方不能调用接口方法;调用接口和接口方法的地方不能实现接口;持有引用,就是持有了接口实例,就可以调用接口方法;
④ 接口方法并不是独自生效,而是和其他接口接口方法共同实现(即一个接口方法底层调用的是另一个接口方法的实现);
⑤ 注意注入,通过注入,实际上就是注入了接口实例,这样就可以直接调用接口的方法。
三,谷歌MVP项目简化,分析实现
其实还有很多种方式实现MVP,这里是基础的一种,感兴趣的可以查看谷歌官方的MVP示例。
2021/05/04补充:谷歌MVPMVP实现的简化版本:
简化后项目结构如上,项目地址:https://gitlab.com/chinstyle/mvp_google;简化后,使用一个典型的MVP项目需要做如下几个步骤:
1)构造Presenter和View接口的基类
public interface BaseView<T> {
void setPresenter(T presenter); // 开始执行注入Presenter的操作
}
public interface BasePresenter {
void start(); // 开始执行请求数据的操作
}
2)根据业务模块展开,定义不同的协议类,对子view接口和Presenter接口进行管理
public interface UserInfoContract {
interface View extends BaseView<Presenter>{
void showLoading();//展示加载框
void dismissLoading();//取消加载框展示
//将网络请求得到的用户信息回调
void showUserInfo(UserInfoModel userInfoModel);
String loadUserId();//假设接口请求需要一个userId
}
interface Presenter extends BasePresenter {
void loadUserInfo();
}
}
使用MVP模式后,几乎每一个Activity或者Fragment(除非特别简单的页面逻辑)都会有一个对应的View接口和Presenter接口,然后最好定义一个契约类进行管理。如果是一个大项目话,就会使用基类+泛型类的方式实现,这样可以减少契约类,减少管控的成本。
3)然后编写View接口实现类 [XXXActivity/XXXFragment]
Activity/Fragment之所以被称之为View层,就是因为其一定会去实现View接口,以执行对应的UI层面的逻辑;然后最终会实现View基类接口中的注入Presenter的方法;一般会在界面初始化的时候,执行P层请求数据的方法。
4)编写Presenter接口的实现类
在这里进行model层和view层的交互。这里model层指的就是网络请求对数据的获取,而得到数据后是presenter去决定让view显示什么。它去做逻辑处理。
自定义Presenter类去实现Presenter接口;由于View层需要和Presenter层进行交互,P层控制V层的展示,所以需要注入V接口,这里采用构造方法注入的方式进行注入;然后实现P层接口的接口方法,开始请求数据,然后在P层接口方法的回调中,去操作V层接口的方法,执行对应的UI逻辑。
5)关于Model层
M层主要指的就是对数据的处理,比如数据的获取、存储、数据状态变化都是model层的任务,比如Javabean、(项目封装的底层网络库、数据库),持久化数据增删改查等任务;要知道model不仅仅只是实体类的集合,同时还包含关于数据的各种处理操作。
网络请求的发起是在P层,网络请求的回调根据成功与失败等做逻辑处理也是在P层,
但真正去请求获取数据(比如okhttp、或是自己封装的HttpUtil)的复杂任务是在M层处理。
最后,当然不能MVP模式当作万能解药,它也有自己的缺点,由于是面向接口编程,它会增加更多个接口类(一个Activity/Fragment会需要一个V接口,一个P接口,一个契约类,还有对应M层的实现)。如果是一些业务逻辑比较简单的页面,使用MVP模式反而会使逻辑更加复杂,增加代码量,有画蛇添足的嫌疑,所以什么时候使用MVP模式,要根据业务需求而定。
四,Android Studio中使用Git进行代码管理,并将代码托管到Gitlab上
2021/05/04 补充 关于Android Studio上使用Git版本管理工具,提交代码到Gitlab进行管理的操作:
《详细讲解Android Studio中使用Git——结合GitLab》
文章内关于代码分支的处理,在实际开发中很有用。