Android之MVP架构

一、Android MVP初探

最近Android MVP非常火,特地研究了一翻,至于为什么火,自行百度google
我比较喜欢的两点是:

  1. 业务逻辑与View和Model分离,需求变更的时候修改起来简单
  2. 方便单元测试

未使用MVP程序是这样的


Paste_Image.png

使用MVP后是这样的


Paste_Image.png

那么现在开始用MVP来做一个简单的登录页面:

1. 写接口

- Model层接口

public interface IUserModel {
    /**
     * 登录
     *
     * @param username 用户名
     * @param password 密码
     * @param callback 回调
     */
    void login(String username, String password, Callback callback);
}

IUserModel接口就一个login方法,参数为用户名,密码以及回调

- Presenter层接口

public interface ILoginPresenter {
    /**
     * 登录
     */
    void login();
}

ILoginPresenter接口就一个login,不需要参数,描述登录这个行为

- View层接口

public interface ILoginView {
    /**
     * 从UI中获取用户输入的用户名
     *
     * @return
     */
    String getUsername();

    /**
     * 从UI中获取用户输入的密码
     *
     * @return
     */
    String getPassword();

    /**
     * 显示加loading
     *
     * @param msg
     */
    void showLoading(String msg);

    /**
     * 取消loading显示
     */
    void hideLoading();

    /**
     * 显示结果
     *
     * @param result
     */
    void showResult(String result);
}

ILoginView方法分两部份:
一部份是给Presenter提供数据的(View->Presenter)
getUsername
getPassword
另一部份是从Presenter接收数据用来显示的(Presenter->View)
showLoading
hideLoading
showResult
说白了就是数据交互

2. 实现接口

- Model层实现

public class UserModel implements IUserModel {

    Handler handler = new Handler();

    /**
     * 登录
     *
     * @param username 用户名
     * @param password 密码
     * @param callback 回调
     */
    @Override
    public void login(final String username, final String password, final Callback callback) {
        handler.postDelayed(new Runnable() {//延时200ms回调,模拟网络请求
            @Override
            public void run() {
                if ("huangx".equals(username) && "123456".equals(password)) {
                    callback.onSuccess();
                } else {
                    callback.onFailure("用户名或密码错误");
                }
            }
        }, 2000);
    }
}

UserModel 是IUserModel 的实现,正常情况就里就是调服务器登录接口,我们Demo这里就用延时操作来模拟网络请求。这里对用户名密码做了一个判断,如果用户名为huangx密码为123456就返回登录成功,否则返回“用户名或密码错误”

- Presenter层实现

public class LoginPresenter implements ILoginPresenter {

    private ILoginView loginView;
    private UserModel userModel;

    public LoginPresenter(ILoginView loginView, UserModel userModel) {
        this.loginView = loginView;
        this.userModel = userModel;
    }

    /**
     * 登录
     */
    @Override
    public void login() {
        loginView.showLoading("登录中...");
        userModel.login(loginView.getUsername(), loginView.getPassword(), new Callback() {
            @Override
            public void onSuccess() {
                loginView.hideLoading();
                loginView.showResult("登录成功");
            }

            @Override
            public void onFailure(String errorMsg) {
                loginView.hideLoading();
                loginView.showResult(errorMsg);
            }
        });
    }
}

LoginPresenter 是ILoginPresenter 的实现,也是MVP中最关键的一个角色,主要的业务逻辑都放在这里,起到连接Model层和View层的作用,从构造方法可以看出,它需要传入ILoginView和IUserModel的实例。
在login方法被调用时,LoginPresenter会从loginView获取到用户输入的用户名和密码,
并以此为参数调用userModel的login方法,
然后通过回调把结果返回给loginView

- View层实现

public class LoginActivity extends AppCompatActivity implements ILoginView, View.OnClickListener {

    private EditText username;
    private EditText password;
    private ProgressDialog progressDialog;
    private LoginPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        username = (EditText) findViewById(R.id.username);
        password = (EditText) findViewById(R.id.password);
        findViewById(R.id.login).setOnClickListener(this);
        progressDialog = new ProgressDialog(this);
        presenter = new LoginPresenter(this, new UserModel());
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.login:
                presenter.login();
                break;
        }
    }

    /**
     * 从UI中获取用户输入的用户名
     *
     * @return
     */
    @Override
    public String getUsername() {
        return username.getText().toString().trim();
    }

    /**
     * 从UI中获取用户输入的密码
     *
     * @return
     */
    @Override
    public String getPassword() {
        return password.getText().toString().trim();
    }

    /**
     * 显示加loading
     *
     * @param msg
     */
    @Override
    public void showLoading(String msg) {
        progressDialog.setMessage(msg);
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }
    }

    /**
     * 取消loading显示
     */
    @Override
    public void hideLoading() {
        if (progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

    /**
     * 显示结果
     *
     * @param result
     */
    @Override
    public void showResult(String result) {
        Toast.makeText(LoginActivity.this, result, Toast.LENGTH_SHORT).show();
    }
}

LoginActivity 是ILoginView的实现,让LoginPresenter 可以从这里获取到用户名和密码,并用Toast显示登录给果。点击登录按钮的时候调用LoginPresenter 的login方法角色登录动作。

至此就实现了一个最简单的MVP。代码在这里

二、Android MVP进阶

上一篇文章讲了最简单的一个Android MVP,这个只是用来让初学者了解MVP。网络上大部份讲MVP的只到那一步了,上一篇其中存在一些问题:

一. View层ILoginView 中showLoading、hideLoading这两个方法,在其它页也会用到,比如再来一个IRegisterView 注册页面,它同样也要showLoading、hideLoading,对于这些共有的方法抽出来放到一个基础接口里,其它接口继承它,看代码:

public interface MvpView {
    /**
     * 显示loading对话框
     *
     * @param msg
     */
    void showLoading(String msg);

    /**
     * 隐藏loading对话框
     */
    void hideLoading();

    /**
     * 显示错误信息
     *
     * @param errorMsg
     */
    void showError(String errorMsg);
}

这里写一个MvpView接口,包含三个方法,这三个方法大部份页面都会有(当然还有其它共有的方法也可以往上加)


改造ILoginView如下:

public interface ILoginView extends MvpView {
    /**
     * 从UI中获取用户输入的用户名
     *
     * @return
     */
    String getUsername();

    /**
     * 从UI中获取用户输入的密码
     *
     * @return
     */
    String getPassword();

    /**
     * 显示结果
     *
     * @param result
     */
    void showResult(String result);
}

ILoginView继承了MvpView,公有的方法在MvpView里,这里只存放特有的方法。

二. Presenter层LoginPresenter存在一个隐藏 的安全隐患:
当其成员loginView对应的页面不存在了,那么在Callback的onSuccess或都onFailure方法中直接调用loginView的hideLoading和showResult方法会报空指针异常。(特别是对于Fragment这种,有时候实例还在但是View已经销毁了,如ViewPager+Fragment这种)
针对这个问题,将Presenter层改造下:

public interface Presenter<V extends MvpView> {
    /**
     * Presenter与View建立连接
     *
     * @param mvpView 与此Presenter相对应的View
     */
    void attachView(V mvpView);

    /**
     * Presenter与View连接断开
     */
    void detachView();
}

写一个基础接口Presenter它接收MvpView的子类,有两个方法,attachView,detachView分别用于与MvpView建立与断开连接。

然后写一个对Presenter的基础实现类BasePresenter:

public class BasePresenter<V extends MvpView> implements Presenter<V> {
    /**
     * 当前连接的View
     */
    private V mvpView;

    /**
     * Presenter与View建立连接
     *
     * @param mvpView 与此Presenter相对应的View
     */
    @Override
    public void attachView(V mvpView) {
        this.mvpView = mvpView;
    }

    /**
     * Presenter与View连接断开
     */
    @Override
    public void detachView() {
        this.mvpView = null;
    }

    /**
     * 是否与View建立连接
     *
     * @return
     */
    public boolean isViewAttached() {
        return mvpView != null;
    }

    /**
     * 获取当前连接的View
     *
     * @return
     */
    public V getMvpView() {
        return mvpView;
    }

    /**
     * 每次调用业务请求的时候都要先调用方法检查是否与View建立连接,没有则抛出异常
     */
    public void checkViewAttached() {
        if (!isViewAttached()) {
            throw new MvpViewNotAttachedException();
        }
    }

    public static class MvpViewNotAttachedException extends RuntimeException {
        public MvpViewNotAttachedException() {
            super("请求数据前请先调用 attachView(MvpView) 方法与View建立连接");
        }
    }
}

BasePresenter是所有Presenter的父类,它实现的Presenter接口,并增加
checkViewAttached方法用于在调用业务前检查当前Presenter是否与MvpView建立连接
isViewAttached方法用于判断是否与MvpView处于连接状态
getMvpView获取当前所连接的MvpView对象


再然后把之前的LoginPresenter改造下:

public class LoginPresenter extends BasePresenter<ILoginView> implements ILoginPresenter {

    private IUserModel userModel;

    public LoginPresenter(IUserModel userModel) {
        this.userModel = userModel;
    }

    /**
     * 登录
     */
    @Override
    public void login() {
        checkViewAttached();
        getMvpView().showLoading("登录中...");
        userModel.login(getMvpView().getUsername(), getMvpView().getPassword(), new Callback() {
            @Override
            public void onSuccess() {
                if (isViewAttached()) {
                    getMvpView().hideLoading();
                    getMvpView().showResult("登录成功");
                }
            }

            @Override
            public void onFailure(String errorMsg) {
                if (isViewAttached()) {
                    getMvpView().hideLoading();
                    getMvpView().showResult(errorMsg);
                }
            }
        });
    }
}

LoginPresenter继承自BasePresenter并实现ILoginPresenter接口。
相比之前,去掉了成员 loginView,这个通过getMvpView()直接获得。
然后就是login方法中针对空指针隐患的解决:
先调用checkViewAttached()确保View存在才往下走。
然后在Callback回调里调用isViewAttached()判断此时View是否还存在,存在就执行下面的UI操作。


这一步以后,就是LoginActivity的修改:

public class LoginActivity extends AppCompatActivity implements ILoginView, View.OnClickListener {

    private EditText username;
    private EditText password;
    private ProgressDialog progressDialog;
    private LoginPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        username = (EditText) findViewById(R.id.username);
        password = (EditText) findViewById(R.id.password);
        findViewById(R.id.login).setOnClickListener(this);
        progressDialog = new ProgressDialog(this);
        presenter = new LoginPresenter(new UserModel());
        presenter.attachView(this);//这里与View建立连接
    }

    @Override
    protected void onDestroy() {
        presenter.detachView();//这里与View断开连接
        super.onDestroy();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.login:
                presenter.login();
                break;
        }
    }

    /**
     * 从UI中获取用户输入的用户名
     *
     * @return
     */
    @Override
    public String getUsername() {
        return username.getText().toString().trim();
    }

    /**
     * 从UI中获取用户输入的密码
     *
     * @return
     */
    @Override
    public String getPassword() {
        return password.getText().toString().trim();
    }

    /**
     * 显示结果
     *
     * @param result
     */
    @Override
    public void showResult(String result) {
        Toast.makeText(LoginActivity.this, result, Toast.LENGTH_SHORT).show();
    }

    /**
     * 显示loading对话框
     *
     * @param msg
     */
    @Override
    public void showLoading(String msg) {
        progressDialog.setMessage(msg);
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }
    }

    /**
     * 隐藏loading对话框
     */
    @Override
    public void hideLoading() {
        if (progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

    /**
     * 显示错误信息
     *
     * @param errorMsg
     */
    @Override
    public void showError(String errorMsg) {
        Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
    }
}

这里presenter的实例化只需要传入IUserModel实例,

presenter = new LoginPresenter(new UserModel());

接着关键点来了
在onCreate方法中

presenter.attachView(this);//这里与View建立连接

在onDestroy方法中

  presenter.detachView();//这里与View断开连接


但是看看LoginActivity:showLoading,hideLoading,showError这些公共的方法应该像MvpView一样抽出来。于是产生BaseActivity:

public class BaseActivity extends AppCompatActivity implements MvpView {

    protected ProgressDialog progressDialog;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        progressDialog = new ProgressDialog(this);
    }

    /**
     * 显示loading对话框
     *
     * @param msg
     */
    @Override
    public void showLoading(String msg) {
        progressDialog.setMessage(msg);
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }
    }

    /**
     * 隐藏loading对话框
     */
    @Override
    public void hideLoading() {
        if (progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

    /**
     * 显示错误信息
     *
     * @param errorMsg
     */
    @Override
    public void showError(String errorMsg) {
        Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
    }
}

BaseActivity实现了MvpView接口,把这些公共的方法放这里。


然后看看LoginActivity

public class LoginActivity extends BaseActivity implements ILoginView, View.OnClickListener {

    private EditText username;
    private EditText password;
    private LoginPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        username = (EditText) findViewById(R.id.username);
        password = (EditText) findViewById(R.id.password);
        findViewById(R.id.login).setOnClickListener(this);
        presenter = new LoginPresenter(new UserModel());
        presenter.attachView(this);//这里与View建立连接
    }

    @Override
    protected void onDestroy() {
        presenter.detachView();//这里与View断开连接
        super.onDestroy();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.login:
                presenter.login();
                break;
        }
    }

    /**
     * 从UI中获取用户输入的用户名
     *
     * @return
     */
    @Override
    public String getUsername() {
        return username.getText().toString().trim();
    }

    /**
     * 从UI中获取用户输入的密码
     *
     * @return
     */
    @Override
    public String getPassword() {
        return password.getText().toString().trim();
    }

    /**
     * 显示结果
     *
     * @param result
     */
    @Override
    public void showResult(String result) {
        Toast.makeText(LoginActivity.this, result, Toast.LENGTH_SHORT).show();
    }
}

瞬间清爽了有木有@_@
好了,Android MVP进阶篇就到此结束了,代码看这里


三、Android MVP高级

前面一篇文章,我们讲了Android MVP的进阶应用,解决了一个View空指针异常隐患,但是这样就完美了么?

No!还是有不足之处的:手机状态改变时,Activity状态会影响到Presenter实例对象!
Activity会在很多情况下被系统重启:当用户旋转屏幕、在后台时内存不足、改变语言设置、attache 一个外部显示器等。这种情况下如何保证Presenter不受影响呢?一般有如下几种方案:

方案1:就让Presenter一起挂掉吧

在这种方案下当Activity/Fragment挂掉时Presenter也一起挂掉了。此时如果想要保存一些状态信息的话就要借助onSaveInstanceState()方法了,需要在View的实现类中保存状态并在重新onCreate()时利用状态信息重新实例化Presenter,或者直接把保存下来的instance Bundle直接传给Presenter以便Presenter处理。如果是纯数据的话这样也没什么不好的,但如果要保存一些后台线程的引用的话就很难办了。

方案2:将Presenter保存在一个地方,再次onCreate时还原

我见到的最多的就是这个方案,不过具体的实现方法千差万别。
最简单最naive的实现方式,就是在Activity/Fragment中保留对Presenter的静态引用。这样我们可以再手机旋转时保留Presenter对象,但整个Applicaiton中只能有一个同样的Activity/Fragment-Presenter实例组合。但当一个ViewPager中有多个相同的Fragment时,这种方法就行不通了。

方案3:另一个方式就是调用Fragment的setRetainInstance(true)方法。

设置Fragment的这个属性可以保证Fragment不会被destroy,这样Presenter就随之被保留。但这种方式仍有局限,对一个子Fragment或是Activity这样就不起作用。

方案4:最后一种方式就是使用单例缓存机制并通过标识符来存储一个Presenter类的不同实例。

为了保留Presenter,Activity/Fragment需要在onSaveInstanceState()中传递Presenter实例的标识符。这里的问题是如何实现这种逻辑以及何时将Presenter从单例缓存中移除。

上述方案或这或那都有那么些毛病,那么有没有最佳方案呢?当然有,有请我们强大的Loader登场,铛铛铛铛~~
下面内容摘自:
译文:通过Loader延长Presenter生命周期
原文:Presenter surviving orientation changes with Loaders

最优方案:使用Loader
什么是Loader?它有什么用?

我们都知道,当手机状态发生改变比如旋转时,Activity会重新启动。Loader是Android框架中提供的在手机状态改变时不会被销毁的工具。Loader的生命周期是是由系统控制的,只有在向Loader请求数据的Activity/Fragment被永久销毁时才会被清除,所以也不需要自己写代码来清空它。

一般Loader是用来在后台加载数据的,而且是用它的子类CursorLoader或AsyncTaskLoader,尤其是CursorLoader,直接就绑定了Content Provider和数据库。当然如果写个类继承Loader基类的话也不需要开启后台线程

听起来好厉害。但这和Presenter有什么关系?

就像刚才说的一样,关键问题就是在哪里存储Presenter以及什么时候销毁它们。而我们刚刚就看到了Loader的强大之处:由安卓系统框架提供,有单独生命周期,会被自动回收且不必在后台运行。所以思考一下需求以及Loader的功能,我们可以

让Loader作为Presenter的提供者,而不需要担心手机状态改变

我们已经看到安卓系统框架中提供的Loader类有如下特点:

  • 在手机状态改变时不会被销毁

  • 会在Activity/Fragment不再被使用后由系统回收。

  • 与Activity/Fragment的生命周期绑定,所以事件会自己分发。

  • 每一个Activity/Fragment持有自己的Loader对象的引用,所以可以同时存在多个Presenter-Activity/Fragment组合,比如说在ViewPager中。

  • 可以同步运行,自己确定什么时候数据准备好了可以被传递。

当把这一切组合在一起并进行合理的抽象后,我们就有用来缓存Presenter的工具了,这样Presenter在手机旋转时就不会被销毁了,想活多长或多长。

将同步的Loader作为存放Presenter的缓存。

这里的重点就在于同步使用Loader时,我们可以知道在生命周期的哪个阶段Presenter被创建了并且可以工作了。甚至是在Activity/Fragment可见之前。

要注意的是,Activity和Fragment在何时传递Presenter对象这个问题上是有区别的。对于任何一个Activity实例,只需要在调用super.onStart()之后就可以使用Presenter了,但对于Fragment实例来说,首次创建时可以在super.onStart()之后传入,但在Fragment被重新create时,就必须要在super.onResume()之后传入了。所以在Fragment中,只需要在onResume()方法执行后将Presenter传入就行了。

使这种方法可行的另外一个要点就是系统对Loader的处理方式,每一个Activity/Fragment都有一个LoaderManager,而且只有这个LoaderManager可以管理与Activity/Fragment相关联的Loader,这就使得相同的Fragment与其Presenter可以同时存在多个实例。

贴代码:

public class PresenterLoader<T extends Presenter> extends Loader<T> {

    private final PresenterFactory<T> factory;

    private T presenter;

    public PresenterLoader(Context context, PresenterFactory factory) {
        super(context);
        this.factory = factory;
    }

    @Override
    protected void onStartLoading() {
        if (presenter != null) {
            deliverResult(presenter);
            return;
        }
        forceLoad();
    }

    @Override
    protected void onForceLoad() {
        presenter = factory.create();
        deliverResult(presenter);
    }

    @Override
    protected void onReset() {
        presenter = null;
    }
}

onStartLoading():会在Activity的onStart()调用之后被系统调用来获取一个Loader实例。在这里先判断是否已经有Presenter对象了还是需要创建。

onForceLoad():在调用forceLoad()方法后自动调用,我们在这个方法中创建Presenter并返回它。

deliverResult():会将Presenter传递给Activity/Fragment。

onReset():会在Loader被销毁之前调用,我们可以在这里告知Presenter以终止某些操作或进行清理工作。

PresenterFactory:这个接口可以隐藏创建Presenter所需要的参数。通过这个接口我们可以调用各种构造器,这样可以避免写一堆PresenterLoader的子类来返回不同类型的Presenter
这个接口形式上大概就是这样:

public interface PresenterFactory<T extends Presenter> {
    T create();
}

以下引上前面引文内容(太多文字写不出来,拿来主义,哈哈):

在Activity/Fragment中如何实现呢?

现在我们已经实现了Loader,下面要将Loader和Activity/Fragment连接起来,刚才说过,这个连接点就是LoaderManager。我们需要调用FragmentActivity的getSupportLoaderManager()或Fragment的getLoaderManager()方法来获得LoaderManager实例并调用其initLoader()方法。Google建议在Activity的onCreate()与Fragment的onActivityCreated()中调用此方法。

当调用initLoader()方法时要传入一个id,只需要保证在一个Activity/Fragment内单一即可,不需要全局单一。这个id就是用来识别Loader的。还可以选择传入一个Bundle,但在这个例子中不需要。还要穿入一个LoaderCallbacks实例。

刚才说过,不要再Fragment的onCreate()方法中调用initLoader()方法,要在onActivityCreated()中调用它,不然就会遇到不同Fragment共享一个Loader的问题。

如果你发现在手机状态改变时onLoadFinished()会被调用两次,不妨参考stackoverflow下的这个问题。Presenter在被传入Activity后的逻辑可能会使这个成为一个问题,此时可以尝试在Fragment的onResume()方法中调用initLoader()方法,或者在onActivityCreate()方法中存一个flag以防多次获取Presenter。

当我们调用了initLoader()后Loader和Activity/Fragment的生命周期就绑定了:执行onStart()方法时会调用onStartLoading(),执行onStop()方法时会调用onStopLoading()。但onReset()方法只会在Activity/Fragment被销毁或主动调用destroyLoader()时被调用。

LoaderManager有一个restartLoader()方法可以强制重新加载。不过除非我们需要重新创建Presenter,不然不需要调用这个方法。

通过LoaderCallbacks获取Presenter
LoaderCallbacks是Activity/Fragment和Loader之间的桥梁。共有三个回调方法:
onCreateLoader():在这里构造Loader实例。
onLoadFinished():Loader在这里传入数据,在这个例子中,也就是Presenter。
onLoadReset():在这里清除对于数据的引用。

引文结束,代码贴自己的:
这里跟引文不一样,我们把Presenter LoaderCallbacks的实现都放在我们上篇文章中的BaseActivity里(当然同理可加一个BaseFragment)

public class BaseActivity<P extends Presenter<V>, V extends MvpView> extends AppCompatActivity implements MvpView, LoaderManager.LoaderCallbacks<P> {

    public final static int BASE_ACTIVITY_LOADER_ID = 100;

    protected ProgressDialog progressDialog;

    protected P presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        progressDialog = new ProgressDialog(this);
        getSupportLoaderManager().initLoader(BASE_ACTIVITY_LOADER_ID, null, this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (presenter != null) {
            presenter.attachView((V) this);
        }
    }

    @Override
    protected void onDestroy() {
        if (presenter != null) {
            presenter.detachView();
        }
        super.onDestroy();
    }

    /**
     * 显示loading对话框
     *
     * @param msg
     */
    @Override
    public void showLoading(String msg) {
        progressDialog.setMessage(msg);
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }
    }

    /**
     * 隐藏loading对话框
     */
    @Override
    public void hideLoading() {
        if (progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

    /**
     * 显示错误信息
     *
     * @param errorMsg
     */
    @Override
    public void showError(String errorMsg) {
        Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public Loader<P> onCreateLoader(int id, Bundle args) {
        // TODO: 子类要实现此方法创建Loader 
        return null;
    }

    @Override
    public void onLoadFinished(Loader<P> loader, P data) {
        presenter = data;
    }

    @Override
    public void onLoaderReset(Loader<P> loader) {
        presenter = null;
    }
}

这段代码的要点有:

在Activity/Fragment实例中通过一个唯一的ID初始化Loader。

getSupportLoaderManager().initLoader(LOADER_ID, null, this);
onStart()方法执行时会创建Loader或重新连接到一个已经存在的Loader。在这里我们在onLoadFinished()里创建并传递Presenter对象。由于这些代码都是同步的,所以当onStart()方法执行完后Presenter也可以正常工作了

另外注意一点,引文原文作者的代码里把detachView放在onStop里,这是不合适的,当你调用startActivityForResult的时候,当前Activity会调onStop,Presenter与view断开连接,等从下一个页面返回的时候如果带着数据,并要在onActivityResult里执行业务操作的时候,问题就出现了,这个时候你的Activity的onStart还没有调呢!onActivityResult在onStart之前调@@@,于是Presenter与view还没有建立连接,然后就抛出了MvpViewNotAttachedException这个异常,解决办法就是把detachView放到onDestory里。

我们再看BaseActivity里:

    @Override
    public Loader<P> onCreateLoader(int id, Bundle args) {
        // TODO: 子类要实现此方法创建Loader 
        return null;
    }

onCreateLoader返回空,这个要在子类中实现,那么就看看子类中怎么实现,改造我们的LoginActivity

public class LoginActivity extends BaseActivity<LoginPresenter, ILoginView> implements ILoginView, View.OnClickListener {

    private EditText username;
    private EditText password;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        username = (EditText) findViewById(R.id.username);
        password = (EditText) findViewById(R.id.password);
        findViewById(R.id.login).setOnClickListener(this);
    }

    @Override
    public Loader<LoginPresenter> onCreateLoader(int id, Bundle args) {
        return new PresenterLoader(this, new PresenterFactory<LoginPresenter>() {
            @Override
            public LoginPresenter create() {
                return new LoginPresenter(new UserModel());
            }
        });
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.login:
                presenter.login();
                break;
        }
    }

    /**
     * 从UI中获取用户输入的用户名
     *
     * @return
     */
    @Override
    public String getUsername() {
        return username.getText().toString().trim();
    }

    /**
     * 从UI中获取用户输入的密码
     *
     * @return
     */
    @Override
    public String getPassword() {
        return password.getText().toString().trim();
    }

    /**
     * 显示结果
     *
     * @param result
     */
    @Override
    public void showResult(String result) {
        Toast.makeText(LoginActivity.this, result, Toast.LENGTH_SHORT).show();
    }
}

看到没有,我们在这里实现了了onCreateLoader,在这里产生LoginPresenter
哈哈,整个LoginActivity又变干净了有木有,大部份Presenter,Loaders都放在了BaseActivity中。LoginActivity里只要实现onCreateLoader就行了
Android MVP 高级篇到此结束,代码在这里

四、Android MVP扩展(Android Clean Architecture干净架构)

我们在M-V-P中的M和P之间加一层:UseCase,这个就是大名鼎鼎的Android Clean Architecture,

Android 干净架构(Android Clean Architecture)

Clean一般是指,代码以洋葱的形状依据一定的依赖规则被划分为多层:内层对于外层一无所知。这就意味着依赖只能由外向内

下面用几张图简单说下什么是Android Clean Architecture:


Paste_Image.png

上面这张图清晰的表明的Android Clean Architecture的思想:
它把整个应用从外到内分为四层

  • 最底层的是Entities也就是Model层,只负责取数据,不管是从网络还是从本地
  • 第二层是UseCase层,它负责把Model层获取来的数据进行处理,然后把处理结果传给上层
  • 第三层是Presenter层,它负责联通UseCase层与最外围的UI层
  • 最外层是UI层,负责UI处理逻辑

下面这张图是googlesample的Android Clean Architecture结构


Paste_Image.png

分了三层:

  • DataLayer ,也就是Model层
  • DomainLayer ,也就是UseCase层
  • PresentationLayer,也就是Presenter+UI层

个人觉得google这个分层方式比较好,后面我就以这三层来讲。
下面引用这篇文章内容说明下 简单的 Android Clean Architecture 实现

DataLayer

最底层,完全不知道有 DomainLayer , PresentationLayer 的存在,它的主要职责是:
1、从网络获取数据,向网络提交数据,总之就是和网络打交道。
2、从本地DB,shareprefence等等,内存等,总之就是本地获取数据,缓存数据,总之就是和本地数据打交道的。

DomainLayer

中间层,他完全不知道有一个 PresentationLayer 存在,他只知道,有DataLayer,它的主要职责是:
控制 DataLayer 对数据做 增删改查

PresentationLayer

最上层,他知道 DomainLayer,注意他并不知道DataLayer,它的职责是:
通知 DomainLayer 有活干了,根据 DomainLayer 反馈变化界面 

三层结构每一层都只知道自己的上一层,就好像一个公司结构一样,老板指挥管理层,管理层指挥基层,老板不直接指挥基层

对Android来说的意义

一般而言,你的应用可以有任意数量的层。但是,除非需要在所有的Android应用中采用企业级的业务逻辑,一般的应用至多只有三层:

  • 外层:实现层

  • 中层:接口适配层

  • 内层:业务逻辑层

实现层包含了所有框架相关的东西。框架相关的代码包含了所有不是专门用来解决目标问题的部分,例如创建activity和fragment、发送目标以及网络和数据库相关的框架代码。

接口适配层的目的就是负责连接业务逻辑和框架相关的代码。

应用中最重要的就是业务逻辑层。它负责解决你的应用所真正想解决的问题。该层不包含任何框架相关的代码,因此其代码应该可以在没有模拟器的情况下独立运行。这样,测试、开发和维护业务逻辑代码就要容易很多。而这就是干净架构的主要优势。

在核心层以上的每一层,也都负责在更低层使用模型之前将它们转换为更低层的模型。内层不能使用任何属于外部的模型类的引用;但是外层可以使用和引用内层的模型——这都是因为依赖规则。这种方式会造成一定的开销,但确保了层与层之间代码的解耦合。

为什么模型转换是必须的呢?那是因为,业务逻辑模型未必适合直接向用户进行展示,或是需要一次展示多个业务逻辑模型的组合。因此,我建议先创建一个ViewModel类来充当中间人。然后,在外层使用一个转换类将业务模型转换为合适的ViewModel。

好了,Android Clean Architecture就介绍到这里,下面讲实现:
我们在上一篇源码基础上改
选加一个BaseUseCase:

public abstract class BaseUseCase<Q extends BaseUseCase.RequestValues, P extends BaseUseCase.ResponseValue> {

    private Q mRequestValues;

    private UseCaseCallback<P> mUseCaseCallback;

    public void setRequestValues(Q requestValues) {
        mRequestValues = requestValues;
    }

    public Q getRequestValues() {
        return mRequestValues;
    }

    public UseCaseCallback<P> getUseCaseCallback() {
        return mUseCaseCallback;
    }

    public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) {
        mUseCaseCallback = useCaseCallback;
    }

    public void run() {
        executeUseCase(mRequestValues);
    }

    /**
     * 执行该用例
     *
     * @param requestValues 用例输入
     */
    public abstract void executeUseCase(Q requestValues);

    /**
     * 用例的输入(请求参数)
     */
    public interface RequestValues {
    }

    /**
     * 用例的输出(返回结果)
     */
    public interface ResponseValue {
    }

    /**
     * 用例回调
     *
     * @param <R>
     */
    public interface UseCaseCallback<R> {
        void onSuccess(R response);

        void onError(String errorCode, Object... errorMsg);
    }
}

这是一个虚类,是所有用例的父类,子类需要实现executeUseCase方法。
BaseUseCase类里有三个接口:

  • RequestValues:代表用例的输入数据,所有用例输入都要实现此接口
  • ResponseValue :代表用例的输出数据,所有用例输出都要实现此接口
  • UseCaseCallback :是用例的回调,用于向外层反馈用例执行结果

每个用例都要传入RequestValues以及UseCaseCallback,当用例执行的时候使用RequestValues向model层取数据,然后将数据封装成ResponseValue,通过UseCaseCallback返回给调用者。

接下来写一个LoginCase,继承自BaseUseCase

public class LoginCase extends BaseUseCase<LoginCase.RequestValues, LoginCase.ResponseValue> {

    private final IUserModel userModel;

    public LoginCase(IUserModel userModel) {
        this.userModel = userModel;
    }

    /**
     * 执行该用例
     *
     * @param requestValues 用例输入
     */
    @Override
    public void executeUseCase(RequestValues requestValues) {
        String username = requestValues.getUsername();
        String password = requestValues.getPassword();
        userModel.login(username, password, new Callback() {
            @Override
            public void onSuccess() {
                getUseCaseCallback().onSuccess(new ResponseValue());
            }

            @Override
            public void onFailure(String errorMsg) {
                getUseCaseCallback().onError("1001", errorMsg);
            }
        });
    }

    public static final class RequestValues implements BaseUseCase.RequestValues {
        private final String username;
        private final String password;

        public RequestValues(@NonNull String username, @NonNull String password) {
            this.username = username;
            this.password = password;
        }

        public String getUsername() {
            return username;
        }

        public String getPassword() {
            return password;
        }
    }

    public static final class ResponseValue implements BaseUseCase.ResponseValue {
    }
}

LoginCase包含两个类RequestValues,ResponseValue ,分别实现了BaseUseCase中的RequestValues,ResponseValue 接口
LoginCase构造方法中传入了IUserModel参数,这里就把DataLayer引进来了。
LoginCase实现了executeUseCase方法,这里从RequestValues获取用户名和密码,然后调用userModel的login方法,发出登录请求,之后通过getUseCaseCallback获取回调,把结果通过回调返回给调用者(Presenter)。
到这里一个登录的用例就写好了,然后是Presenter的改造

public class LoginPresenter extends BasePresenter<ILoginView> implements ILoginPresenter {

    private final LoginCase loginCase;

    public LoginPresenter(LoginCase loginCase) {
        this.loginCase = loginCase;
    }


    /**
     * 登录
     */
    @Override
    public void login() {
        checkViewAttached();
        getMvpView().showLoading("登录中...");
        LoginCase.RequestValues requestValues = new LoginCase.RequestValues(getMvpView().getUsername(), getMvpView().getPassword());
        loginCase.setRequestValues(requestValues);
        loginCase.setUseCaseCallback(new BaseUseCase.UseCaseCallback<LoginCase.ResponseValue>() {
            @Override
            public void onSuccess(LoginCase.ResponseValue response) {
                if (isViewAttached()) {
                    getMvpView().hideLoading();
                    getMvpView().showResult("登录成功");
                }
            }

            @Override
            public void onError(String errorCode, Object... errorMsg) {
                if (isViewAttached()) {
                    getMvpView().hideLoading();
                    getMvpView().showError(errorMsg.toString());
                }
            }
        });
        loginCase.run();
    }
}

LoginPresenter构造方法传入了参数LoginCase, 调用login方法时候往loginCase中传入RequestValues和UseCaseCallback,然的调用loginCase的run方法就OK了,这样一来Presenter就变得很干净了。

接着我们写一个UseCaseManager用来管理UseCase的实例化(以便切换分支操作参考我这篇文章:Android Studio Flavors的妙用

public class UseCaseManager {
    public static LoginCase provideLoginCase() {
        return new LoginCase(ModelManager.provideUserModel());
    }
}

最后修改下LoginActivity的onCreateLoader方法

    @Override
    public Loader<LoginPresenter> onCreateLoader(int id, Bundle args) {
        return new PresenterLoader(this, new PresenterFactory<LoginPresenter>() {
            @Override
            public LoginPresenter create() {
                return new LoginPresenter(UseCaseManager.provideLoginCase());
            }
        });
    }

到此Android MVP+Android Clean Architecture就讲完了,代码在这里

或http://download.csdn.net/detail/lvxiangan/9659827



其他问题:
问:android MVP结构中context该如何传递?
答:MvpView中定义一个getContext() 方法就可以了




评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值