Android Architecture Component 和架構升級在銘師堂的實踐

e網通大前端專欄發表於2018-12-04

前言

升學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 中,我們抽象出了一些基礎類, 例如 BasePresenterActivityBaseActivity,這段程式碼可能是這樣的

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。當然,是一個自動管理者生命週期的 PresenterViewModel 的官網簡介就是

Manage UI-related data in a lifecycle-conscious way
複製程式碼

從文件裡面我們可以看到 ViewModel 的基本用法:

image

image

從官網的這張圖我們也可以看到,ViewModel 會隨著 view 物件的 onDestory 執行 onCleared 方法銷燬

image

我們把資料的邏輯儲存在 ViewModel 中,在 Activity 生命週期發生變化的時候,我們可以從 ViewModel 中獲取資料進行 UI 的恢復。 在 ViewMdoel 中,我們也讓它承擔了一些單純的邏輯操作的職責。

在文件中我們看到的 ViewModel 初始化方式是

ViewModelProviders.of(this).get(ModelClass::class.java)
複製程式碼

在開發中, 我們也經常需要把上個 Activity 傳過來的資料傳給 ViewModel , 這時候我們可以利用 ViewModelProvider。Factory 進行初始化。

我們在團隊內的約定是,為了較複雜邏輯的抽象,我們不限制 ActivityViewModel 的對應關係。一個 Activity 中可以持有多個 ViewModel 物件。但是,在很多邏輯不算很複雜的頁面,可能仍然只是一個 Activity 需要一個ViewModel 就夠了,所以我們也寫封裝了一個對應的基礎類。

image

其中:

  • arguments() 為我們傳給 ViewModel 的引數,放在 Bundle 物件裡面。使用這個類的同學只需要關心他傳什麼值,不需要關心 Factory 的使用方法

  • viewModelClass() 返回的是 ViewModel 的 Class 物件

ViewModel 的初始化如下圖:

image

在利用 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 之後,我們梳理了我們的資料流向圖

image

這裡我們可以看到,資料的傳遞方向看其實是一個單向資料流。不會有資料從 UI 層到邏輯層互相扔來扔去的情況。即使程式碼多了,我們也只需要關注單向的資料變化就能輕鬆瞭解到邏輯。程式碼也更加容易維護。

類比一下,我們也可以發現,這個架構,和前端 React + ReduxFlux 架構也十分相似。

image

實際上,在 Jetpack 的原始碼中,我們也可以看見類似 StoreDispatcher 的概念。雖然在業務程式碼的結構我們仍然和 MVP 沒有很大差異,但是從整體的角度看,我們的架構更像是 Flux

這裡,我們就很方便的解決了自建 MVP 中,令人頭疼的生命週期問題。也不需要擔心資料返回的時候 View 已經銷燬了。因為這時候 LiveData 已經不會再執行 observer 的回撥。

LiveData和資料相關的架構

Paging的使用

Jetpack 中,還要一個令人眼前一亮的元件就是 Paging。在最新迭代的圖片選擇元件中,我們也使用了 Paging 作為列表分頁載入的載體。

Paging 將相簿選擇的邏輯抽象成了幾個部分:

資料
  • PagedList 一個繼承了 AbstractList 的 List 子類, 包括了資料來源獲取的資料
  • DataSource 資料來源的概念,分別提供了 PageKeyedDataSourceItemKeyedDataSourcePositionalDataSource, 在資料來源中,我們可以定義我們自己的資料載入邏輯。
UI
  • UI 部分 paging 提供了一個新的 PagedListAdapter, 在例項化這個 Adapter 的時候,我們需要提供一個自己實現的 DiffUtil.ItemCallback 或者 AsyncDifferConfig

在相簿選擇中,我們每頁讀取一定量的圖片,避免一次性載入所有本地圖片可能出現的卡頓

image

配置相對應的配置

image

到這裡我們就實現了一個很優雅的列表分頁載入,我們可以畫出 Paging 簡單的架構圖

image

在一般情況下,我們最原始的方式,列表 UI 所在的部分,是需要知道資料的來源等邏輯部分。Paging實際是抽象了列表分頁載入這個行為的 Presenter 層及其下游處理。這種模式,業務的編寫者,可以把 UI 部分的程式碼模板化, 只需要關心業務邏輯,並且把業務邏輯中的資料獲取寫在 DataSource 中,使分頁載入的操作解耦程度更高。

總結

通過實踐,我們總結了 Android Jetpack 元件的一些優點:

  • 官方出品,值得在第一時間使用,並且可以保證穩定性
  • 解決了自建 MVP 架構關於生命週期難以控制,介面複雜等導致的 部分程式碼不好維護的問題
  • 架構比較清晰,不會出現因為理解差異寫出風格不同的程式碼

同時我們也有一些自己的思考,思考如何去把架構升級這件事做的更好:

  • 我們需要整理出現有架構的不足,新的架構升級終究是為了解決痛點問題,不是單純為了追求新技術而升級架構。
  • 架構升級的過程,應該儘量減少對原有架構的侵入性,如果能實現無感知的替換則會更好,某些細節部分可以進行封裝,讓其他業務線的同學只關注業務的處理過程。

以上我們介紹了升學e網通客戶端的架構升級,以及 Android Jetpack 在我們團隊內的實踐。目前,文中介紹的部分都已經上線,部分內容已經經過了幾個版本的迭代,沒有出現明顯的線上 crash

遠景

在初步進行架構的升級之後,在客戶端穩定性的前提下,我們團隊將會進一步嘗試架構的升級。其中包括:

  • DI 的引入:架構在逐步的完善過程中,會分出很多的程式碼層,例如 資料庫、網路、複雜的邏輯處理層。這些物件目前在我們的程式碼中都是單例類。單例同時也意味著生命週期不好管理,我們需要一個依賴注入庫幫助我們管理物件。目前,我們正準備針對kotlin 的 koin 進行嘗試
  • 其他jetpack元件的嘗試:例如 NavigationWorkManager
  • Paging 的進一步使用:Paging 在我們的客戶端目前沒有大量使用,我們在往後將會嘗試和現有的三方 RecyclerView 元件結合,在網路請求的場景下使用它來做分頁載入邏輯

作者

  • 燒麥, 銘師堂 Android 開發工程師

審稿

  • pighead, 銘師堂 Android 開發工程師

相關文章