Android mvp 詳解(下)

原文鏈接:http://www.jianshu.com/p/0590f530c617

5. 最佳實踐

好了終於要點講自己的東西了,有點小激動。下面這些僅表示個人觀點,非一定之規,各位看官按需取用,有說的不對的,敬請諒解。關於命名規範可以參考我的另一篇文章“Android 編碼規範”。老規矩先上圖:


MVPBestPractice 思維導圖


在參考了 kenjuwagatsuma MVP Architecture in Android Development Saúl Molinero A useful stack on android #1, architecture 之後,我決定採用如下的分層方案來構建這個演示Demo,如下:


分層架構方案


總體架構可以被分成四個部分 :
Presentation:負責展示圖形界面,並填充數據,該層囊括了 View 和 Presenter (上圖所示的Model我理解爲 ViewModel -- 爲 View 提供數據的 Model,或稱之爲 VO -- View Object)。
Domain:負責實現app的業務邏輯,該層中由普通的Java對象組成,一般包括 Usecases 和 Business Logic。
Data:負責提供數據,這裏採用了 Repository 模式,Repository 是倉庫管理員,Domain 需要什麼東西只需告訴倉庫管理員,由倉庫管理員把東西拿給它,並不需要知道東西實際放在哪。Android 開發中常見的數據來源有,RestAPI、SQLite數據庫、本地緩存等。
Library:負責提供各種工具和管理第三方庫,現在的開發一般離不開第三方庫(當然可以自己實現,但是不要重複造輪子不是嗎?),這裏建議在統一的地方管理(那就是建一個單獨的 module),儘量保證和 Presentation 層分開。


AndroidStudio 中構建項目

5.1. 關於包結構劃分

一個項目是否好擴展,靈活性是否夠高,包結構的劃分方式佔了很大比重。很多項目裏面喜歡採用按照特性分包(就是Activity、Service等都分別放到一個包下),在模塊較少、頁面不多的時候這沒有任何問題;但是對於模塊較多,團隊合作開發的項目中,這樣做會很不方便。所以,我的建議是按照模塊劃分包結構。其實這裏主要是針對 Presentation 層了,這個演示 Demo 我打算分爲四個模塊:登錄,首頁,查詢天氣和我的(這裏僅僅是爲了演示需要,具體如何劃分模塊還得根據具體的項目,具體情況具體分析了)。劃分好包之後如下圖所示:


包結構劃分

5.2. 關於res拆分

功能越來越多,項目越做越大,導致資源文件越來越多,雖然通過命名可以對其有效歸類(如:通過添加模塊名前綴),但文件多了終究不方便。得益於 Gradle,我們也可以對 res 目錄進行拆分,先來看看拆分後的效果:


按模塊拆分 res 目錄


注意:resource 目錄的命名純粹是個人的命名偏好,該目錄的作用是用來存放那些不需要分模塊放置的資源。
res 目錄的拆分步驟如下:
1) 首先打開 module 的 build.gradle 文件


res 拆分 Step1


2) 定位到 defaultConfig {} 與 buildTypes {} 之間


res 拆分 Step2.png


3) 在第二步定位處編輯輸入 sourceSets {} 內容,具體內容如下:

sourceSets {
    main {
        manifest.srcFile 'src/main/AndroidManifest.xml'
        java.srcDirs = ['src/main/java','.apt_generated']
        aidl.srcDirs = ['src/main/aidl','.apt_generated']
        assets.srcDirs = ['src/main/assets']
        res.srcDirs =
        [
                'src/main/res/home',
                'src/main/res/login',
                'src/main/res/mine',
                'src/main/res/weather',
                'src/main/res/resource',
                'src/main/res/'

        ]
    }
}

4) 在 res 目錄下按照 sourceSets 中的配置建立相應的文件夾,將原來 res 下的所有文件(夾)都移動到 resource 目錄下,並在各模塊中建立 layout 等文件夾,並移入相應資源,最後 Sync Project 即可。

5.3. 怎麼寫 Model

這裏的 Model 其實貫穿了我們項目中的三個層,Presentation、Domain 和 Data。暫且稱之爲 Model 吧,這也我將提供 Repository 功能的層稱之爲 Data Layer 的緣故(有些稱這一層爲 Model Layer)。

首先,談談我對於 Model 是怎麼理解的。應用都離不開數據,而這些數據來源有很多,如網絡、SQLite、文件等等。一個應用對於數據的操作無非就是:獲取數據、編輯(修改)數據、提交數據、展示數據這麼幾類。從分層的思想和 JavaEE 開發中積累的經驗來看,我覺得 Model 中的類需要分類。從功能上來劃分,可以分出這麼幾類:
VO(View Object):視圖對象,用於展示層,它的作用是把某個指定頁面(或組件)的所有數據封裝起來。
DTO(Data Transfer Object):數據傳輸對象,這個概念來源於 JavaEE 的設計模式,原來的目的是爲了 EJB 的分佈式應用提供粗粒度的數據實體,以減少分佈式調用的次數,從而提高分佈式調用的性能和降低網絡負載,但在這裏,我泛指用於展示層與服務層之間的數據傳輸對象。
DO(Domain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。
PO(Persistent Object):持久化對象,它跟持久層(通常是關係型數據庫)的數據結構形成一一對應的映射關係,如果持久層是關係型數據庫,那麼,數據表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。

注意:關於vo、dto、do、po可以參考這篇文章-“領域驅動設計系列文章——淺析VO、DTO、DO、PO的概念、區別和用處

當然這些不一定都存在,這裏只是列舉一下,可以有這麼多分類,當然列舉的也不全。

其次,要搞清楚 Domain 層和 Data 層分別是用來做什麼的,然後才知道哪些 Model 該往 Data 層中寫,哪些該往 Domain 層中寫。
Data 層負責提供數據。
Data 層不會知道任何關於 Domain 和 Presentation 的數據。它可以用來實現和數據源(數據庫,REST API或者其他源)的連接或者接口。這個層面同時也實現了整個app所需要的實體類。
Domain 層相對於 Presentation 層完全獨立,它會實現應用的業務邏輯,並提供 Usecases。
Presentation 從 Domain 層獲取到的數據,我的理解就是 VO 了,VO 應該可以直接使用。

注意:這裏說的直接使用是指不需要經過各種轉換,各種判斷了,如 Activity 中某個控件的顯示隱藏是根據 VO 中的 visibility 字段來決定,那麼這個最好將 visibility 作爲 int 型,而且,取值爲VISIBLE/INVISIBLE/GONE,或者至少是 boolean 型的。

注意:這裏所謂的業務邏輯可能會於 Presenter 的功能概念上有點混淆。打個比方,假如 usecase 接收到的是一個 json 串,裏面包含電影的列表,那麼把這個 json 串轉換成 json 以及包裝成一個 ArrayList,這個應當是由 usecase 來完成。而假如 ArrayList 的 size 爲0,即列表爲空,需要顯示缺省圖,這個判斷和控制應當是由 Presenter 完成的。(上述觀點參考自:Saúl Molinero

最後,就是關於 Data 層,採用的 Repository 模式,建議抽象出接口來,Domain 層需要感知數據是從哪裏取出來的。

5.4. 怎麼寫 View

先區分一下Android View、View、界面的區別
Android View: 指的是繼承自android.view.View的Android組件。
View:接口和實現類,接口部分用於由 Presenter 向 View 實現類通信,可以在 Android 組件中實現它。一般最好直接使用 Activity,Fragment 或自定義 View。
界面:界面是面向用戶的概念。比如要在手機上進行界面間切換時,我們在代碼中可以通過多種方式實現,如 Activity 到 Activity 或一個 Activity 內部的 Fragment/View 進行切換。所以這個概念基於用戶的視覺,包括了所有 View 中能看到的東西。

那麼該怎麼寫 View 呢?

在 MVP 中 View 是很薄的一層,裏面不應該有業務邏輯,所以一般只提供一些 getter 和 setter 方法,供 Presenter 操作。關於 View,我有如下建議:

  1. 簡單的頁面中直接使用 Activity/Fragment 作爲 View 的實現類,然後抽取相應的接口
  2. 在一些有 Tab 的頁面中,可以使用 Activity + Fragment ( + ViewPager) 的方式來實現,至於 ViewPager,視具體情況而定,當然也可以直接 Activity + ViewPager 或者其他的組合方式
  3. 在一些包含很多控件的複雜頁面中,那麼建議將界面拆分,抽取自定義 View,也就是一個 Activity/Fragment 包含多個 View(實現多個 View 接口)

5.5. 怎麼寫 Presenter

Presenter 是 Android MVP 實現中爭論的焦點,上篇中介紹了多種“MVP 框架”,其實都是圍繞着Presenter應該怎麼寫。有一篇專門介紹如何設計 Presenter 的文章(Modeling my presentation layer),個人感覺寫得不錯,這裏借鑑了裏面不少的觀點,感興趣的童鞋可以去看看。下面進入正題。
爲什麼寫 Presenter 會這麼糾結,我認爲主要有以下幾個問題:

  1. 我們將 Activity/Fragment 視爲 View,那麼 View 層的編寫是簡單了,但是這有一個問題,當手機的狀態發生改變時(比如旋轉手機)我們應該如何處理Presenter對象,那也就是說 Presenter 也存在生命週期,並且還要“手動維護”(別急,這是引起來的,下面會細說)
  2. Presenter 中應該沒有 Android Framework 的代碼,也就是不需要導 Framework 中的包,那麼問題來了,頁面跳轉,顯示對話框這些情況在 Presenter 中該如何完成
  3. 上面說 View 的時候提到複雜的頁面建議通過抽取自定義 View 的方式,將頁面拆分,那麼這個時候要怎麼建立對應的 Presenter 呢
  4. View 接口是可以有多個實現的,那我們的 Presenter 該怎麼寫呢

好,現在我將針對上面這些問題一一給出建議。

5.5.1. 關於 Presenter 生命週期的問題

先看圖(更詳細講解可以看看這篇文章Presenter surviving orientation changes with Loaders


Presenter生命週期


如上圖所示,方案1和方案2都不夠優雅(這也是很多“MVP 框架”採用的實現方案),而且並不完善,只適用於一些場景。而方案3,讓人耳目一新,看了之後不禁想說 Loader 就是爲 Presenter 準備的啊。這裏我們抓住幾個關鍵點就好了:

  • Loader 是 Android 框架中提供的
  • Loader 在手機狀態改變時是不會被銷燬
  • Loader 的生命週期是是由系統控制的,會在Activity/Fragment不再被使用後由系統回收
  • Loader 與 Activity/Fragment 的生命週期綁定,所以事件會自己分發
  • 每一個 Activity/Fragment 持有自己的 Loader 對象的引用
  • 具體怎麼用,在 Antonio Gutierrez 的文章已經闡述的很明白,我就不再贅述了

好吧,我有一點要補充,上面說的方案1和方案2不是說就沒有用了,還是視具體情況而定,如果沒有那麼多複雜的場景,那麼用更簡單的方案也未嘗不可。能解決問題就好,不要拘泥於這些條條框框...(話說,咱這不是爲了追求完美嗎,哈哈)

5.5.2. 關於頁面跳轉和顯示Dialog

首先說說頁面跳轉,前一陣子忙着重構公司的項目,發現項目中很多地方使用 startActivity() 和使用 Intent 的 putExtra() 顯得很亂;更重要的是從 Intent 中取數據的時候需要格外小心——類型要對應,key 要寫對,不然輕則取不到數據,重則 Crash。還有一點,就是當前 Activity/Fragment 必須要知道目標 Activity 的類名,這裏耦合的很嚴重,有沒有。當時就在想這是不是應該封裝一下啊,或者有更好的解決方案。於是,先在網上搜了一下,知乎上有類似的提問,有人建議寫一個 Activity Router(Activity 路由表)。嗯,正好和我的思路類似,那就開幹。

我的思路很簡單,在 util 包中定義一個 NavigationManager 類,在該類中按照模塊使用註釋先分好區塊(爲什麼要分區塊,去看看我的 “Android 編碼規範”)。然後爲每個模塊中的 Activity 該如何跳轉,定義一個靜態方法。

如果不需要傳遞數據的,那就很簡單了,只要傳入調用者的 Context,直接 new 出 Intent,調用該 Context 的 startActivity() 方法即可。代碼如下:


導航管理類-跳轉系統頁面

導航管理類-跳轉不需要傳遞數據的頁面

如果需要傳遞數據呢?剛纔說了,使用 Bundle 或者 putExtra() 這種方式很不優雅,而且容易出錯(那好,你個給優雅的來看看,哈哈)。確實,我沒想到比較優雅的方案,在這裏我提供一個粗糙的方案,僅供大家參考一下,如有你有更好的,那麻煩也和我分享下。

我的方案是這樣的,使用序列化對象來傳遞數據(建議使用 Parcelable,不要偷懶去用 Serializable,這個你懂的)。爲需要傳遞數據的 Activity 新建一個實現了 Parcelable 接口的類,將要傳遞的字段都定義在該類中。其他頁面需要跳轉到該 Activity,那麼就需要提供這個對象。在目標 Activity 中獲取到該對象後,那就方便了,不需要去找對應的 key 來取數據了,反正只要對象中有的,你就能直接使用。

注意:這裏我建議將序列化對象中的所有成員變量都定義爲 public 的,一來,可以減少代碼量,主要是爲了減少方法數(雖說現在對於方法數超 64K 有比較成熟的 dex 分包方案,但是儘量不超不是更好);二來,通過對象的 public 屬性直接讀寫比使用 getter/setter 速度要快(聽說的,沒有驗證過)。

注意:這裏建議在全局常量類(沒有,那就定義一個,下面會介紹)中定義一個唯一的 INTENT_EXTRA_KEY,往 Bundle 中存和取得時候都用它,也不用去爲命名 key 費神(命名從來不簡單,不是嗎),取的時候也不用思考是用什麼 key 存的,簡單又可以避免犯錯。

具體如下圖所示:


導航管理類-跳轉需要傳遞數據的頁面

導航管理類-傳遞數據

導航管理類-獲取傳遞的數據


導航管理類代碼如下:

//==========邏輯方法==========
    public static <T> T getParcelableExtra(Activity activity) {
        Parcelable parcelable = activity.getIntent().getParcelableExtra(NavigateManager.PARCELABLE_EXTRA_KEY);
        activity = null;
        return (T)parcelable;
    }

    private static void overlay(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
        Intent intent = new Intent(context, targetClazz);
        setFlags(intent, flags);
        putParcelableExtra(intent, parcelable);
        context.startActivity(intent);
        context = null;
    }

    private static void overlay(Context context, Class<? extends Activity> targetClazz, Parcelable parcelable) {
        Intent intent = new Intent(context, targetClazz);
        putParcelableExtra(intent, parcelable);
        context.startActivity(intent);
        context = null;
    }

    private static void overlay(Context context, Class<? extends Activity> targetClazz, Serializable serializable) {
        Intent intent = new Intent(context, targetClazz);
        putSerializableExtra(intent, serializable);
        context.startActivity(intent);
        context = null;
    }

    private static void overlay(Context context, Class<? extends Activity> targetClazz) {
        Intent intent = new Intent(context, targetClazz);
        context.startActivity(intent);
        context = null;
    }

    private static void forward(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
        Intent intent = new Intent(context, targetClazz);
        setFlags(intent, flags);
        intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);
        context.startActivity(intent);
        if (isActivity(context)) return;
        ((Activity)context).finish();
        context = null;
    }

    private static void forward(Context context, Class<? extends Activity> targetClazz, Parcelable parcelable) {
        Intent intent = new Intent(context, targetClazz);
        putParcelableExtra(intent, parcelable);
        context.startActivity(intent);
        if (isActivity(context)) return;
        ((Activity)context).finish();
        context = null;
    }

    private static void forward(Context context, Class<? extends Activity> targetClazz, Serializable serializable) {
        Intent intent = new Intent(context, targetClazz);
        putSerializableExtra(intent, serializable);
        context.startActivity(intent);
        if (isActivity(context)) return;
        ((Activity)context).finish();
        context = null;
    }

    private static void forward(Context context, Class<? extends Activity> targetClazz) {
        Intent intent = new Intent(context, targetClazz);
        context.startActivity(intent);
        if (isActivity(context)) return;
        ((Activity)context).finish();
        context = null;
    }

    private static void startForResult(Context context, Class<? extends Activity> targetClazz, int flags) {
        Intent intent = new Intent(context, targetClazz);
        if (isActivity(context)) return;
        ((Activity)context).startActivityForResult(intent, flags);
        context = null;
    }

    private static void startForResult(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
        Intent intent = new Intent(context, targetClazz);
        if (isActivity(context)) return;
        putParcelableExtra(intent, parcelable);
        ((Activity)context).startActivityForResult(intent, flags);
        context = null;
    }

    private static void setResult(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
        Intent intent = new Intent(context, targetClazz);
        setFlags(intent, flags);
        putParcelableExtra(intent, parcelable);
        if (isActivity(context)) return;
        ((Activity)context).setResult(flags, intent);
        ((Activity)context).finish();
    }

    private static boolean isActivity(Context context) {
        if (!(context instanceof Activity)) return true;
        return false;
    }

    private static void setFlags(Intent intent, int flags) {
        if (flags < 0) return;
        intent.setFlags(flags);
    }

    private static void putParcelableExtra(Intent intent, Parcelable parcelable) {
        if (parcelable == null) return;
        intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);
    }

    private static void putSerializableExtra(Intent intent, Serializable serializable) {
        if (serializable == null) return;
        intent.putExtra(PARCELABLE_EXTRA_KEY, serializable);
    }

傳遞數據用的序列化對象,如下:

public class DishesStockVO implements Parcelable {

    public boolean isShowMask; 
    public int pageNum; 

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeByte(isShowMask ? (byte) 1 : (byte) 0);
        dest.writeInt(this.pageNum);
    }

    public DishesStockVO() {
    }

    protected DishesStockVO(Parcel in) {
        this.isShowMask = in.readByte() != 0;
        this.pageNum = in.readInt();
    }

    public static final Creator<DishesStockVO> CREATOR = new Creator<DishesStockVO>() {
        public DishesStockVO createFromParcel(Parcel source) {
            return new DishesStockVO(source);
        }

        public DishesStockVO[] newArray(int size) {
            return new DishesStockVO[size];
        }
    };

    @Override
    public String toString() {
        return "DishesStockVO{" +
                "isShowMask=" + isShowMask +
                ", pageNum=" + pageNum +
                '}';
    }
}

好像,還沒入正題。這裏再多說一句,beautifulSoup 寫了一篇文章,說的就是 Android 路由表框架的,可以去看看——“Android路由框架設計與實現”。

好了,回到主題,在 Presenter 中該如何處理頁面跳轉的問題。在這裏我建議簡單處理,在 View Interface 中定義好接口(方法),在 View 的實現類中去處理(本來就是它的責任,不是嗎?)。在 View 的實現類中,使用 NavigationManager 工具類跳轉,達到解耦的目的。如下圖所示:


對頁面跳轉的處理


顯示對話框
我在這裏採用和頁面跳轉的處理類似的方案,這也是 View 的責任,所以讓 View 自己去完成。這裏建議每個模塊都定義一個相應的 XxxDialogManager 類,來管理該模塊所有的彈窗,當然對於彈窗本來就不多的,那就直接在 util 包中定義一個 DialogManager 類就好了。如下圖:


對顯示對話框的處理

5.5.3. 一個頁面多個View的問題

對於複雜頁面,一般建議拆成多個自定義 View,那麼這就引出一個問題,這時候是用一個 Presenter 好,還是定義多個 Presenter 好呢?我的建議是,每個 View Interface 對應一個 Presenter,如下圖所示:


一個頁面多個 View 處理

5.5.4. 一個View有兩個實現類的問題

有些時候會遇到這樣的問題,只是展示上有差別,兩個頁面上所有的操作都是一樣的,這就意味着 View Interface 是一樣的,只是有兩個實現類。

這個問題該怎麼處理,或許可以繼續使用同樣的Presenter並在另一個Android組件中實現View接口。不過這個界面似乎有更多的功能,那要不要把這些新功能加進這個Presenter呢?這個視情況而定,有多種方案:一是將Presenter整合負責不同操作,二是寫兩個Presenter分別負責操作和展示,三是寫一個Presenter包含所有操作(在兩個View相似時)。記住沒有完美的解決方案,編程的過程就是讓步的過程。(參考自:Christian Panadero PaNaVTEC Modeling my presentation layer
如下圖所示:


一個 View 多個實現類處理

5.6. 關於 RestAPI

一般項目當中會用到很多和服務器端通信用的接口,這裏建議在每個模塊中都建立一個 api 包,在該包下來統一處理該模塊下所有的 RestAPI。
如下圖所示:


統一管理 RestAPI


對於網絡請求之類需要異步處理的情況,一般都需要傳入一個回調接口,來獲取異步處理的結果。對於這種情況,我建議參考 onClick(View v) {} 的寫法。那就是爲每一個請求編一個號(使用 int 值),我稱之爲 taskId,可以將該編號定義在各個模塊的常量類中。然後在回調接口的實現類中,可以在回調方法中根據 taskId 來統一處理(一般是在這裏分發下去,分別調用不同的方法)。
如下圖所示:


定義 taskId

異步任務回調處理

5.6. 關於項目中的常量管理

Android 中不推薦使用枚舉,推薦使用常量,我想說說項目當中我一般是怎麼管理常量的。
靈感來自 R.java 類,這是由項目構建工具自動生成並維護的,可以進去看看,裏面是一堆的靜態內部類,如下圖:


Android 中的 R 文件


看到這,可能大家都猜到了,那就是定義一個類來管理全局的常量數據,我一般喜歡命名爲 C.java。這裏有一點要注意,我們的項目是按模塊劃分的包,所以會有一些是該模塊單獨使用的常量,那麼這些最好不要寫到全局常量類中,否則會導致 C 類膨脹,不利於管理,最好是將這些常量定義到各個模塊下面。如下圖所示:


全局常量 C 類

5.7. 關於第三方庫

Android 開發中不可避免要導入很多第三方庫,這裏我想談談我對第三方庫的一些看法。關於第三方庫的推薦我就不做介紹了,很多專門說這方面的文章。

5.7.1. 挑選第三方庫的一些建議

  1. 項目中確實需要(這不是廢話嗎?用不着,我要它幹嘛?呵呵,建議不要爲了解決一個小小的問題導入一個大而全的庫)
  2. 使用的人要多(大家都在用的一般更新會比較快,出現問題解決方案也多)
  3. 效率和體量的權衡(如果效率沒有太大影響的情況下,我一般建議選擇體量小點的,如,Gson vs Jackson,Gson 勝出;還是 65K 的問題)

5.7.2. 使用第三方庫儘量二次封裝

爲什麼要二次封裝?
爲了方便更換,說得稍微專業點爲了降低耦合。
有很多原因可能需要你替換項目中的第三方庫,這時候如果你是經過二次封裝的,那麼很簡單,只需要在封裝類中修改一下就可以了,完全不需要去全局檢索代碼。
我就遇到過幾個替換第三方庫的事情:

  1. 替換項目中的統計埋點工具
  2. 替換網絡框架
  3. 替換日誌工具

那該怎麼封裝呢?
一般的,如果是一些第三方的工具類,都會提供一些靜態方法,那麼這個就簡單了,直接寫一個工具類,提供類似的靜態方法即可(就是用靜態工廠模式)。
如下代碼所示,這是對系統 Log 的簡單封裝:

/**
 * Description: 企業中通用的Log管理
 * 開發階段LOGLEVEL = 6
 * 發佈階段LOGLEVEL = -1
 */

public class Logger {

    private static int LOGLEVEL = 6;
    private static int VERBOSE = 1;
    private static int DEBUG = 2;
    private static int INFO = 3;
    private static int WARN = 4;
    private static int ERROR = 5;

    public static void setDevelopMode(boolean flag) {
        if(flag) {
            LOGLEVEL = 6;
        } else {
            LOGLEVEL = -1;
        }
    }

    public static void v(String tag, String msg) {
        if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
            Log.v(tag, msg);
        }
    }

    public static void d(String tag, String msg) {
        if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
            Log.d(tag, msg);
        }
    }

    public static void i(String tag, String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
            Log.i(tag, msg);
        }
    }

    public static void w(String tag, String msg) {
        if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
            Log.w(tag, msg);
        }
    }

    public static void e(String tag, String msg) {
        if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
            Log.e(tag, msg);
        }
    }

}

現在如果想替換爲 orhanobut Logger,那很簡單,代碼如下:

/**
 * Description: 通用的Log管理工具類
 * 開發階段LOGLEVEL = 6
 * 發佈階段LOGLEVEL = -1
 */

public class Logger {

    public static String mTag = "MVPBestPractice";
    private static int LOGLEVEL = 6;
    private static int VERBOSE = 1;
    private static int DEBUG = 2;
    private static int INFO = 3;
    private static int WARN = 4;
    private static int ERROR = 5;

    static {
        com.orhanobut.logger.Logger
                .init(mTag)                       // default PRETTYLOGGER or use just init()
                .setMethodCount(3)                // default 2
                .hideThreadInfo()                 // default shown
                .setLogLevel(LogLevel.FULL);      // default LogLevel.FULL
    }

    public static void setDevelopMode(boolean flag) {
        if(flag) {
            LOGLEVEL = 6;
            com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.FULL);
        } else {
            LOGLEVEL = -1;
            com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.NONE);
        }
    }

    public static void v(@NonNull String tag, String msg) {
        if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.v(tag, msg);
            com.orhanobut.logger.Logger.t(tag).v(msg);
        }
    }

    public static void d(@NonNull String tag, String msg) {
        if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.d(tag, msg);
            com.orhanobut.logger.Logger.t(tag).d(msg);
        }
    }

    public static void i(@NonNull String tag, String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.t(tag).i(msg);
        }
    }

    public static void w(@NonNull String tag, String msg) {
        if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.w(tag, msg);
            com.orhanobut.logger.Logger.t(tag).w(msg);
        }
    }

    public static void e(@NonNull String tag, String msg) {
        if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.e(tag, msg);
            com.orhanobut.logger.Logger.t(tag).e(msg);
        }
    }

    public static void e(@NonNull String tag, Exception e) {
        tag = checkTag(tag);
        if(LOGLEVEL > ERROR) {
//            Log.e(tag, e==null ? "未知錯誤" : e.getMessage());
            com.orhanobut.logger.Logger.t(tag).e(e == null ? "未知錯誤" : e.getMessage());
        }
    }

    public static void v(String msg) {
        if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
//            Log.v(mTag, msg);
            com.orhanobut.logger.Logger.v(msg);
        }
    }

    public static void d(String msg) {
        if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
//            Log.d(mTag, msg);
            com.orhanobut.logger.Logger.d(msg);
        }
    }

    public static void i(String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
//            Log.i(mTag, msg);
            com.orhanobut.logger.Logger.i(msg);
        }
    }

    public static void w(String msg) {
        if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
//            Log.w(mTag, msg);
            com.orhanobut.logger.Logger.v(msg);
        }
    }

    public static void e(String msg) {
        if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
//            Log.e(mTag, msg);
            com.orhanobut.logger.Logger.e(msg);
        }
    }

    public static void e(Exception e) {
        if(LOGLEVEL > ERROR) {
//            Log.e(mTag, e==null ? "未知錯誤" : e.getMessage());
            com.orhanobut.logger.Logger.e(e == null ? "未知錯誤" : e.getMessage());
        }
    }

    public static void wtf(@NonNull String tag, String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.t(tag).wtf(msg);
        }
    }

    public static void json(@NonNull String tag, String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.t(tag).json(msg);
        }
    }

    public static void xml(@NonNull String tag, String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
            tag = checkTag(tag);
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.t(tag).xml(msg);
        }
    }

    public static void wtf(String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.wtf(msg);
        }
    }

    public static void json(String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.json(msg);
        }
    }

    public static void xml(String msg) {
        if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
//            Log.i(tag, msg);
            com.orhanobut.logger.Logger.xml(msg);
        }
    }

    private static String checkTag(String tag) {
        if (TextUtils.isEmpty(tag)) {
            tag = mTag;
        }
        return tag;
    }

這裏是最簡單的一些替換,如果是替換網絡框架,圖片加載框架之類的,可能要多費點心思去封裝一下,這裏可以參考“門面模式”。(在這裏就不展開來講如何對第三庫進行二次封裝了,以後有時間專門寫個帖子)

5.7.3. 建立單獨的 Module 管理所有的第三庫

原因前面已經說過了,而且操作也很簡單。網上有不少拆分 Gradle 文件的方法,講的都很不錯。那我們就先從最簡單的做起,趕快行動起來,把項目中用到的第三方庫都集中到 Library Module 中來吧。

5.8. MVP vs MVVM

關於 MVP 和 MVVM 我只想說一句,它們並不是相斥的。具體它們是怎麼不相斥的,markzhai 的這篇文章“MVPVM in Action, 誰告訴你MVP和MVVM是互斥的”說得很詳細。

5.9. Code

抱歉,要食言了,AndroidStudio 出了點問題,代碼還沒寫完,代碼估計要這週末才能同步到 GitHub 上了,目前只上傳了一個空框架。

5.10. 小結

歷時三天的 MVP 總結,總算要告一段落了。前期斷斷續續地花了將近一週左右零散的時間去調研 MVP,直到正式開始碼字的時候才發現準備的還不夠。看了很多文章,有觀點一致的,也有觀點很不一致的。最關鍵的是,自己對於 MVP 還沒有比較深刻的認知,所以在各種觀點中取捨花了很長時間。
這算得上是我第一次真正意義上的寫技術性的文章,說來慚愧,工作這麼長時間了,現在纔開始動筆。
總體來說,寫得並不盡如人意,套一句老話——革命尚未成功,同志仍需努力。這算是一次嘗試,希望以後會越寫越順暢。在這裏給各位堅持看到此處的看官們問好了,祝大家一同進步。(歡迎大家圍觀我的GitHub,週末更新,會漸漸提交更多有用的代碼的)

6. 進階與不足

鑑於本人能力有限,還有很多想寫的和該寫的內容沒有寫出來,很多地方表達的也不是很清晰。下面說一說我覺得還有哪些不足和下一步要進階的方向。

  1. 說好的“show me the code”,代碼呢?(再次抱歉了)
  2. 上篇當中關於各種 Presenter 方案只是做了簡單的羅列,並沒有仔細分析各個方案的優點和不足
  3. 沒有形成自己的框架(呵呵,好高騖遠了,但是夢想還是要有的...)
  4. 沒有單元測試(項目代碼都還沒有呢,提倡 TDD 不是,呵呵)
  5. 很多細節沒有介紹清楚(如關於Model、Domain、Entity 等概念不是很清晰)
  6. 很多引用的觀點沒有指明出處(如有侵權,馬上刪除)
    ......

最後想說一句,沒有完美的架構,沒有完美的框架,趕緊編碼吧!

7. 附錄

Android MVP 總結資料彙總
附上我的思維導圖:
MVPBestPractice.mmap
MVP總結.mmap
Presenter生命週期.mmap
怎麼寫Presenter.mmap

參考:
https://segmentfault.com/a/1190000003871577
http://www.open-open.com/lib/view/open1450008180500.html
http://www.myexception.cn/android/2004698.html
http://gold.xitu.io/entry/56cbf38771cfe40054eb3a34
http://kb.cnblogs.com/page/531834/
http://blog.zhaiyifan.cn/2016/03/16/android-new-project-from-0-p3/
http://www.open-open.com/lib/view/open1446377609317.html
http://my.oschina.net/mengshuai/blog/541314?fromerr=3J2TdbiW
http://gold.xitu.io/entry/56fcf1f75bbb50004d872e74
https://github.com/googlesamples/android-architecture/tree/todo-mvp-loaders/todoapp
http://blog.zhaiyifan.cn/2016/03/16/android-new-project-from-0-p3/
http://android.jobbole.com/82375/
http://blog.csdn.net/weizhiai12/article/details/47904135
http://android.jobbole.com/82051/
http://android.jobbole.com/81153/
http://blog.chengdazhi.com/index.php/115
http://blog.chengdazhi.com/index.php/131
http://www.codeceo.com/article/android-mvp-practice.html
http://www.wtoutiao.com/p/h01nn2.html
http://blog.jobbole.com/71209/
http://www.cnblogs.com/tianzhijiexian/p/4393722.html
https://github.com/xitu/gold-miner/blob/master/TODO/things-i-wish-i-knew-before-i-wrote-my-first-android-app.md
http://gold.xitu.io/entry/56cd79c12e958a69f944984c
http://blog.yongfengzhang.com/cn/blog/write-code-that-is-easy-to-delete-not-easy-to/
http://kb.cnblogs.com/page/533808/


文/diygreen(簡書作者)
原文鏈接:http://www.jianshu.com/p/0590f530c617
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章