Android MVP 詳解(下)

5. 最佳實踐

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

MVPBestPractice 思維導(dǎo)圖

在參考了kenjuwagatsumaMVP Architecture in Android DevelopmentSaúl MolineroA useful stack on android #1, architecture之后,我決定采用如下的分層方案來構(gòu)建這個演示Demo,如下:

分層架構(gòu)方案

總體架構(gòu)可以被分成四個部分 :

Presentation:負責展示圖形界面,并填充數(shù)據(jù),該層囊括了 View 和 Presenter (上圖所示的Model我理解為 ViewModel -- 為 View 提供數(shù)據(jù)的 Model,或稱之為 VO -- View Object)。

Domain:負責實現(xiàn)app的業(yè)務(wù)邏輯,該層中由普通的Java對象組成,一般包括 Usecases 和 Business Logic。

Data:負責提供數(shù)據(jù),這里采用了 Repository 模式,Repository 是倉庫管理員,Domain 需要什么東西只需告訴倉庫管理員,由倉庫管理員把東西拿給它,并不需要知道東西實際放在哪。Android 開發(fā)中常見的數(shù)據(jù)來源有,RestAPI、SQLite數(shù)據(jù)庫、本地緩存等。

Library:負責提供各種工具和管理第三方庫,現(xiàn)在的開發(fā)一般離不開第三方庫(當然可以自己實現(xiàn),但是不要重復(fù)造輪子不是嗎?),這里建議在統(tǒng)一的地方管理(那就是建一個單獨的 module),盡量保證和 Presentation 層分開。

AndroidStudio 中構(gòu)建項目

5.1. 關(guān)于包結(jié)構(gòu)劃分

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

包結(jié)構(gòu)劃分

5.2. 關(guān)于res拆分

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

按模塊拆分 res 目錄

注意:resource 目錄的命名純粹是個人的命名偏好,該目錄的作用是用來存放那些不需要分模塊放置的資源。

res 目錄的拆分步驟如下:

1) 首先打開 module 的 build.gradle 文件

res 拆分 Step1

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

res 拆分 Step2.png

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

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 中的配置建立相應(yīng)的文件夾,將原來 res 下的所有文件(夾)都移動到 resource 目錄下,并在各模塊中建立 layout 等文件夾,并移入相應(yīng)資源,最后 Sync Project 即可。

5.3. 怎么寫 Model

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

首先,談?wù)勎覍τ?Model 是怎么理解的。應(yīng)用都離不開數(shù)據(jù),而這些數(shù)據(jù)來源有很多,如網(wǎng)絡(luò)、SQLite、文件等等。一個應(yīng)用對于數(shù)據(jù)的操作無非就是:獲取數(shù)據(jù)、編輯(修改)數(shù)據(jù)、提交數(shù)據(jù)、展示數(shù)據(jù)這么幾類。從分層的思想和 JavaEE 開發(fā)中積累的經(jīng)驗來看,我覺得 Model 中的類需要分類。從功能上來劃分,可以分出這么幾類:

VO(View Object):視圖對象,用于展示層,它的作用是把某個指定頁面(或組件)的所有數(shù)據(jù)封裝起來。

DTO(Data Transfer Object):數(shù)據(jù)傳輸對象,這個概念來源于 JavaEE 的設(shè)計模式,原來的目的是為了 EJB 的分布式應(yīng)用提供粗粒度的數(shù)據(jù)實體,以減少分布式調(diào)用的次數(shù),從而提高分布式調(diào)用的性能和降低網(wǎng)絡(luò)負載,但在這里,我泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對象。

DO(Domain Object):領(lǐng)域?qū)ο螅褪菑默F(xiàn)實世界中抽象出來的有形或無形的業(yè)務(wù)實體。

PO(Persistent Object):持久化對象,它跟持久層(通常是關(guān)系型數(shù)據(jù)庫)的數(shù)據(jù)結(jié)構(gòu)形成一一對應(yīng)的映射關(guān)系,如果持久層是關(guān)系型數(shù)據(jù)庫,那么,數(shù)據(jù)表中的每個字段(或若干個)就對應(yīng) PO 的一個(或若干個)屬性。

注意:關(guān)于vo、dto、do、po可以參考這篇文章-“領(lǐng)域驅(qū)動設(shè)計系列文章——淺析VO、DTO、DO、PO的概念、區(qū)別和用處

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

其次,要搞清楚 Domain 層和 Data 層分別是用來做什么的,然后才知道哪些 Model 該往 Data 層中寫,哪些該往 Domain 層中寫。

Data 層負責提供數(shù)據(jù)。

Data 層不會知道任何關(guān)于 Domain 和 Presentation 的數(shù)據(jù)。它可以用來實現(xiàn)和數(shù)據(jù)源(數(shù)據(jù)庫,REST API或者其他源)的連接或者接口。這個層面同時也實現(xiàn)了整個app所需要的實體類。

Domain 層相對于 Presentation 層完全獨立,它會實現(xiàn)應(yīng)用的業(yè)務(wù)邏輯,并提供 Usecases。

Presentation 從 Domain 層獲取到的數(shù)據(jù),我的理解就是 VO 了,VO 應(yīng)該可以直接使用。

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

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

最后,就是關(guān)于 Data 層,采用的 Repository 模式,建議抽象出接口來,Domain 層需要感知數(shù)據(jù)是從哪里取出來的。

5.4. 怎么寫 View

先區(qū)分一下Android View、View、界面的區(qū)別

Android View: 指的是繼承自android.view.View的Android組件。

View:接口和實現(xiàn)類,接口部分用于由 Presenter 向 View 實現(xiàn)類通信,可以在 Android 組件中實現(xiàn)它。一般最好直接使用 Activity,F(xiàn)ragment 或自定義 View。

界面:界面是面向用戶的概念。比如要在手機上進行界面間切換時,我們在代碼中可以通過多種方式實現(xiàn),如 Activity 到 Activity 或一個 Activity 內(nèi)部的 Fragment/View 進行切換。所以這個概念基于用戶的視覺,包括了所有 View 中能看到的東西。

那么該怎么寫 View 呢?

在 MVP 中 View 是很薄的一層,里面不應(yīng)該有業(yè)務(wù)邏輯,所以一般只提供一些 getter 和 setter 方法,供 Presenter 操作。關(guān)于 View,我有如下建議:

簡單的頁面中直接使用 Activity/Fragment 作為 View 的實現(xiàn)類,然后抽取相應(yīng)的接口

在一些有 Tab 的頁面中,可以使用 Activity + Fragment ( + ViewPager) 的方式來實現(xiàn),至于 ViewPager,視具體情況而定,當然也可以直接 Activity + ViewPager 或者其他的組合方式

在一些包含很多控件的復(fù)雜頁面中,那么建議將界面拆分,抽取自定義 View,也就是一個 Activity/Fragment 包含多個 View(實現(xiàn)多個 View 接口)

5.5. 怎么寫 Presenter

Presenter 是 Android MVP 實現(xiàn)中爭論的焦點,上篇中介紹了多種“MVP 框架”,其實都是圍繞著Presenter應(yīng)該怎么寫。有一篇專門介紹如何設(shè)計 Presenter 的文章(Modeling my presentation layer),個人感覺寫得不錯,這里借鑒了里面不少的觀點,感興趣的童鞋可以去看看。下面進入正題。

為什么寫 Presenter 會這么糾結(jié),我認為主要有以下幾個問題:

我們將 Activity/Fragment 視為 View,那么 View 層的編寫是簡單了,但是這有一個問題,當手機的狀態(tài)發(fā)生改變時(比如旋轉(zhuǎn)手機)我們應(yīng)該如何處理Presenter對象,那也就是說 Presenter 也存在生命周期,并且還要“手動維護”(別急,這是引起來的,下面會細說)

Presenter 中應(yīng)該沒有 Android Framework 的代碼,也就是不需要導(dǎo) Framework 中的包,那么問題來了,頁面跳轉(zhuǎn),顯示對話框這些情況在 Presenter 中該如何完成

上面說 View 的時候提到復(fù)雜的頁面建議通過抽取自定義 View 的方式,將頁面拆分,那么這個時候要怎么建立對應(yīng)的 Presenter 呢

View 接口是可以有多個實現(xiàn)的,那我們的 Presenter 該怎么寫呢

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

5.5.1. 關(guān)于 Presenter 生命周期的問題

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

Presenter生命周期

如上圖所示,方案1和方案2都不夠優(yōu)雅(這也是很多“MVP 框架”采用的實現(xiàn)方案),而且并不完善,只適用于一些場景。而方案3,讓人耳目一新,看了之后不禁想說 Loader 就是為 Presenter 準備的啊。這里我們抓住幾個關(guān)鍵點就好了:

Loader 是Android 框架中提供的

Loader 在手機狀態(tài)改變時是不會被銷毀

Loader 的生命周期是是由系統(tǒng)控制的,會在Activity/Fragment不再被使用后由系統(tǒng)回收

Loader 與 Activity/Fragment 的生命周期綁定,所以事件會自己分發(fā)

每一個 Activity/Fragment 持有自己的 Loader 對象的引用

具體怎么用,在Antonio Gutierrez的文章已經(jīng)闡述的很明白,我就不再贅述了

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

5.5.2. 關(guān)于頁面跳轉(zhuǎn)和顯示Dialog

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

我的思路很簡單,在 util 包中定義一個 NavigationManager 類,在該類中按照模塊使用注釋先分好區(qū)塊(為什么要分區(qū)塊,去看看我的 “Android 編碼規(guī)范”)。然后為每個模塊中的 Activity 該如何跳轉(zhuǎn),定義一個靜態(tài)方法。

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

導(dǎo)航管理類-跳轉(zhuǎn)系統(tǒng)頁面

導(dǎo)航管理類-跳轉(zhuǎn)不需要傳遞數(shù)據(jù)的頁面

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

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

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

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

具體如下圖所示:

導(dǎo)航管理類-跳轉(zhuǎn)需要傳遞數(shù)據(jù)的頁面

導(dǎo)航管理類-傳遞數(shù)據(jù)

導(dǎo)航管理類-獲取傳遞的數(shù)據(jù)

導(dǎo)航管理類代碼如下:

//==========邏輯方法==========publicstaticTgetParcelableExtra(Activity activity){? ? ? ? Parcelable parcelable = activity.getIntent().getParcelableExtra(NavigateManager.PARCELABLE_EXTRA_KEY);? ? ? ? activity =null;return(T)parcelable;? ? }privatestaticvoidoverlay(Context context, Class targetClazz,intflags, Parcelable parcelable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? setFlags(intent, flags);? ? ? ? putParcelableExtra(intent, parcelable);? ? ? ? context.startActivity(intent);? ? ? ? context =null;? ? }privatestaticvoidoverlay(Context context, Class targetClazz, Parcelable parcelable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? putParcelableExtra(intent, parcelable);? ? ? ? context.startActivity(intent);? ? ? ? context =null;? ? }privatestaticvoidoverlay(Context context, Class targetClazz, Serializable serializable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? putSerializableExtra(intent, serializable);? ? ? ? context.startActivity(intent);? ? ? ? context =null;? ? }privatestaticvoidoverlay(Context context, Class targetClazz){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? context.startActivity(intent);? ? ? ? context =null;? ? }privatestaticvoidforward(Context context, Class targetClazz,intflags, Parcelable parcelable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? setFlags(intent, flags);? ? ? ? intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);? ? ? ? context.startActivity(intent);if(isActivity(context))return;? ? ? ? ((Activity)context).finish();? ? ? ? context =null;? ? }privatestaticvoidforward(Context context, Class targetClazz, Parcelable parcelable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? putParcelableExtra(intent, parcelable);? ? ? ? context.startActivity(intent);if(isActivity(context))return;? ? ? ? ((Activity)context).finish();? ? ? ? context =null;? ? }privatestaticvoidforward(Context context, Class targetClazz, Serializable serializable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? putSerializableExtra(intent, serializable);? ? ? ? context.startActivity(intent);if(isActivity(context))return;? ? ? ? ((Activity)context).finish();? ? ? ? context =null;? ? }privatestaticvoidforward(Context context, Class targetClazz){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? context.startActivity(intent);if(isActivity(context))return;? ? ? ? ((Activity)context).finish();? ? ? ? context =null;? ? }privatestaticvoidstartForResult(Context context, Class targetClazz,intflags){? ? ? ? Intent intent =newIntent(context, targetClazz);if(isActivity(context))return;? ? ? ? ((Activity)context).startActivityForResult(intent, flags);? ? ? ? context =null;? ? }privatestaticvoidstartForResult(Context context, Class targetClazz,intflags, Parcelable parcelable){? ? ? ? Intent intent =newIntent(context, targetClazz);if(isActivity(context))return;? ? ? ? putParcelableExtra(intent, parcelable);? ? ? ? ((Activity)context).startActivityForResult(intent, flags);? ? ? ? context =null;? ? }privatestaticvoidsetResult(Context context, Class targetClazz,intflags, Parcelable parcelable){? ? ? ? Intent intent =newIntent(context, targetClazz);? ? ? ? setFlags(intent, flags);? ? ? ? putParcelableExtra(intent, parcelable);if(isActivity(context))return;? ? ? ? ((Activity)context).setResult(flags, intent);? ? ? ? ((Activity)context).finish();? ? }privatestaticbooleanisActivity(Context context){if(!(contextinstanceofActivity))returntrue;returnfalse;? ? }privatestaticvoidsetFlags(Intent intent,intflags){if(flags <0)return;? ? ? ? intent.setFlags(flags);? ? }privatestaticvoidputParcelableExtra(Intent intent, Parcelable parcelable){if(parcelable ==null)return;? ? ? ? intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);? ? }privatestaticvoidputSerializableExtra(Intent intent, Serializable serializable){if(serializable ==null)return;? ? ? ? intent.putExtra(PARCELABLE_EXTRA_KEY, serializable);? ? }

傳遞數(shù)據(jù)用的序列化對象,如下:


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

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

對頁面跳轉(zhuǎn)的處理

顯示對話框

我在這里采用和頁面跳轉(zhuǎn)的處理類似的方案,這也是 View 的責任,所以讓 View 自己去完成。這里建議每個模塊都定義一個相應(yīng)的 XxxDialogManager 類,來管理該模塊所有的彈窗,當然對于彈窗本來就不多的,那就直接在 util 包中定義一個 DialogManager 類就好了。如下圖:

對顯示對話框的處理

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

對于復(fù)雜頁面,一般建議拆成多個自定義 View,那么這就引出一個問題,這時候是用一個 Presenter 好,還是定義多個 Presenter 好呢?我的建議是,每個 View Interface 對應(yīng)一個 Presenter,如下圖所示:

一個頁面多個 View 處理

5.5.4. 一個View有兩個實現(xiàn)類的問題

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

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

如下圖所示:

一個 View 多個實現(xiàn)類處理

5.6. 關(guān)于 RestAPI

一般項目當中會用到很多和服務(wù)器端通信用的接口,這里建議在每個模塊中都建立一個 api 包,在該包下來統(tǒng)一處理該模塊下所有的 RestAPI。

如下圖所示:

統(tǒng)一管理 RestAPI

對于網(wǎng)絡(luò)請求之類需要異步處理的情況,一般都需要傳入一個回調(diào)接口,來獲取異步處理的結(jié)果。對于這種情況,我建議參考 onClick(View v) {} 的寫法。那就是為每一個請求編一個號(使用 int 值),我稱之為 taskId,可以將該編號定義在各個模塊的常量類中。然后在回調(diào)接口的實現(xiàn)類中,可以在回調(diào)方法中根據(jù) taskId 來統(tǒng)一處理(一般是在這里分發(fā)下去,分別調(diào)用不同的方法)。

如下圖所示:

定義 taskId

異步任務(wù)回調(diào)處理

5.6. 關(guān)于項目中的常量管理

Android 中不推薦使用枚舉,推薦使用常量,我想說說項目當中我一般是怎么管理常量的。

靈感來自 R.java 類,這是由項目構(gòu)建工具自動生成并維護的,可以進去看看,里面是一堆的靜態(tài)內(nèi)部類,如下圖:

Android 中的 R 文件

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

全局常量 C 類

5.7. 關(guān)于第三方庫

Android 開發(fā)中不可避免要導(dǎo)入很多第三方庫,這里我想談?wù)勎覍Φ谌綆斓囊恍┛捶ājP(guān)于第三方庫的推薦我就不做介紹了,很多專門說這方面的文章。

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

項目中確實需要(這不是廢話嗎?用不著,我要它干嘛?呵呵,建議不要為了解決一個小小的問題導(dǎo)入一個大而全的庫)

使用的人要多(大家都在用的一般更新會比較快,出現(xiàn)問題解決方案也多)

效率和體量的權(quán)衡(如果效率沒有太大影響的情況下,我一般建議選擇體量小點的,如,Gson vs Jackson,Gson 勝出;還是 65K 的問題)

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

為什么要二次封裝?

為了方便更換,說得稍微專業(yè)點為了降低耦合。

有很多原因可能需要你替換項目中的第三方庫,這時候如果你是經(jīng)過二次封裝的,那么很簡單,只需要在封裝類中修改一下就可以了,完全不需要去全局檢索代碼。

我就遇到過幾個替換第三方庫的事情:

替換項目中的統(tǒng)計埋點工具

替換網(wǎng)絡(luò)框架

替換日志工具

那該怎么封裝呢?

一般的,如果是一些第三方的工具類,都會提供一些靜態(tài)方法,那么這個就簡單了,直接寫一個工具類,提供類似的靜態(tài)方法即可(就是用靜態(tài)工廠模式)。

如下代碼所示,這是對系統(tǒng) Log 的簡單封裝:

/**

* Description: 企業(yè)中通用的Log管理

* 開發(fā)階段LOGLEVEL = 6

* 發(fā)布階段LOGLEVEL = -1

*/publicclassLogger{privatestaticintLOGLEVEL =6;privatestaticintVERBOSE =1;privatestaticintDEBUG =2;privatestaticintINFO =3;privatestaticintWARN =4;privatestaticintERROR =5;publicstaticvoidsetDevelopMode(booleanflag){if(flag) {? ? ? ? ? ? LOGLEVEL =6;? ? ? ? }else{? ? ? ? ? ? LOGLEVEL = -1;? ? ? ? }? ? }publicstaticvoidv(String tag, String msg){if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {? ? ? ? ? ? Log.v(tag, msg);? ? ? ? }? ? }publicstaticvoidd(String tag, String msg){if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {? ? ? ? ? ? Log.d(tag, msg);? ? ? ? }? ? }publicstaticvoidi(String tag, String msg){if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {? ? ? ? ? ? Log.i(tag, msg);? ? ? ? }? ? }publicstaticvoidw(String tag, String msg){if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {? ? ? ? ? ? Log.w(tag, msg);? ? ? ? }? ? }publicstaticvoide(String tag, String msg){if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {? ? ? ? ? ? Log.e(tag, msg);? ? ? ? }? ? }}

現(xiàn)在如果想替換為orhanobutLogger,那很簡單,代碼如下:

/**

* Description: 通用的Log管理工具類

* 開發(fā)階段LOGLEVEL = 6

* 發(fā)布階段LOGLEVEL = -1

*/publicclassLogger{publicstaticString mTag ="MVPBestPractice";privatestaticintLOGLEVEL =6;privatestaticintVERBOSE =1;privatestaticintDEBUG =2;privatestaticintINFO =3;privatestaticintWARN =4;privatestaticintERROR =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}publicstaticvoidsetDevelopMode(booleanflag){if(flag) {? ? ? ? ? ? LOGLEVEL =6;? ? ? ? ? ? com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.FULL);? ? ? ? }else{? ? ? ? ? ? LOGLEVEL = -1;? ? ? ? ? ? com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.NONE);? ? ? ? }? ? }publicstaticvoidv(@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);? ? ? ? }? ? }publicstaticvoidd(@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);? ? ? ? }? ? }publicstaticvoidi(@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);? ? ? ? }? ? }publicstaticvoidw(@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);? ? ? ? }? ? }publicstaticvoide(@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);? ? ? ? }? ? }publicstaticvoide(@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());? ? ? ? }? ? }publicstaticvoidv(String msg){if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.v(mTag, msg);com.orhanobut.logger.Logger.v(msg);? ? ? ? }? ? }publicstaticvoidd(String msg){if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.d(mTag, msg);com.orhanobut.logger.Logger.d(msg);? ? ? ? }? ? }publicstaticvoidi(String msg){if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.i(mTag, msg);com.orhanobut.logger.Logger.i(msg);? ? ? ? }? ? }publicstaticvoidw(String msg){if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.w(mTag, msg);com.orhanobut.logger.Logger.v(msg);? ? ? ? }? ? }publicstaticvoide(String msg){if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.e(mTag, msg);com.orhanobut.logger.Logger.e(msg);? ? ? ? }? ? }publicstaticvoide(Exception e){if(LOGLEVEL > ERROR) {//? ? ? ? ? ? Log.e(mTag, e==null ? "未知錯誤" : e.getMessage());com.orhanobut.logger.Logger.e(e ==null?"未知錯誤": e.getMessage());? ? ? ? }? ? }publicstaticvoidwtf(@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);? ? ? ? }? ? }publicstaticvoidjson(@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);? ? ? ? }? ? }publicstaticvoidxml(@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);? ? ? ? }? ? }publicstaticvoidwtf(String msg){if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.i(tag, msg);com.orhanobut.logger.Logger.wtf(msg);? ? ? ? }? ? }publicstaticvoidjson(String msg){if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.i(tag, msg);com.orhanobut.logger.Logger.json(msg);? ? ? ? }? ? }publicstaticvoidxml(String msg){if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {//? ? ? ? ? ? Log.i(tag, msg);com.orhanobut.logger.Logger.xml(msg);? ? ? ? }? ? }privatestaticStringcheckTag(String tag){if(TextUtils.isEmpty(tag)) {? ? ? ? ? ? tag = mTag;? ? ? ? }returntag;? ? }

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

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

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

5.8. MVP vs MVVM

關(guān)于 MVP 和 MVVM 我只想說一句,它們并不是相斥的。具體它們是怎么不相斥的,markzhai的這篇文章“MVPVM in Action, 誰告訴你MVP和MVVM是互斥的”說得很詳細。

5.9. Code

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

5.10. 小結(jié)

歷時三天的 MVP 總結(jié),總算要告一段落了。前期斷斷續(xù)續(xù)地花了將近一周左右零散的時間去調(diào)研 MVP,直到正式開始碼字的時候才發(fā)現(xiàn)準備的還不夠。看了很多文章,有觀點一致的,也有觀點很不一致的。最關(guān)鍵的是,自己對于 MVP 還沒有比較深刻的認知,所以在各種觀點中取舍花了很長時間。

這算得上是我第一次真正意義上的寫技術(shù)性的文章,說來慚愧,工作這么長時間了,現(xiàn)在才開始動筆。

總體來說,寫得并不盡如人意,套一句老話——革命尚未成功,同志仍需努力。這算是一次嘗試,希望以后會越寫越順暢。在這里給各位堅持看到此處的看官們問好了,祝大家一同進步。(歡迎大家圍觀我的GitHub,周末更新,會漸漸提交更多有用的代碼的)

6. 進階與不足

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

說好的“show me the code”,代碼呢?(再次抱歉了)

上篇當中關(guān)于各種 Presenter 方案只是做了簡單的羅列,并沒有仔細分析各個方案的優(yōu)點和不足

沒有形成自己的框架(呵呵,好高騖遠了,但是夢想還是要有的...)

沒有單元測試(項目代碼都還沒有呢,提倡 TDD 不是,呵呵)

很多細節(jié)沒有介紹清楚(如關(guān)于Model、Domain、Entity 等概念不是很清晰)

很多引用的觀點沒有指明出處(如有侵權(quán),馬上刪除)

......

最后想說一句,沒有完美的架構(gòu),沒有完美的框架,趕緊編碼吧!

7. 附錄

Android MVP 總結(jié)資料匯總

附上我的思維導(dǎo)圖:

MVPBestPractice.mmap

MVP總結(jié).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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內(nèi)容