前言
升學e網通是杭州銘師堂旗下的一款線上教育產品,集助學、助考、和升學為一體,是國內最領先的高中生綜合指導系統,專為高中同學打造的提供視訊學習、助學備考、志願填報、升學報考等服務的平臺。在客戶端的高速業務迭代下,我們對Android客戶端的架構進行了一次升級。我們將用這篇文章將我們最近幾個月的技術工作進行分享。
早年,我們採用了大多數客戶端採用的 MVP 架構。但是隨著業務程式碼的逐步增加,我們遇到了下面幾個頭疼的問題。
生命週期的不可控
在我們早期 MVP 的架構中,view 層就是 Actiivity、Fragment 等承載檢視的部分,這部分一般都會有自己的生命週期,在 view 層物件中,會持有一個 Presenter 的物件例項。但是我們沒有辦法保證 presenter 層物件的生命週期和 view 層保持一致。比如團隊的同學很早在 v 層的destroy中寫了如下程式碼
@Override
public void onDestroy() {
this = null;
}
複製程式碼
我們這裡暫時不討論這個做法是否有必要或者是否正確,但是這裡確實在 view 層物件置空後出現了 presenter 層對 view 層的呼叫,會發生不可預料的錯誤。 例如,我們在 presenter 層加入了最經典的 Retrofit + Rxjava 的程式碼。當弱網情況下,網路請求沒有返回,回退介面,如果當前的 Activity 物件被銷燬,而 presenter 內的網路回撥完成並呼叫了 view 層的方法重新整理 UI,就會出現 crash(NullPointException)
所以我們每次都需要在網路請求的時候對 Rxjava 的 Flowable
物件新增訂閱,在 v 層物件的生命週期中呼叫取消訂閱。
大致的程式碼如下:
addSubscribe(myApi.requestNetwork(requestModel)
.compose()
.subscribeWith(new MySubscriber<MyBean>() {
@Override
public void onFail(int errorCode, String msg) {
// todo something
}
@Override
public void onNext() {
// todo something
}
}));
複製程式碼
在團隊人員增加的時候,如果在新同學入職的時候不強調這個規則的時候,很容易就會出現線上的 NullPointException
異常
基礎物件難以維護
在 mvp 中,我們抽象出了一些基礎類, 例如 BasePresenterActivity
和 BaseActivity
,這段程式碼可能是這樣的
public abstract class BasePresenterActivity<T extends BasePresenter> extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// todo something
}
this.setContentView(getLayout());
// todo something
}
}
複製程式碼
在 onCreate
中,我們可以看到有不少程式碼邏輯,在未來的開發中,我們可能需要其他的相似功能的 Activity, 或者在某些 Fragment 中,我們需要類似的邏輯。但是,新上手的同學可能只想關心我需要複製哪些 Activity 相關的邏輯,或者只想關心和生命週期相關的邏輯,這時候,Activity 和生命週期的邏輯就耦合在了一起,終究會變得難以維護。
MVP介面過多,影響可維護性
我們使用 MVP 的初衷是為了程式碼分層解耦,利於閱讀和維護,但是在程式碼量增加後卻發現,view 層和 presenter 層通過介面來互動,導致介面中定義的方法越來越多,如果修改一個地方的邏輯,可能需要順著好多個檔案來找被影響的方法並修改。
整理一下 MVP 的資料流向,可以發現 MVP 其實是雙向的資料流。view 可以把資料傳給 presenter, presenter 也可以把資料帶給 view。邏輯複雜了之後及其不方便
團隊同學對MVP的理解不一致
MVP 雖然基本的原理很簡單,只是 MVC 的一個改進和變種。但是網上其實也有很多的 MVP 寫法。在團隊內部,對於是否應該保證 presenter 層只擁有純 Java/Kotlin 程式碼,而不出現 Android 的相關包,也有過各自的意見。
綜合以上 MVP架構 遇到的問題,升級一套新的架構,讓業務程式碼抽象程度更高,開發更簡便,程式碼更利於維護,迫在眉睫。於是我們開始關注 Google 官方出的 Jetpack 架構元件。
Jetpack
Android Jetpack
是 Google 在今年的 IO 大會上,根據去年 IO 大會上釋出的 Android Architecture Component
進一步釋出的內容,針對我們的問題,我們關注的主要是架構元件。
Lifecycle
我們使用了 Lifecycle
來重構我們的基礎 Activity
類,將 lifecycle 相關的內容和具體邏輯分類
abstract class BaseActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(bindLayout())
lifecycle.addObserver(BaseActivityLifecycle(this))
}
/**
* Activity 的 Layout questionId
*/
abstract fun bindLayout(): Int
}
複製程式碼
BaseActivityLifecycle
的程式碼如下:
class BaseActivityLifecycle(val context: Context) : LifecycleObserver {
private val value:String? = null
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
// todo something
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
// todo something
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume()) {
// todo something
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
// todo something
}
}
複製程式碼
目前,Activity內部的 lifecycle 包含了 EventBus
和我們自己的埋點庫。我們可以一目瞭然的看到我們的基類 Activity 在每個生命週期中有哪些三方庫或者二方庫需要初始化和銷燬。如果某個同學需要重構 BaseFragment 類,可以直接複用這個 lifecycle 的程式碼,也不用擔心自己寫漏了什麼 lifecycle 相關的初始化。
ViewModel
我們使用 ViewModel
來解決自建 MVP 架構中 presenter的生命週期問題。
這裡的 ViewModel
和 MVVM 的 ViewModel 並不是一回事,簡單理解,其實 ViewModel
仍然是 Presenter
。當然,是一個自動管理者生命週期的 Presenter
, ViewModel
的官網簡介就是
Manage UI-related data in a lifecycle-conscious way
複製程式碼
從文件裡面我們可以看到 ViewModel
的基本用法:
從官網的這張圖我們也可以看到,ViewModel
會隨著 view 物件的 onDestory
執行 onCleared
方法銷燬
我們把資料的邏輯儲存在 ViewModel
中,在 Activity
生命週期發生變化的時候,我們可以從 ViewModel
中獲取資料進行 UI 的恢復。 在 ViewMdoel
中,我們也讓它承擔了一些單純的邏輯操作的職責。
在文件中我們看到的 ViewModel
初始化方式是
ViewModelProviders.of(this).get(ModelClass::class.java)
複製程式碼
在開發中, 我們也經常需要把上個 Activity 傳過來的資料傳給 ViewModel
, 這時候我們可以利用 ViewModelProvider。Factory
進行初始化。
我們在團隊內的約定是,為了較複雜邏輯的抽象,我們不限制 Activity
和 ViewModel
的對應關係。一個 Activity
中可以持有多個 ViewModel
物件。但是,在很多邏輯不算很複雜的頁面,可能仍然只是一個 Activity
需要一個ViewModel
就夠了,所以我們也寫封裝了一個對應的基礎類。
其中:
-
arguments() 為我們傳給
ViewModel
的引數,放在Bundle
物件裡面。使用這個類的同學只需要關心他傳什麼值,不需要關心Factory
的使用方法 -
viewModelClass() 返回的是
ViewModel
的 Class 物件
ViewModel
的初始化如下圖:
在利用 Factory
初始化物件的時候,因為我們使用了反射,所以在 proguard-rules.pro
中我們要去掉相關類的混淆。
如果是你自己使用,需要新增
-keepclassmembers public class * extends android.arch.lifecycle.ViewModel {
public <init>(...);
}
複製程式碼
例如我們上面封裝的,則需要新增
-keepclassmembers public class * extends <your_package_name>.BaseViewModel {
public <init>(...);
}
複製程式碼
解決了生命週期的問題,那麼我們在 ViewModel
中獲取了邏輯處理的結果,應該如何反饋給 UI 呢?我們選擇使用 LiveData
完成這些。
LiveData
是一個可觀察資料的持有者,並且具有生命週期的感知。簡單的 LiveData
用法如下:
在 ViewModel
中給 LiveData
賦值,
myLiveData?.post(value)
複製程式碼
在 view 中,對 LiveData
進行觀察
mViewModel.myLiveData?.observer{v->
v?.let{
updateUI(it)
}
}
複製程式碼
關於 LiveData
更多的使用,我們會在接下來的章節介紹
在擁有了 View
, ViewModel
, LiveData
之後,我們梳理了我們的資料流向圖
這裡我們可以看到,資料的傳遞方向看其實是一個單向資料流。不會有資料從 UI 層到邏輯層互相扔來扔去的情況。即使程式碼多了,我們也只需要關注單向的資料變化就能輕鬆瞭解到邏輯。程式碼也更加容易維護。
類比一下,我們也可以發現,這個架構,和前端 React
+ Redux
的 Flux
架構也十分相似。
實際上,在 Jetpack
的原始碼中,我們也可以看見類似 Store
和 Dispatcher
的概念。雖然在業務程式碼的結構我們仍然和 MVP 沒有很大差異,但是從整體的角度看,我們的架構更像是 Flux
這裡,我們就很方便的解決了自建 MVP 中,令人頭疼的生命週期問題。也不需要擔心資料返回的時候 View
已經銷燬了。因為這時候 LiveData
已經不會再執行 observer 的回撥。
LiveData和資料相關的架構
Paging的使用
在 Jetpack
中,還要一個令人眼前一亮的元件就是 Paging
。在最新迭代的圖片選擇元件中,我們也使用了 Paging
作為列表分頁載入的載體。
Paging
將相簿選擇的邏輯抽象成了幾個部分:
資料
PagedList
一個繼承了 AbstractList 的 List 子類, 包括了資料來源獲取的資料DataSource
資料來源的概念,分別提供了PageKeyedDataSource
、ItemKeyedDataSource
、PositionalDataSource
, 在資料來源中,我們可以定義我們自己的資料載入邏輯。
UI
- UI 部分 paging 提供了一個新的
PagedListAdapter
, 在例項化這個 Adapter 的時候,我們需要提供一個自己實現的DiffUtil.ItemCallback
或者AsyncDifferConfig
在相簿選擇中,我們每頁讀取一定量的圖片,避免一次性載入所有本地圖片可能出現的卡頓
配置相對應的配置
到這裡我們就實現了一個很優雅的列表分頁載入,我們可以畫出 Paging
簡單的架構圖
在一般情況下,我們最原始的方式,列表 UI 所在的部分,是需要知道資料的來源等邏輯部分。Paging
實際是抽象了列表分頁載入這個行為的 Presenter
層及其下游處理。這種模式,業務的編寫者,可以把 UI 部分的程式碼模板化, 只需要關心業務邏輯,並且把業務邏輯中的資料獲取寫在 DataSource 中,使分頁載入的操作解耦程度更高。
總結
通過實踐,我們總結了 Android Jetpack
元件的一些優點:
- 官方出品,值得在第一時間使用,並且可以保證穩定性
- 解決了自建 MVP 架構關於生命週期難以控制,介面複雜等導致的 部分程式碼不好維護的問題
- 架構比較清晰,不會出現因為理解差異寫出風格不同的程式碼
同時我們也有一些自己的思考,思考如何去把架構升級這件事做的更好:
- 我們需要整理出現有架構的不足,新的架構升級終究是為了解決痛點問題,不是單純為了追求新技術而升級架構。
- 架構升級的過程,應該儘量減少對原有架構的侵入性,如果能實現無感知的替換則會更好,某些細節部分可以進行封裝,讓其他業務線的同學只關注業務的處理過程。
以上我們介紹了升學e網通客戶端的架構升級,以及 Android Jetpack
在我們團隊內的實踐。目前,文中介紹的部分都已經上線,部分內容已經經過了幾個版本的迭代,沒有出現明顯的線上 crash
遠景
在初步進行架構的升級之後,在客戶端穩定性的前提下,我們團隊將會進一步嘗試架構的升級。其中包括:
- DI 的引入:架構在逐步的完善過程中,會分出很多的程式碼層,例如 資料庫、網路、複雜的邏輯處理層。這些物件目前在我們的程式碼中都是單例類。單例同時也意味著生命週期不好管理,我們需要一個依賴注入庫幫助我們管理物件。目前,我們正準備針對kotlin 的
koin
進行嘗試 - 其他
jetpack
元件的嘗試:例如Navigation
和WorkManager
- Paging 的進一步使用:Paging 在我們的客戶端目前沒有大量使用,我們在往後將會嘗試和現有的三方 RecyclerView 元件結合,在網路請求的場景下使用它來做分頁載入邏輯
作者
- 燒麥, 銘師堂 Android 開發工程師
審稿
- pighead, 銘師堂 Android 開發工程師