升學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 中,咱們抽象出了一些基礎類, 例如 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 架構組件。
Android Jetpack
是 Google 在今年的 IO 大會上,根據去年 IO 大會上發佈的 Android Architecture Component
進一步發佈的內容,針對咱們的問題,咱們關注的主要是架構組件。
咱們使用了 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
來解決自建 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 的回調。
在 Jetpack
中,還要一個使人眼前一亮的組件就是 Paging
。在最新迭代的圖片選擇組件中,咱們也使用了 Paging
做爲列表分頁加載的載體。
Paging
將相冊選擇的邏輯抽象成了幾個部分:
PagedList
一個繼承了 AbstractList 的 List 子類, 包括了數據源獲取的數據DataSource
數據源的概念,分別提供了 PageKeyedDataSource
、ItemKeyedDataSource
、PositionalDataSource
, 在數據源中,咱們能夠定義咱們本身的數據加載邏輯。PagedListAdapter
, 在實例化這個 Adapter 的時候,咱們須要提供一個本身實現的 DiffUtil.ItemCallback
或者 AsyncDifferConfig
在相冊選擇中,咱們每頁讀取必定量的圖片,避免一次性加載全部本地圖片可能出現的卡頓
配置相對應的配置
到這裏咱們就實現了一個很優雅的列表分頁加載,咱們能夠畫出 Paging
簡單的架構圖
在通常狀況下,咱們最原始的方式,列表 UI 所在的部分,是須要知道數據的來源等邏輯部分。Paging
實際是抽象了列表分頁加載這個行爲的 Presenter
層及其下游處理。這種模式,業務的編寫者,能夠把 UI 部分的代碼模板化, 只須要關心業務邏輯,而且把業務邏輯中的數據獲取寫在 DataSource 中,使分頁加載的操做解耦程度更高。
經過實踐,咱們總結了 Android Jetpack
組件的一些優勢:
同時咱們也有一些本身的思考,思考如何去把架構升級這件事作的更好:
以上咱們介紹了升學e網通客戶端的架構升級,以及 Android Jetpack
在咱們團隊內的實踐。目前,文中介紹的部分都已經上線,部份內容已經通過了幾個版本的迭代,沒有出現明顯的線上 crash
在初步進行架構的升級以後,在客戶端穩定性的前提下,咱們團隊將會進一步嘗試架構的升級。其中包括:
koin
進行嘗試jetpack
組件的嘗試:例如 Navigation
和WorkManager