MVP在Android平臺上的應用

jopen 8年前發布 | 33K 次閱讀 安卓開發 Android開發 移動開發

  • 原文鏈接 : Introduction to Model-View-Presenter on Android
  • 原文作者 : konmik
  • 譯文出自 : 其他 http://konmik.github.io/introduction-to-model-view-presenter-on-android.html
  • 譯者 : MiJack
  • 校對者: MiJack
  • 狀態 : 校對完成
  • </ul> </div>

    =======

    Android平臺上MVP的介紹

    這篇文章向你介紹Android平臺上的MVP模式,從一個簡淺的例子開始實踐之路。文章也會介紹一個一個庫讓你在Android平臺上輕松的實現MVP

    簡單嗎?我怎么才能從中受益?

    什么是MVP?

    • View 層主要是用于展示數據并對用戶行為做出反饋。在Android平臺上,他可以對應為Activity, Fragment,View或者對話框。
    • Model 是數據訪問層,往往是數據庫接口或者服務器的API。
    • Presenter 層可以想View層提供來自數據訪問層的數據,除此以外,他也會處理一些后臺事務。

    在Android平臺上,MVP可以將后臺事務從Activity/View/Fragment中分離出來,讓它們獨立于大部分生命周期事件。這樣,一個應用將會變得簡單, 整個應用可靠性可以提高10倍,應用的代碼將會變短, 代碼的可維護性提高,開發者也為此感到高興。

    Android為什么需要MVP

    理由1:盡量簡單

    如果你還有讀過這篇文章,請閱讀它: Kiss原則 (Keep It Stupid Simple)

    • 大部分的安卓應用只使用View-Model結構
    • 程序員現在更多的是和復雜的View打交道而不是解決業務邏輯。

    當你在應用中只使用Model-View時,到最后,你會發現“所有的事物都被連接到一起”。

    如果這張圖看上去還不是很復雜,那么請你想象一下以下情況:每一個View在任意一個時刻都有可能出現或者消失。不要忘記View的保存和恢復,在臨時的view上掛載一個后臺任務。

    “所有的事物都被連接到一起”的替代品是一個萬能對象(god object)。

    god object是十分復雜的,他的每一個部分都不能重復利用,無法輕易的測試、或者調試和重構。

    With MVP

    使用MVP

    復雜的任務被分成細小的任務,并且很容易解決。越小的東西,bug越少,越容易debug,更好測試。在MVP模式下的View層將會變得簡單,所以即便是他請求數據的時候也不需要回調函數。View邏輯變成十分直接。

    理由2:后臺任務

    當你編寫一個Actviity、Fragment、自定義View的時候,你會把所有的和后臺任務相關的方法寫在一個靜態類或者外部類中。這樣,你的Task不再和Activity聯系在一起,這既不會導致內存泄露,也不依賴于Activity的重建。

    這里有若干種方法處理后臺任務,但是它們的可靠性都不及MVP。

    為什么它是可行的?

    這里有一張表格,用于展示在configuration改變、Activity 重啟、Out-Of-Memory時,不同的應用部分會發生什么?

    情景 1 情景 2 情景 3
    配置改變 Activity 重啟 進程重啟
    對話框 重置 重置 重置
    Activity, View, Fragment 保存/恢復 保存/恢復 保存/恢復
    Fragment with setRetainInstance(true) 無變化 保存/恢復 保存/恢復
    Static variables and threads 無變化 無變化 重置

    情景 1: 當用戶切換屏幕、更改語言設置或者鏈接外部的模擬器時,往往意味著設置改變。 相關更多請閱讀 這里

    情景 2:Activity的重啟發生在當用戶在開發者選項中選中了“Don’t keep activities”(“中文下為 不保留活動”)的復選框,然后另一個Activity在最頂上的時候。

    情景 3: 進程的重啟發生在應用運行在后臺,但是這個時候內存不夠的情況下。

    總結

    現在你可以發現,一個調用了setRetainInstance(true)的Fragment也不奏效,我們還是需要保存/恢復fragment的狀態,所以為簡化問題,我們暫不考慮上述情況的Fragment。 Occam’s razor

    配置改變, Activity重啟 進程重啟
    Activity, View, Fragment, DialogFragment 保存/恢復 保存/恢復
    Static variables and threads 無變化 重置

    現在,看上去更舒服了,我們只需要寫兩段代碼為了恢復應用:

    • 保存/恢復 for Activity, View, Fragment, DialogFragment;

    • 重啟后臺請求由于進程重啟

    第一個部分,用Android的API可以實現。第二個部分,就是Presenter的作用了。Presenter將會記住有哪些請求需要執行,當進程在執行過程中重啟時,Presenter將會出現執行它們。

    一個簡單的例子(no MVP)

    這個例子用于從遠程服務器加載數據并呈現,當發生異常時,會通過Toast提示。

    我推薦使用 RxJava 構建Presenter,因為這個庫更容易控制數據流。

    我想對創造如此簡單的API的伙計說聲謝謝,我把它用于 The Internet Chuck Norris Database

    無MVP的 例子00

    <br />public class MainActivity extends Activity {
        public static final String DEFAULT_NAME = "Chuck Norris";
    
        private ArrayAdapter<ServerAPI.Item> adapter;
        private Subscription subscription;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ListView listView = (ListView)findViewById(R.id.listView);
            listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
            requestItems(DEFAULT_NAME);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            unsubscribe();
        }
    
        public void requestItems(String name) {
            unsubscribe();
            subscription = App.getServerAPI()
                .getItems(name.split("\\s+")[0], name.split("\\s+")[1])
                .delay(1, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<ServerAPI.Response>() {
                    @Override
                    public void call(ServerAPI.Response response) {
                        onItemsNext(response.items);
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable error) {
                        onItemsError(error);
                    }
                });
        }
    
        public void onItemsNext(ServerAPI.Item[] items) {
            adapter.clear();
            adapter.addAll(items);
        }
    
        public void onItemsError(Throwable throwable) {
            Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
        }
    
        private void unsubscribe() {
            if (subscription != null) {
                subscription.unsubscribe();
                subscription = null;
            }
        }
    }

    有經驗的開發者會注意到這個例子有以下不妥:

    當用戶翻轉屏幕時候會開始請求,應用發起了過多的請求,將會是屏幕在切換的時候呈現空白的界面。

    當用戶頻繁的切換屏幕,這將會造成內存泄露,請求運行時,每一個回調將會持有MainActivity的引用,讓其保存在內存中。因此引起的OOM和應用反應遲緩,會引發應用的Crash。

    MVP模式下的 例子 01

    public class MainPresenter {
    
        public static final String DEFAULT_NAME = "Chuck Norris";
    
        private ServerAPI.Item[] items;
        private Throwable error;
    
        private MainActivity view;
    
        public MainPresenter() {
            App.getServerAPI()
                .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1])
                .delay(1, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<ServerAPI.Response>() {
                    @Override
                    public void call(ServerAPI.Response response) {
                        items = response.items;
                        publish();
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        error = throwable;
                        publish();
                    }
                });
        }
    
        public void onTakeView(MainActivity view) {
            this.view = view;
            publish();
        }
    
        private void publish() {
            if (view != null) {
                if (items != null)
                    view.onItemsNext(items);
                else if (error != null)
                    view.onItemsError(error);
            }
        }
    }

    從嚴格意義上來說,MainPresenter有三個事件處理線程: onNext , onError , onTakeView 。他們調用了 publish() 方法, onNextonError 的值將會在MainActivity中發布,而不是由onTakeView提供。

    public class MainActivity extends Activity {
    
        private ArrayAdapter<ServerAPI.Item> adapter;
    
        private static MainPresenter presenter;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ListView listView = (ListView)findViewById(R.id.listView);
            listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
    
            if (presenter == null)
                presenter = new MainPresenter();
            presenter.onTakeView(this);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            presenter.onTakeView(null);
            if (isFinishing())
                presenter = null;
        }
    
        public void onItemsNext(ServerAPI.Item[] items) {
            adapter.clear();
            adapter.addAll(items);
        }
    
        public void onItemsError(Throwable throwable) {
            Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    MainActivty構建了MainPresenter,將其維持在onCreate/onDestroy周期外,MainActivity持有MainPresenter的靜態引用,所以每一個進程由于OOM重啟時,MainActivity可以確認Presenter是否仍然存在,必要時創建。

    當然,確認和使用靜態變量可能是代碼變得臃腫,稍后我們會告訴你如何好看些:。:)

    重要思路:

    • 示例程序不會在每次切換屏幕的時候都開始一個新的請求
    • 當進程重啟時,示例程序將會重新加載數據。
    • 當MainActivity銷毀時,MainPresenter不會持有MainActivity的引用,因此不會在切換屏幕的時候發生內存泄漏,而且沒必要去unsubscribe請求。

    Nucleus

    Nucleus是我從 Mortar 和 Keep It Stupid Simple 這篇文章得到的靈感而建立的庫。

    它有以下特征:

    • 它支持在View/Fragment/Activity的Bundle中保存/恢復Presenter的狀態,一個Presenter可以保存請求參數,以便之后重啟它們

    • 只需要一行代碼,它就可以直接將請求結果或者錯誤反饋給View,所以你不需要寫 != null 之類的核對代碼。

    • 它允許你可以有多個持有Presenter的實例。 不過你不能在用 Dagger 實例化的presenter中這樣使用(傳統方法).

    • 它可以用一行代碼快速的將View和Presenter綁定。

    • 它提供一些現成的基類,例如: NucleusView , NucleusFragment , NucleusSupportFragment , NucleusActivity . 你可以將他們的代碼拷貝出來改造出一個自己的類以利用Nucleus的presenter。

    • 支持在進程重啟后,自動重新發起請求,在 onDestroy 方法中,自動的退訂RxJava的訂閱。

    • 最后,它簡潔明了,每一個開發者都會理解,Presenter的驅動只用了180行代碼,RxJava用了230行代碼。

    使用了 Nucleus例 02

    public class MainPresenter extends RxPresenter<MainActivity> {
    
        public static final String DEFAULT_NAME = "Chuck Norris";
    
        @Override
        protected void onCreate(Bundle savedState) {
            super.onCreate(savedState);
    
            App.getServerAPI()
                .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1])
                .delay(1, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .compose(this.<ServerAPI.Response>deliverLatestCache())
                .subscribe(new Action1<ServerAPI.Response>() {
                    @Override
                    public void call(ServerAPI.Response response) {
                        getView().onItemsNext(response.items);
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        getView().onItemsError(throwable);
                    }
                });
        }
    }
    
    @RequiresPresenter(MainPresenter.class)
    public class MainActivity extends NucleusActivity<MainPresenter> {
    
        private ArrayAdapter<ServerAPI.Item> adapter;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ListView listView = (ListView)findViewById(R.id.listView);
            listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
        }
    
        public void onItemsNext(ServerAPI.Item[] items) {
            adapter.clear();
            adapter.addAll(items);
        }
    
        public void onItemsError(Throwable throwable) {
            Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    正如你看到的,跟上一個代碼相比,這個例子十分簡潔。Nucleus 可以構造/銷毀/變成 Presenter, 向Presenter中添加/分離 View ,并且自動向附加的view發送請求。。

    MainPresenter 的代碼比較短,因為它使用 deliverLatestCache() 的操作,延遲了由一個數據源發出所有的數據和錯誤,直到View可用。它還把數據緩存在內存中,以便它可以在Configuration change時可以被重用。

    MainActivity 的代碼比較短,因為主Presenter的創作由 NucleusActivity 管理。當你需要綁定一個Presenter的時候,只需要添加注解 @RequiresPresenter(MainPresenter.class) 。

    警告!注釋!在Android中,如果你使用注解,這是最好檢查以下這么做會不會降低性能。以我使用的’Galaxy S`(2010年設備)為例,處理此批注耗時不超過0.3毫秒。只在實例化view的時候才會發生,因此注解在這里對性能的影響可以忽略。

    更多例子

    一個擴展的例子,帶有請求參數的Presenter: Nucleus Example

    帶有單元測試的例子: Nucleus Example With Tests

    deliverLatestCache() 方法

    這個RxPresenter的工具方法有三個變種:

    • deliver() will just delay all onNext, onError and onComplete emissions until a View becomes available. Use it for cases when you’re doing a one-time request, like logging in to a web service. Javadoc

    • deliver() 只是推遲onNext、onError、onComplete的調用,直到視圖有效。使用它,你只需要一次請求,就像發起登陸web服務一樣。 Javadoc

    • deliverLatest() 當有新的的onNext值,將會舍棄原有的值,如果你有可更新的數據源,這將讓你去除那些不需要的數據。 Javadoc

    • deliverLatestCache() ,和 deliverLatest() 一樣,但除了它會在內存中保存最新的結果外,當View的另一個實例可用(例如:在配置更改的時候)時,還是會觸發一次。如果你不想組織請求在你的View中的保存/恢復事務(比方說,結果太大或者不能很容易地保存在Bundle中),這個方法可以讓用戶體驗更好。 Javadoc

    Presenter的生命周期

    相比Android組件,Presenter的生命周期更加簡短。

    • void onCreate(Bundle savedState) – 每一個Presenter構造時 . Javadoc

    • void onDestroy() – 用戶離開View時調用 . Javadoc

    • void onSave(Bundle state) – 在View的 onSaveInstanceState 方法中調用,用于持有Presenter的狀態. Javadoc

    • void onTakeView(ViewType view) – 在Activity或者Fragment的 onResume() 方法中或者 android.view.View#onAttachedToWindow() 調用. Javadoc

    • void onTakeView(ViewType view) – 在Activity或者Fragment的 onResume() 方法中或者 android.view.View#onAttachedToWindow() 調用. Javadoc

    • void onDropView() – 在Activity或者Fragment的 onPause() 方法中或者 android.view.View#onDetachedFromWindow() 調用. Javadoc

    View的回收與View棧

    通常來說,你的view(比如fragment或者自定義的view)在用戶的交互過程中掛載與解掛(attached and detached)都是隨機發生的。 這倒是不讓presenter在view每次解掛(detached)的時候都銷毀的一個啟發。你可以在任何時候掛載與解掛view,但是presenter可以在這些行為中幸存下來,繼續后臺的工作。

    這里還存在著一個關于View回收的問題:一個Fragment在Configuration change或者從stack中彈出的情況下,不知道自身有沒有解掛(detached)。

    默認只在Activity處于finish時,才在調用View的 onDetachedFromWindow() / onDestroy() 銷毀Presenter。

    所以,當你在常規的Activity生命周期內,銷毀View,你需要給給View一個銷毀Presenter的信號。在這里,公有方法 NucleusLayout.destroyPresenter() and NucleusFragment.destroyPresenter() 就派上用場了。

    例如,在我的項目中,下面的是我如何進行FragmentManager的 pop() 操作:

    fragment = fragmentManager.findFragmentById(R.id.fragmentStackContainer);
        fragmentManager.popBackStackImmediate();
        if (fragment instanceof NucleusFragment)
            ((NucleusFragment)fragment).destroyPresenter();

    在進行 replace Fragment棧和對處于底部的Fragment進行 push 操作時,你可能需要進行相同的操作。

    在View從Activity解掛(detached)時,您可能會選擇摧毀Presenter來避免問題的發生,但是,這將意味著當View解掛(detached)時,后臺任務無法繼續進行。

    所以這一節的 “view recycling”完全留你你自己考慮,也許有一天我會找到更好的解決辦法,如果你有一個辦法,請告訴我。

    最佳實踐

    在Presenter中保存你的請求參數。

    規則很簡單:Presenter的主要作用是管理請求。所以,View不應該自己處理或者重啟請求。從View中,我們可以看見,后臺事務不會消失,總是會返回結果或者錯誤, 而不是通過回調的方式

    <br />public class MainPresenter extends RxPresenter<MainActivity> {
    
        private String name = DEFAULT_NAME;
    
        @Override
        protected void onCreate(Bundle savedState) {
            super.onCreate(savedState);
            if (savedState != null)
                name = savedState.getString(NAME_KEY);
            ...
    
        @Override
        protected void onSave(@NonNull Bundle state) {
            super.onSave(state);
            state.putString(NAME_KEY, name);
        }
    }

    我推薦使用一個很棒的庫 Icepick 。在不使用運行時注解的前提下,它可以減少代碼量,并簡化應用程序邏輯 – 所有的事都在編譯過程中已經處理好了。這個庫和 ButterKnife 搭配是個不錯的選擇。

    public class MainPresenter extends RxPresenter<MainActivity> {
    
        @Icicle String name = DEFAULT_NAME;
    
        @Override
        protected void onCreate(Bundle savedState) {
            super.onCreate(savedState);
            Icepick.restoreInstanceState(this, savedState);
            ...
        }
        @Override
        protected void onSave(@NonNull Bundle state) {
            super.onSave(state);
            Icepick.saveInstanceState(this, state);
        }
    }

    如果你有不止一對請求參數,這個庫在不使用運行時注解的前提下。您可以創建 BasePresenter 并把 Icepick 到該類中,所有的子類將會自動保存標有 @Icicle 這一注解的變量,而你將不再需要去實現 OnSave 。這也適用于保存Activity,Fragment,View的狀態。

    在主線程中調用 onTakeView 進行即時查詢 Javadoc

    有時候,你要進行少量的數據查詢,如從數據庫中讀取少量的數據。雖然你可以很容易地用Nucleus創建一個可重啟的請求,你不必到處使用這個強大的工具。如果你在fragment創建的時候初始化一個后臺請求,即使只有幾毫秒,用戶也會看到一會兒的空白屏。因此,為了使代碼更短,用戶體驗更好,可以使用主線程。

    不要讓Presenter控制你的View

    這不是很好的工作方式 – 由于這種不自然的方式,應用程序邏輯變得太復雜。

    自然的方式是操作流由用戶發起,通過View,Presenter和Model,最后流向數據。畢竟,用戶將使用應用,用戶是控制應用程序的源頭。因此,控制應該從用戶開始而不是一些應用的內部結構。

    從view,到presenter到model是很直接的形式,很容易書寫這樣的代碼。你將得到以下序列: user -> view -> presenter -> model -> data 。但是,當控制流變成這樣時: user -> view -> presenter -> view -> presenter -> model -> data ,它只是違反KISS原則.

    Fragments?不好意思它是違背了這種自然操作流程的。它們太復雜。這里是一個非常好講訴Fragment的文章: 抨擊Android的Fragment 。fragment的替代者 Flow 并沒有簡化多少東西。

    MVC

    如果你對MVC(模型-View-控制器)-不要去使用。模型-View-控制器和MVP完全不同,不能解決接口開發者面對的問題。

    What is MVC?

    什么是MVC?

    • Model代表著應用程序的內部狀態。它可以負責存儲,當然也可以不考慮。

    • View是唯一的與MVP相同的部分 – 它用于將模型呈現在屏幕上,應用程序的一部分。

    • Controller表示輸入裝置,如鍵盤,鼠標或操縱桿。

    MVC在過去以鍵盤為驅動的應用中(比如游戲),是比較好的模式。沒有窗口和圖形用戶界面的交互——應用接受輸入(Controller),維持狀態(Model),產生輸出(View)。同樣,數據和控制的關系是這樣的。 controller -> model -> view 。這種模式是在Android絕對無用。

    這里有一些關于MVC的困惑。人們(Web開發人員)覺得他們使用MVC,而實際上,他們使用的MVP。許多Android開發者認為Controller是用于控制View的,所以他們試圖在創建View時,從視圖(View)中提取視圖邏輯,交由專門的控制器控制。我個人是沒有看出這種架構的好處。

    在數據復雜的項目中使用固定的數據結構

    在這方面, AutoValue 是十分好的庫,在它的描述中,你會發現一大堆好處,我建議你閱讀它。Android平臺上還有一個接口: AutoParcel 。其主要原因是,你可以四處傳遞,而不用關心是否在程序的某個地方被修改了。而且他們也是線程安全的。

    總結

    試試MVP吧,然后告訴你的朋友。:)

來自: http://www.devtf.cn/?p=567

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!