MVP模式之todoMVP的使用和擴展

前言

構思了一段時間,恰巧今天也有空,就把MVP探索系列的文章暫時畫上一個句號吧,當然,知識的海洋是無窮無盡的,學海無涯,在以後的工作中,如果鄙人總結出了一些好的相關的經驗或是學習到相關的優秀的知識沒我還會回來和大家分享的。

其實,todoMVP是google官方出品的在Android項目中使用MVP模式的一種思路,應該算是比較權威的學習資料吧,研究和學習的人肯定很多,我作爲一枚Android程序員,還是一枚普通的Android程序員,其實是不太敢輕易去置喙什麼的,只是將我在項目裏的實際使用心得和變更的過程寫出來,供大家參考,使自己更深刻的理解MVP這種模式。

一 、todoMVP簡介

先附上todoMVP的官方示例地址

1.1 todoMVP的出現的原因猜測

自從 2015下半年來,MVP漸漸崛起成爲了現在普遍流行的架構模式。但是各種不同實現方式的MVP架構層出不窮,也讓新手不知所措。而Google作爲“老大哥”,針對此現象爲Android架構做出了“規範示例”:android-architecture。

1.2 todoMVP的原理圖

google把model層更加細化,區分出本地數據庫、網絡、內存來管理數據;Activity直接實現presenter可以根據自身的生命週期,直接控制presenter的生命週期;Fragment直接實現view,方便Activity控制和管理。總的來說,就是用大家習慣和熟悉的東西實現了MVP模式,不愧是官方出品。

1.3 todoMVP架構類圖(部分)

這裏以TaskDetail和TaskDataSource層爲例

TaskDetail

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KLFXFXVs-1572250534163)(https://ykbjson.github.io/blogimage/mvppicture3/task_detail.png)]

TaskDataSource

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ic5n7B3f-1572250534164)(https://ykbjson.github.io/blogimage/mvppicture3/task_data_source.png)]

1.4 todoMVP的一般分包結構

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oAXskIST-1572250534164)(http://ofyt9w4c2.bkt.clouddn.com/20170222/20170227212108.png)]

根據圖片可以看出來,todoMVP模式是按功能模塊分包的,項目結構和功能關係一目瞭然,便於維護。在每個功能模塊裏,google引入了一個“契約”類——xxxxContract,該類用來定義該模塊下所有的prsenter和view要實現的功能接口以及presenter和view的關係。如果想了解某個模塊有些什麼功能,直接看這個契約類就可以了;如果想了解該模塊下presenter和view的關係,也可以直接看這個契約類。我們以TasksContract(位於tasks包下)爲例

public interface TasksContract {

    interface View extends BaseView<Presenter> {

        void setLoadingIndicator(boolean active);

        void showTasks(List<Task> tasks);

        void showAddTask();

        void showTaskDetailsUi(String taskId);

        ...
    }

    interface Presenter extends BasePresenter {

        void result(int requestCode, int resultCode);

        void loadTasks(boolean forceUpdate);

        void addNewTask();

        ...
    }
}

看了這個類以後,是不是不用去看該模塊下所有的代碼,就已經知道該模塊大體的功能了?

todoMVP相關的信息就簡單介紹到這裏啦,沒什麼好總結的,非常簡潔。如果想詳細的瞭解更多,可以去剛纔給出的鏈接地址那裏慢慢看,英語不好的童鞋請帶好翻譯…下面我們將探討todoMVP模式在實際項目裏使用的過程和問題。

二、todoMVP在實際項目裏使用的問題

2.1 遇到的問題

我相信,大多數開發者的項目裏都多多少少涉及到網絡請求吧,同步獲取數據的方式我暫且拋開不談,我們來談一下在Fragment或Activity裏面使用異步方式獲取網絡數據的時候,大家都會關係的一個問題,就是網絡請求的生命週期和界面本身生命週期的問題。

舉個簡單的例子哈,Retrofit應該有很多人用過吧,非常好用吧,傑克沃爾頓大神的精品之一,標準的RESTFull設計,基於OkHttp,支持同步異步加載數據,用註解把複雜的網絡請求變換成Java接口,對於習慣於面向對象的程序員來說,簡直就是福利啊。當我們在一個界面(Fragment或Activity)裏使用Retrofit發起異步請求的時候,我們是這麼做的

// 第1部分:在網絡請求接口的註解設置
@GET("openapi.do?keyfrom=Yanzhikai&key=2032414398&type=data&doctype=json&version=1.1&q=car")
Call<Translation>  getCall();

// 第2部分:在創建Retrofit實例時通過.baseUrl()設置
Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://fanyi.youdao.com/") //設置網絡請求的Url地址
                .addConverterFactory(GsonConverterFactory.create()) //設置數據解析器
                .build();

//第3部分發送網絡請求(異步)
    Call<Translation> call= retrofit.create(ITranslation.class).getCall();
    call.enqueue(new Callback<Translation>() {
        //請求成功時回調
        @Override
        public void onResponse(Call<Translation> call, Response<Translation> response) {
            //請求處理,輸出結果
            response.body().show();
        }

        //請求失敗時候的回調
        @Override
        public void onFailure(Call<Translation> call, Throwable throwable) {
            System.out.println("連接失敗");
        }
    });

我們主要看第3部分,當onResponse或者onFailure方法回調的時候,我們可定時要去渲染UI組件的吧,那如果這個時候,Activity或者Fragment已經destroy了,會出現什麼結果?我想,大多數時候出現的都是NPE(NullPointerException),因爲destroy方法之後,Activity或者Fragment的UI組件已經被系統回收了,而我們卻要去渲染他們,所以一般會出現NPE。

2.2 解決思路

當然,作爲一枚程序員,解決這個問題非常簡單,一般我們有幾種方法解決這個問題:

1.我們可以把Call對象放到Activity或者Fragment的全局,在Activity或者Fragment已經destroy的時候調用其cancel方法,取消執行的請求…然而,我可以完全負責任的告訴你,在Retrofit1.x版本里是行不通的,因爲你翻看源碼會發現,cancel方法只會取消在隊列裏還沒有執行的請求,那些已經執行了的請求是沒有效果的。當然啦,Retrofit2.x的Call的cancel方法是有效的,不過這樣的方法導致的結果是,在每個有網絡請求的頁面,你都得維護一個請求列表或是map

2.我們可以在全局定義一個布爾值,在Activity或者Fragment執行destroy的時候去控制這個布爾值,然後當onResponse或者onFailure方法回調的時候,我們根據這個布爾值看是否渲染UI組件。又或者,Activity或者Fragment本身就有獲取自身生命週期狀態的方法,當onResponse或者onFailure方法回調的時候,我們根據那些方法的返回值看是否渲染UI組件。這樣也可以有效的避免NPE問題,不過這樣的辦法導致的結果是,在每個頁面的每個網絡請求回調裏你都不得不去寫一個或多個判斷

3.以上兩個方法都很好實現,這裏就不做詳細的展開和嘗試了,下面我們着重討論另外一種辦法,就是在回調接口Callback裏利用弱引用,將請求網絡的真正發起者關聯進來,在適當的時候通過請求網絡的真正發起者其自身的方法獲取其自身的狀態來決定Callback是否需要繼續執行回調。有點拗口,但是其實實現起來很簡單的。

當然,其實還有一些開源框架,可以綁定Activity或者Fragment的生命週期和網絡請求的生命週期,這裏暫且不做深入討論。

2.3 具體實現

現在,我們回到todoMVP模式來,todoMVP模式裏,所有的數據操作,當然也就包括網絡數據請求操作,都是封裝在xxxRepository裏面的,那麼,我們是否可以改造一下Repository和Callback,讓Repository可已把真正請求網絡的發起者傳遞給Callback,讓Callback自己管理其回調流程呢?我們以一個項目裏登錄模塊爲例

先看看大致的包結構

然後是BasePresenter接口和BaseView接口

BasePresenter接口,和todoMVP裏的一模一樣

public interface BasePresenter {

    void start();
    
}

BaseView接口,稍微有一點改動,增加了一個獲取Context方法(其實後面也沒用到)

interface BaseView<T> {
    @NonNull
    Context getMContext();

    void setPresenter(@NonNull T presenter);
}

我在這裏,爲了讓要充當View的Fragment或Activity方便實綁定和Presenter的關係,所以封裝了一個BaseMVPPresenter

public abstract class BaseMVPPresenter<V extends BaseView> implements BasePresenter {

    protected V mView;

    public BaseMVPPresenter(V view) {
        if (null == view) {
            throw new NullPointerException("View can not be null");
        }
        this.mView = view;
        //noinspection unchecked
        this.mView.setPresenter(this);
    }

    @Override
    public void start() {

    }
}

然後一個BaseMVPFragment應運而生

public abstract class BaseMVPFragment<T extends BaseMVPPresenter> extends BaseFragmentV4 implements BaseView<T> {

    protected T presenter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (null != presenter) {
            presenter.start();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (null == outState) {
            outState = new Bundle();
        }
        outState.putSerializable("FragmentPresenter", presenter);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        if (null == presenter && null != savedInstanceState && savedInstanceState.containsKey("FragmentPresenter")) {
            try {
                presenter = (T) savedInstanceState.getSerializable("FragmentPresenter");
                if (null!=presenter){
                    presenter.mView=this;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        super.onViewStateRestored(savedInstanceState);
    }

	@Override
    public Context getMContext() {
        return getContext();

    }

	@Override
    public void setPresenter(T presenter) {
        this.presenter = presenter;
    }
}

持有一個BaseMVPPresenter的泛型,實現BaseView接口。在這裏涉及到一個很彆扭的問題,後面我會提出來,希望有高手可以指點迷津

現在,按照todoMVP的慣例,我們開始寫那個契約類

interface LoginContract {

    interface BaseLoginView extends BaseView<BaseLoginPresenter> {

        void onLoginSuccess();

        void onLoginFailed(String reason);

        String getAccount();

        String getPassword();
    }

    abstract class BaseLoginPresenter extends BaseMVPPresenter<BaseLoginView> {
        BaseLoginPresenter(BaseLoginView view) {
            super(view);
        }

        abstract void doLogin();
    }

}

這個契約類簡單明瞭,LoginView實現四個方法,LoginPresenter實現一個登錄操作。

登錄的Presenter,LoginPresenter

final class LoginPresenter extends LoginContract.BaseLoginPresenter {

    private LoginRepository repository;

    LoginPresenter(LoginContract.BaseLoginView view) {
        super(view);
        repository = new LoginRepository(view);
    }

    @Override
    void doLogin() {
        final String account = mView.getAccount();
        final String password = mView.getPassword();
        if (TextUtils.isEmpty(account)) {
            mView.onLoginFailed("賬號不能爲空!");
            return;
        }
        if (TextUtils.isEmpty(password)) {
            mView.onLoginFailed("密碼不能爲空!");
            return;
        }

        repository.doLogin(account, password, new OnLoadDataCallback<User>() {
            @Override
            public void onSuccess(User data) {
                saveUser(data);
                mView.onLoginSuccess();
            }

            @Override
            public void onError(Object error) {
                mView.onLoginFailed(error.toString());
            }
        });
    }

    /**
     * 存儲用戶信息
     *
     * @param user
     */
    private void saveUser(User user) {
        ...
    }
}


這裏主要注意的是構造方法裏的兩句代碼,第一句讓實現BaseMVPPresenter的類持有了實現BaseView的View層;第二句,就是讓Repository也持有了實現BaseView的View層(大部分情況下的原始發起者)。

我們爲了減少Repository的代碼,抽象出一個BaseRepository

public class BaseRepository {
    protected Object token;
    public BaseRepository(Object token) {
        this.token = token;
    }
}

在進一步區分本地和網絡請求操作,再抽出BaseRemoteRepository

public class BaseRemoteRepository  extends BaseRepository{

    public BaseRemoteRepository(@NonNull Object token) {
        super(token);
    }

    protected String combineUrl(String endPoint) {
        return Constant.URL.URL_BASE_URL_.concat(endPoint);
    }

    protected <T> T create(Class<T> clazz) {
        return create(clazz, true);
    }

    protected <T> T create(Class<T> clazz, boolean needDefaultHeaders) {
        return RetrofitApi.getInstance().create(clazz, needDefaultHeaders);
    }
}

這裏只是附加了一些便於發起網絡請求的方法。

按照Retrofit的模式,我們要定義登錄相關的接口啦

public interface ILoginDataSource {

    interface ILogin {
        /**
         * 登錄
         */
        @POST(Constant.URL.URL_FOR_LOGIN)
        Call<ResponseBean<User>> doLogin(@Body Map<String, Object> map);

        /**
         * 註銷
         */
        @POST(Constant.URL.URL_FOR_LOGOUT)
        Call<ResponseBean> doLogout(@Body Map<String, Object> map);
    }

    void doLogin(String mobile, String password, OnLoadDataCallback<User> callback);

    void doLogout(OnLoadDataCallback<Void> callback);
}

兩個方法,登錄、註銷。ILogin裏是Retrofit要調用的實際的方法。

然後我們來看看登錄請求操作的真正操作者LoginRepository

public class LoginRepository extends BaseRemoteRepository implements ILoginDataSource {

    public LoginRepository(@NonNull Object token) {
        super(token);
    }

    @Override
    public void doLogin(String mobile, String password, final
    OnLoadDataCallback<User> callback) {
        // TODO: 2018/3/22 replace with real logic
        TokenBean bean = new TokenBean();
        bean.setAccessToken("9rirofnvndjsjnsdjv_lfs47qwq^^");
        User user = new User();
        user.setAvatar("http://pic.downcc.com/upload/2016-7/20167181357357469.png");
        user.setName("孫吉吉");
        user.setRole(1);
        user.setId(1);
        user.setMobile(mobile);
        user.setToken(bean);
        callback.onSuccess(user);

//        Map<String, Object> map = new HashMap<>();
//       	create(ILogin.class).doLogin(map)
//                .enqueue(new RCallback<ResponseBean<User>>(token) {
//                    @Override
//                    public void success(ResponseBean<User> responseBean) {
//                        if (responseBean.isAvailable()) {
//                            callback.onSuccess(responseBean.getData());
//                        } else {
//                            callback.onError(responseBean.getMessage());
//                        }
//                    }
//
//                    @Override
//                    public void failure(String s) {
//                        callback.onError(s);
//                    }
//                });
    }

    @Override
    public void doLogout(final OnLoadDataCallback<Void> callback) {
        Map<String, Object> map = new HashMap<>();
        create(ILogin.class).doLogout(map)
                .enqueue(new RCallback<ResponseBean>(token) {
                    @Override
                    public void success(ResponseBean responseBean) {
                        if (responseBean.isAvailable()) {
                            callback.onSuccess(null);
                        } else {
                            callback.onError(responseBean.getMessage());
                        }
                    }

                    @Override
                    public void failure(String s) {
                        callback.onError(s);
                    }
                });
    }

}

看doLogin方法裏被註釋的部分,這就是真正的登錄請求邏輯。

到這裏,登錄的發起邏輯就算完成了,那麼我前面說的Callback自己管理回調流程的邏輯是這麼實現的呢?這裏似乎還看不出一點關係?事實上,細心的人肯定已經看出了端倪,

	 Map<String, Object> map = new HashMap<>();
    create(ILogin.class).doLogin(map)
            .enqueue(new RCallback<ResponseBean<User>>(token) {
                @Override
                public void success(ResponseBean<User> responseBean) {
                    if (responseBean.isAvailable()) {
                        callback.onSuccess(responseBean.getData());
                    } else {
                        callback.onError(responseBean.getMessage());
                    }
                }

                @Override
                public void failure(String s) {
                    callback.onError(s);
                }
            });

這裏有一句代碼

 new RCallback<ResponseBean<User>>(token) 

這個token是什麼呢?我們回去看一眼LoginPresenter的構造方法你就明白了,我們暫且認爲他就是“View”吧。接下來,我們看看Callback把這個View拿去幹嘛了。

爲了便於實現Callback,我封裝了一層SCallback

public abstract class SCallback<T> implements Callback<T> {
    protected boolean isCanceled;
    protected ContextHolder<Object> contextHolder;

    public SCallback(@NonNull Object host) {
        contextHolder = new ContextHolder<>(host);
    }

    /**
     * 檢測是否符合回調服務器返回數據的條件
     *
     * @param call
     * @return
     */
    protected boolean checkCanceled(Call call) {
        return isCanceled || call.isCanceled() || !contextHolder.isAlive();
    }


    private void cancel() {
        isCanceled = true;
    }

    /**
     * 請求數據失敗
     *
     * @param t
     */
    public abstract void success(T t);

    /**
     * 請求數據成功
     *
     * @param errorMessage
     */
    public abstract void failure(String errorMessage);
}

我們關注一下checkCanceled(Call call)方法,我們定義了一個全局變量isCanceled,供外部手動調用cancel方法,這個意義很明顯,就是告訴Callback,我不在需要你的回調了。如果外部沒有手動調用cancel方法,那麼就結合!contextHolder.isAlive()的值來判斷是否需要回調。ContextHolder,沒什麼神奇的,就是個弱引用


public class ContextHolder extends WeakReference {
public ContextHolder(T r) {
super®;
}

    /**
     * 判斷是否存活
     *
     * @return
     */
    public boolean isAlive() {
        T ref = get();
        if (ref == null) {
            return false;
        } else {
            if (ref instanceof Service) {
                return isServiceAlive((Service) ref);
            } else if (ref instanceof Activity) {
                return isActivityAlive((Activity) ref);
            } else if (ref instanceof Fragment) {
                return isFragmentAlive((Fragment) ref);
            } else if (ref instanceof android.support.v4.app.Fragment) {
                return isV4FragmentAlive((android.support.v4.app.Fragment) ref);
            } else if (ref instanceof View) {
                return isContextAlive(((View) ref).getContext());
            } else if (ref instanceof Dialog) {
                return isContextAlive(((Dialog) ref).getContext());
            }
        }
        return true;
    }

    /**
     * 判斷服務是否存活
     *
     * @param candidate
     * @return
     */
    boolean isServiceAlive(Service candidate) {
        if (candidate == null) {
            return false;
        }
        ActivityManager manager = (ActivityManager) candidate.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningServiceInfo> services = manager.getRunningServices(Integer.MAX_VALUE);
        if (services == null) {
            return false;
        }
        for (ActivityManager.RunningServiceInfo service : services) {
            if (candidate.getClass().getName().equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判斷activity是否存活
     *
     * @param a
     * @return
     */
    boolean isActivityAlive(Activity a) {
        if (a == null) {
            return false;
        }
        if (a.isFinishing()) {
            return false;
        }
        return true;
    }

    /**
     * 判斷fragment是否存活
     *
     * @param fragment
     * @return
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    boolean isFragmentAlive(Fragment fragment) {
        boolean ret = isActivityAlive(fragment.getActivity());
        if (!ret) {
            return false;
        }
        if (fragment.isDetached()) {
            return false;
        }
        return true;
    }

    /**
     * 判斷fragment是否存活
     *
     * @param fragment
     * @return
     */
    boolean isV4FragmentAlive(android.support.v4.app.Fragment fragment) {
        boolean ret = isActivityAlive(fragment.getActivity());
        if (!ret) {
            return false;
        }
        if (fragment.isDetached()) {
            return false;
        }
        return true;
    }

    /**
     * 判斷是否存活
     *
     * @param context
     * @return
     */
    boolean isContextAlive(Context context) {
        if (context instanceof Service) {
            return isServiceAlive((Service) context);
        } else if (context instanceof Activity) {
            return isActivityAlive((Activity) context);
        }
        return true;
    }
}

我前面說了一句很拗口的話:“通過請求網絡的真正發起者其自身的方法獲取其自身的狀態來決定Callback是否需要繼續執行回調”,看了這個類之後,是不是有柳暗花明又一村的感覺?這個類就是對所持有的真正的網絡發起者的狀態進行判斷,幫助Callback判斷是否需要繼續執行回調,其本身也是一個弱引用,所以相對來說,其持有的對象更容易被回收,被回收後,Callback也不會在執行回調流程了(看isAlive方法)。

接下來就是我們實現SCallback的實際參與回調的RCallback

public abstract class RCallback<T extends BaseResponseBean> extends SCallback<T> {

    public RCallback(@NonNull Object host) {
        super(host);
    }

    @Override
    public final void onResponse(Call<T> call, Response<T> response) {
        //請求失敗
        if (!response.isSuccessful()) {
            onFailure(call, new IllegalArgumentException("Request data failed"));
            return;
        }
        T baseBen = response.body();
        //數據爲空
        if (null == baseBen) {
            onFailure(call, new IllegalArgumentException("Invalid data returned by the server"));
            return;
        }

        boolean isTokenError = TextUtils.equals(baseBen.getCode(), ErrorCode.ERROR_CODE_TOKEN_ERROR);
        //token錯誤或過期
        if (isTokenError) {
            ToolToast.showShort(baseBen.getMessage());
            Appcontext.assistApp.backToLogin();
        }
        //請求的發起者是否還合適接收回調
        if (checkCanceled(call)) {
            return;
        }
        //回調數據
        success(baseBen);
    }

    @Override
    public final void onFailure(Call<T> call, Throwable t) {
        Utils.handleException(t);
        if (checkCanceled(call)) {
            return;
        }
        String errorMessage;
        if (!ToolNetwork.getInstance().isAvailable()) {
            errorMessage = "網絡未連接,請檢查!";
        } else if (t instanceof UnknownHostException) {
            errorMessage = "服務器地址無法解析!";
        } else if (t instanceof HttpException) {
            errorMessage = "服務器錯誤!";
        } else if (t instanceof SocketTimeoutException) {
            errorMessage = "服務器連接超時!";
        } else if (t instanceof IOException) {
            errorMessage = "服務器連接失敗!";
        } else {
            if (t instanceof IllegalArgumentException) {
                errorMessage = t.getMessage();
            } else {
                errorMessage = "未知錯誤";
            }
        }
        failure(errorMessage);
    }

}

RCallback是對SCallback的擴展,可以根據不同項目和需求實現不同的SCallback,這裏面我們主要看onResponse方法和onFailure方法。他們都有一個共同的邏輯處理: if (checkCanceled(call)) {
return;
}。這個checkCanceled方法剛纔我們已經討論過了,到了這一步,Callback就解決了**“通過請求網絡的真正發起者其自身的方法獲取其自身的狀態來決定Callback是否需要繼續執行回調”**的問題。

前面還說了一有一個很彆扭的問題,就是BaseMVPFragment其實已經實現了BaseView,然而我們的LoginFragment也實現了一個實現至BaseView的BaseLoginView。在這個環節上一直讓我很困惑:一個已經實現了“某個“接口的類又去實現了一個繼承至”某個“接口的類,我感覺這裏肯定是有問題的,可是自己又說不上來問題到底在哪裏,希望有精通設計模式的高手指點迷津,感激不盡。

三、結語

其實標題所說的是todoMVP的嘗試和擴展,但看到這裏,大家可能並沒有看到太多todoMVP,這絕對是我水平有限的原因。在這裏我所闡述的,就是在實際使用todoMVP模式的過程中,爲了適應項目本身,而不得不讓todoMVP模式做出了一些調整,比如Presenter的調整,比如Repository的調整,而這個調整的過程並不是像現在這篇文章裏寥寥幾百字就能闡述完成的,我前面列出的爲了解決NPE的三個方法我都一一實踐過。在這裏還是要強調我前面第一篇文章裏說過的話:沒有最好的架構或設計模式,只有最適合的架構或設計模式

MVP模式系列的探討到此暫時搞一個段落吧,我可能需要花更多的時間去回憶去分析以前的所有實踐過程,等哪一天徹底參透了MVP的種種,再來和大家一起分享吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章