5. 最佳實踐
好了終于要點講自己的東西了,有點小激動。下面這些僅表示個人觀點,非一定之規(guī),各位看官按需取用,有說的不對的,敬請諒解。關(guān)于命名規(guī)范可以參考我的另一篇文章“Android 編碼規(guī)范”。老規(guī)矩先上圖:
MVPBestPractice 思維導(dǎo)圖
在參考了kenjuwagatsuma的MVP Architecture in Android Development和Saúl Molinero的A 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 PaNaVTEC的Modeling 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)在如果想替換為orhanobut的Logger,那很簡單,代碼如下:
/**
* 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. 附錄
附上我的思維導(dǎo)圖:
參考:
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
http://gold.xitu.io/entry/56cd79c12e958a69f944984c
http://blog.yongfengzhang.com/cn/blog/write-code-that-is-easy-to-delete-not-easy-to/