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

前言

升學e網通是杭州銘師堂旗下的一款在線教育產品,集助學、助考、和升學爲一體,是國內最領先的高中生綜合指導系統,專爲高中同窗打造的提供視頻學習、助學備考、志願填報、升學報考等服務的平臺。在客戶端的高速業務迭代下,咱們對Android客戶端的架構進行了一次升級。咱們將用這篇文章將咱們最近幾個月的技術工做進行分享。前端

早年,咱們採用了大多數客戶端採用的 MVP 架構。可是隨着業務代碼的逐步增長,咱們遇到了下面幾個頭疼的問題。java

生命週期的不可控android

在咱們早期 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)bash

因此咱們每次都須要在網絡請求的時候對 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 異常ide

基礎對象難以維護post

在 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 開發工程師
相關文章
相關標籤/搜索