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架构的实现。其实真正理解起来,可以做如下概括:

View层和Presenter层双向绑定,体现在代码中各自持有一个对方的引用;同时Presenter层持有Model层引用

Presenter层持有View的引用,能够通知View层做相应的更新,也有了Model层的引用,可以从Model层获取想要的数据

每一层定义一套接口,然后面向接口编程 —— 实现接口和接口方法的地方不能调用接口方法;调用接口和接口方法的地方不能实现接口;持有引用,就是持有了接口实例,就可以调用接口方法

④ 接口方法并不是独自生效,而是和其他接口接口方法共同实现(即一个接口方法底层调用的是另一个接口方法的实现);

⑤ 注意注入,通过注入,实际上就是注入了接口实例,这样就可以直接调用接口的方法。

 

三,谷歌MVP项目简化,分析实现

其实还有很多种方式实现MVP,这里是基础的一种,感兴趣的可以查看谷歌官方的MVP示例。

android-architecture

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

文章内关于代码分支的处理,在实际开发中很有用。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值