Android 架構組件 - 讓天下沒有難作的 App

Google 爲了幫助 Android 開發者更快更好地開發 App,推出了一系列組件,這些組件被打包成了一個總體,稱做 Android Jetpack,它包含的組件以下圖所示:javascript



老的 support 包被整合進了 Jetpack,例如上圖 Foundation 模塊的 AppCompat,整合進去以後,包名作了一下修改,所有以 androidx 開頭。Android Studio 提供的遷移工具(Refactor > Migrate to AndroidX)能夠將源碼中的舊包名替換成新的,可是若是 Maven 依賴的產物還未遷移到 AndroidX 的話,還須要配置一個工具—— Jetifier,只須要在 build.gradle 中加上兩行配置便可:java

android.enableJetifier=true
複製代碼

Jetfier 會在編譯階段直接修改依賴產物的字節碼,簡單粗暴。node

架構大圖

Jetpack 不屬於 Android Framework,不是 Android 開發的必需品,它只是應用層開發的一種輔助手段,幫咱們解決了一些常見問題,好比版本兼容、API 易用性、生命週期管理等。其中 Architecture 部分的組件(Android Architecture Components,如下簡稱 AAC)組合起來造成了一套完整的架構解決方案,在沒有更好的方案被髮明出來以前,咱們姑且把 AAC 當作 Android 架構領域的最佳實踐,它的出現必定程度上避免了不少沒必要要的輪子。android

官方給出的架構指導很是明確地表達出了每一個架構組件的位置:程序員



這張圖背後隱含了三大設計思想:數據庫

  • 關注點分離(SOC / Separation Of Concerns)
  • 數據驅動 UI(Reactive)
  • 惟一真相源(SSOC / Single Source Of Truth)

SOC 具體到工程實踐中就是分層合理,單層的職責越明確,對上下游的依賴越清晰就意味着它的結構更穩定,也
更可測(testable)。一個 App 從全局來看,能夠劃分爲三部分:首先是 UI Controller 層,包含 Activity 和 Fragment;其次是 ViewModel 層,既能夠作 MVVM 的 VM、MVP 的 P,也能夠作 UI 的數據適配,這一層能夠實現數據驅動 UI;最後是 Repository 層,它做爲 SSOC,是一個 Facade 模式,對上層屏蔽了數據的來源,能夠來自 local,也是來自 remote,數據持久化策略向上透明。編程

一張架構藍圖,三大設計原則,接下來深刻細節,看看組件之間如何配合才能實現這個架構後端

Lifecycle

與 React/Vue 或者 iOS 相比,Android 的生命週期都比較複雜,若是要監聽生命週期,通常狀況下只能覆寫 Activity / Fragment 的回調方法(onCreate、onResume、onPause、onDestroy 等),樣板代碼少不了,可維護性也變差。緩存

若是要對生命週期進行簡化,能夠抽象成一個圖,點表示狀態,線表示事件:安全



Lifecycle 負責處理這些點(states)和線(events),Activity / Fragment 是 LifecycleOwner,監聽者則是 LifecycleObserver,一個很是清晰的觀察者模式。

class MyObserver : LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun connectListener() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun disconnectListener() {
    }
}
複製代碼

若是咱們的組件須要強綁定聲明週期,那麼只須要藉助 Lifecycle 去監聽生命週期的狀態和事件便可,不再用覆寫各類回調方法了。下面將要講到的 LiveData 和 ViewModel 都是 Lifecycle-Aware Components,它們都用到了 Lifecycle。

Android 生命週期管理不當帶來的最大問題就是內存泄露,舉一個咱們常常遇到的場景:一個異步任務(好比網絡請求)持有了 UI 元素的引用,只要任務沒有執行完,全部與這個 UI 元素有強引用關係的元素都無法被 GC,若是這樣的場景多發生幾回,極可能會引發 OOM。

爲了異步對象引用的問題,最先咱們使用 AsyncTask,任務執行在 worker thread,執行結果在主線程上發起回調。AsyncTask 的致命缺點是不支持流式數據(stream),並且回調嵌套太深(callback hell),與軟件質量衡量指標之一的 maintainable 背道而馳,很差用天然就會慢慢被淘汰。

後來咱們開始使用 RxJava,響應式編程,聲明式寫法,再借助 retrolambda 這種 backport,即便當年 Android 只支持到 JDK7,咱們依然能夠利用各類 operator 寫出很是簡潔的代碼,「filter map 我閉~着眼」。RxJava 不但完美解決了線程調度的問題,還爲咱們提供了 OO 以外的抽象——做用在流上的 lambda,基於函數的抽象。可是,即使完美如斯,生命週期的問題依然沒法迴避,由於 Java 天生的侷限性,一個 lambda 不管僞造地再像高階函數,它本質上仍是一個匿名內部類,這個匿名內部類依然持有對 outer class 實例的引用。因而咱們必須經過 CompositeDisposable 來管理訂閱關係,發起異步操做時記錄訂閱,離開頁面時取消訂閱,仍然須要覆寫 onDestory 或者 onPause 。

若是咱們以 Repository 層爲界把架構藍圖分爲上下兩部分的話,上面的部分是數據展現,下面的部分是數據獲取,數據獲取部分由於要請求 Remote 數據,必然會依賴到線程調度,而數據展現必然運行在 UI 線程,與生命週期強相關,這個時候就須要 LiveData 登場了。

LiveData

LiveData 也是一個觀察者模型,可是它是一個與 Lifecycle 綁定了的 Subject,也就是說,只有當 UI 組件處於 ACTIVE 狀態時,它的 Observer 才能收到消息,不然會自動切斷訂閱關係,不用再像 RxJava 那樣經過 CompositeDisposable 來手動處理。

LiveData 的數據相似 EventBus 的 sticky event,不會被消費掉,只要有數據,它的 observer 就會收到通知。若是咱們要把 LiveData 用做事件總線,還須要作一些定製,Github 上搜 SingleLiveEvent 能夠找到源碼實現。

咱們無法直接修改 LiveData 的 value,由於它是不可變的(immutable),可變(mutable)版本是 MutableLiveData,經過調用 setValue(主線程)或 postValue(非主線程)能夠修改它的 value。若是咱們對外暴露一個 LiveData,可是不但願外部能夠改變它的值,能夠用以下技巧實現:

private val _waveCode = MutableLiveData<String>()
val waveCode: LiveData<String> = _waveCode
複製代碼

內部用 MutableLiveData ,能夠修改值,對外暴露成 LiveData 類型,只能獲取值,不能修改值。

LiveData 有一個實現了中介者模式的子類 —— MediatorLiveData,它能夠把多個 LiveData 整合成一個,只要任何一個 LiveData 有數據變化,它的觀察者就會收到消息:

val liveData1 = ...
 val liveData2 = ...

 val liveDataMerger = MediatorLiveData<>();
 liveDataMerger.addSource(liveData1) { value -> liveDataMerger.setValue(value))
 liveDataMerger.addSource(liveData2) { value -> liveDataMerger.setValue(value))
複製代碼

綜上,咱們彙總一下 LiveData 的使用場景:

  • LiveData - immutable 版本
  • MutableLiveData - mutable 版本
  • MediatorLiveData - 可彙總多個數據源
  • SingleLiveEvent - 事件總線

LiveData 只存儲最新的數據,雖然用法相似 RxJava2 的 Flowable,可是它不支持背壓(backpressure),因此不是一個流(stream),利用 LiveDataReactiveStreams 咱們能夠實現 Flowable 和 LiveData 的互換。

若是把異步獲取到的數據封裝成 Flowable,經過 toLiveData 方法轉換成 LiveData,既利用了 RxJava 的線程模型,還消除了 Flowable 與 UI Controller 生命週期的耦合關係,藉助 Data Binding 再把 LiveData 綁定到 xml UI 元素上,數據驅動 UI,妥妥的響應式。因而一幅以下模樣的數據流向圖就被勾勒了出來:


圖中右上角的 Local Data 是 AAC 提供的另外一個強大武器 —— ORM 框架 Room。

Room

數據庫做爲數據持久層,其重要性不言而喻,當設備處於離線狀態時,數據庫可用於緩存數據;當多個 App 須要共享數據時,數據庫能夠做爲數據源,可是基於原生 API 徒手寫 CRUD 實在是痛苦,雖然 Github 上出現了很多 ORM 框架,可是它們的易用性也不敢讓人恭維,直到 Room 出來以後,Android 程序員終於能夠像 mybatis 那樣輕鬆地操縱數據庫了。

Room 是 SQLite 之上的應用抽象層,而 SQLite 是一個位於 Android Framework 層的內存型數據庫。雖然 Realm 也是一個優秀的數據庫,可是它並無內置於 Android 系統,所會增大 apk 的體積,使用 Room 則沒有這方面煩惱。

Room 的結構抽象得很是簡單,數據對象(表名 + 字段)用 @Entity 註解來定義,數據訪問用 @Dao 來註解,db 自己則用 @Database 來定義,若是要支持複雜類型,能夠定義 @TypeConverters,而後在編譯階段,apt 會根據這些註解生成代碼。Room 與 App 其餘部分的交互以下圖所示:


Entity 是一個數據實體,表示一條記錄,它的用法以下:

@Entity(tableName = "actors")
data class Actor( @PrimaryKey @ColumnInfo(name = "id")
        val actorId: String,
        val name: String,
        val birthday: Date?,
        val pictureUrl: String
)
複製代碼

Actor 是一個用 @Entity 註解的 data class,它會生成一個名字是 actors 的表,注意到有一個字段是 @Date? ,可是 SQLite 自己不支持這種複雜類型(complex type),因此咱們還須要寫一個能夠轉換成基礎類型的轉換器:

class Converters {
    @TypeConverter
    fun timestampToDate(value: Long?) = value?.let { Date(it) }

    @TypeConverter
    fun dateToTimestamp(date: Date?) = date?.time
}
複製代碼

轉換器經過 @TypeConverters 可做用於 class、field、method、parameter,分別表明不一樣的做用域。好比做用在 @Database 類的上,那麼它的做用域就是 db 中出現的全部 @Dao 和 @Entity。

@Database(entities = [Actor::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun actorDao(): ActorDao
}
複製代碼

代碼出現的 ActorDao 定義了 CRUD 操做。用 @Dao 來註解,它既能夠是一個接口,也能夠是抽象類,用法以下:

@Dao
interface ActorDao {
    @Query("SELECT * FROM actors WHERE id = :actorId")
    fun getActor(actorId: String): LiveData<Actor> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(actors: List<Actor>) } 複製代碼

@Query 中的 SQL 語句能夠直接引用方法參數,並且它的返回值能夠是 LiveData 類型,也支持 Flowable 類型,也就是說,Room 原生支持響應式,這是對數據驅動最有利的支持,也是 Room 區別於其餘 ORM 框架的顯著特徵。

至此,咱們能夠肯定,不管數據來自 Remote 仍是來自本地 DB,架構藍圖中的 Repository 對 ViewModel 提供的數據能夠永遠是 LiveData 類型,接下來咱們看一下 ViewModel 的妙用。

ViewModel

ViewModel 是一個多面手,由於它的生命週期比較長,能夠跨越由於配置變更(configuration changed,好比屏幕翻轉)引發的 Activity 重建,所以 ViewModel 不能持有對 Activity / Fragment 的引用。

若是 ViewModel 中要用到 context 怎麼辦呢?不要緊,框架提供了一個 ViewModel 的子類 AndroidViewModel ,它在構造時須要傳入 Application 實例。

既然 ViewModel 與 UI Controller 無關,固然能夠用做 MVP 的 Presenter 層提供 LiveData 給 View 層,由於 LiveData 綁定了 Lifecycle,因此不存在內存泄露的問題。除此以外,ViewModel 也能夠用作 MVVM 模式的 VM 層,利用 Data Binding 直接把 ViewModel 的 LiveData 屬性綁定到 xml 元素上,xml 中聲明式的寫法避免了不少樣板代碼,數據驅動 UI 的最後一步,咱們只須要關注數據的變化便可,UI 的狀態會自動發生變化。

ViewModel 配合 Data Binding 的用法與 React 很是類似,ViewModel 實例至關於 state,xml 文件就比如 render 函數,只要 state 數據發生變化,render 就會從新渲染 UI,可是 data binding 還有更強大的一點,它支持雙向綁定。舉個例子,UI 須要展現一個評論框,容許展現評論,也容許用戶修改,那麼咱們能夠直接把 EditText 雙向綁定到一個 LiveData 之上,只要用戶有輸入,咱們就能夠收到通知,徹底不須要經過 Kotlin/Java 來操控 UI:

android:text="@={viewModel.commentText}" />
複製代碼

注意,若是要在 xml 中使用 LiveData,須要把 lifecycle owner 賦給 binding:

val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.main)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
複製代碼

由於 ViewModel 拿到的數據是 Repository 給的,可能不適用於 UI 元素,因此 ViewModel 還承擔了數據適配的工做,有時候咱們須要彙總 repository 的多個返回值一次性給到 UI,那麼就可使用 LiveData 的「操做符」 Transformations.switchMap,用法能夠認爲等同於 Rx 的 flatMap;若是隻想對 LiveData 的 value 作一些映射,可使用 Transformations.map,目前 Transformations 只有這兩個操做符,由於無論 Kotlin 仍是 Java8,都提供了不少聲明式的操做符,對流的支持都比較友好,而 LiveData 自己不是一個流,因此這兩個操做符足矣。

除了數據適配以外,ViewModel 還有一個強大的用法 —— Fragment 之間共享數據,這樣 ViewModel 又扮演了 FLUX 模式中的 store 這一角色,是多個頁面(fragment)之間惟一的數據出口。


ViewModel 的用法也很是簡單,經過 ViewModelProviders.of 能夠獲取 ViewModel 實例:

.get(ActorViewModel::class.java)
複製代碼

一通操做猛如虎以後,UI controller 層變得薄如蟬翼,它只作了一件事情,把數據從左手(ViewModel)倒給了右手(使用了 Data Binding 的 xml)。

若是把 ViewModel 做爲 SSOC(惟一真相源),多個 Fragment 之間共享數據,再利用 SingleLiveEvent 作總線,一個 Activity 配多個 Fragment 的寫法就避免了 Activity 之間經過 Intent 傳遞數據的繁瑣。可是 Fragment 的堆棧管理一直是一個讓人頭疼的問題,AAC 的 Navigation 不但完美解決了這個問題,並且還提供可視化的路由,只需拖拽一下就能生成類型安全的跳轉邏輯。

Navigation

Navigation 用一個圖(graph)來表示頁面間的路由關係,圖的節點(node)表示頁面,邊(edge)表示跳轉關係。例以下圖 8 個頁面的跳轉關係,一目瞭然:

```

頁面與頁面之間的連線叫 action,它能夠配置進離場動畫(Animations),也能夠配置出棧行爲(Pop Behavior),還支持 Single Top 的啓動選項(Launch Options)。進離場動畫和啓動選項很好理解,出棧行爲是一個比較強大的功能,action 箭頭所指的方向表示目標頁面入棧,箭頭的反方向則表示目標頁面出棧,而出棧的行爲在 Navigation 編輯器中徹底可控,咱們能夠指定要出棧到哪一個頁面,甚至能夠指定目標頁面是否也須要出棧:

微信圖片_20190626165018.jpg

針對頁面節點,還能夠定義它要接收的參數(arguments),支持默認值,今後 Fragment 之間的參數傳遞變得很是直觀,很是安全。

看一下具體用法,首先在跳轉發起頁面,經過 apt 生成的跳轉函數傳入參數:

val direction = ActorListFragmentDirections.showDetail(actorId)
findNavController().navigate(direction)
複製代碼

而後利用目標頁面生成的 *Args 獲取參數:

private val args: ActorDetailFragmentArgs by navArgs() 複製代碼

這裏的 navArgs 是一個擴展函數,利用了 Kotlin 的 ReadWriteProperty。

幾行代碼就搞定了頁面之間的跳轉,並且仍是可視化!從沒有想過 Android 的頁面跳轉竟會變得如何簡單,可是 Navigation 的方案並非原創,iOS 的 Storyboard 很早就支持拖拽生成路由。當年 Android 推出 ConstraintLayout 之時,咱們都認爲是參考了 Storyboard 的頁面拖拽,如今再配上 Navigation,從頁面到跳轉,一個完整的拖拽鏈路就造成了。平臺雖然有差別化,可是使用場景一致的前提下,解決方案也就異曲同工了。

瞭解完了與生命週期有關的組件,接下來咱們來看細節。

Paging

UI 沒有辦法一次性展現全部的數據,端上的系統資源(電量、內存)也有限制,不可能把全部數據都加載到內存中;並且大批量請求數據不但浪費帶寬,在某些網絡狀況(弱網、慢網)下還會致使請求失敗,因此分頁是不少情景下的剛需。Github 上有各式各樣的解決方案,這一次,Google 直接推出了官方的分頁組件——Paging。

Paging 將分頁邏輯拆解爲三部分:

  • 數據源 DataSource
  • 數據塊 PagedList
  • 數據展現 PagedListAdapter

DataSource 的數據來源於後端服務或者本地數據庫,而且用三個子類來表示三種分頁模式:

  • PageKeyedDataSource - 單頁數據以 page key 爲標識,例如當前頁的 Response 中包含了下一頁的 url,這個 url 就是 page key。
  • ItemKeyedDataSource - 單頁數據以 item key 爲標識,好比下一頁的請求要帶當前頁最後一個 item 的 id,這個 itemId 就是 item key。
  • PositionalDataSource - 單頁數據以位置爲標識,這種模式比較常見,Room 只支持這一種,由於數據庫查詢以 OFFSET 和 LIMIT 作分頁。

PageKeyedDataSource 和 ItemKeyedDataSource 適用於內存型數據,好比直接從後端獲取後須要展現的數據。PositionalDataSource 適用於本地 Room 數據或者使用 Room 作緩存的 Cache 數據。

數據流向的關係圖以下所示:


LivePagedListBuilder 利用 DataSource.Factory 和 PageList.Config 建立 LiveData,UI Controller 拿到數據以後交給 PagedListAdapter 展現到 RecyclerView。

上圖表達了數據的流向,若是從 UI 層往回看,頁面展現的數據存儲在 PagedList 中,PagedList 只是 DataSource 的數據塊(chunk),當 PagedList 須要更多數據時,DataSource 就會給更多,當 DataSource 一無全部時便會觸發 BoundaryCallback 獲取更多數據,直到數據所有展現完畢。

LivePagedListBuilder 會將 PagedList 包裝成 LiveData<PagedList> 給到下游,它在整個數據交互鏈路中的位置以下圖所示:

Repository 拿到 Dao 的 DataSource.Factory 以後,調用它的 toLiveData 方法並傳入 PagedList.Config,而後生成一個分頁的 LiveData<PagedList> 交給 ViewModel 層。

Paging 加上生命週期相關的架構組件解決了數據存儲、數據流轉和數據展現的問題。除此以外,AAC 還包括一個強大的異步任務執行器 WorkManager,它解決了任務執行的可靠性,不管 App 退出仍是設備重啓,交給 WorkerManager 的任務都會被執行。

WorkManager

WorkManager 雖然解決了任務執行可靠性的問題,可是它沒法精確控制任務的執行時間,由於 WorkManager 要根據 OS 資源來選擇執行任務。Android 自身提供了不少方案來解決後臺任務執行的問題,能夠根據下圖的決策路徑選擇不一樣的組件:


WorkManager 總體上可分爲四部分:任務類型、任務構建、任務監控和任務控制。

1、任務類型,WorkManager 提供了一次性任務和週期性任務兩種任務類型:

  • OneTimeWorkRequest —— 一次性任務
  • PeriodicTimeWorkRequest —— 週期性任務

2、任務構建,一是執行條件,二是執行順序。

  • Constraints —— 經過 Constraints.Builder 構建任務執行的條件(網絡類型、電量、設備空間等)
  • WorkContinuation —— 能夠指定任務的執行順序,例如能夠按照 PERT 圖的順序執行任務:



    3、任務監控,經過回調來獲知任務的當前狀態:


4、任務控制,包括加入隊列,取消任務,其中 UniqueWork 提供了多種加入隊列的策略(REPLACE、KEEP、APPEND):

  • cancelWorkById(UUID) —— 經過 ID 取消單個任務
  • cancelAllWorkByTag(String) —— 經過 Tag 取消全部任務
  • cancelUniqueWork(String) —— 經過名字取消惟一任務

除此以外,WorkerManager 還提供了四種不一樣線程模型的 Worker:
Worker —— 基於默認後臺線程

  • CoroutineWorker —— 基於 Kotlin 的協程
  • RxWorker —— 基於 RxJava2
  • ListenableWorker —— 基於回調的異步

總結

Google 官方架構組件 AAC 爲咱們提供了太多通用問題的解決方案,使用場景包括數據持久化、異步任務調度、生命週期管理,UI 分頁、UI 導航,固然還有強大的 MVVM 框架 Data Binding,這些架構組件不但使代碼變得清晰易讀,並且獨立於 Android SDK 向下兼容,AAC 使咱們更加聚焦產品,專一於解決問題,而不是花太多的時間重複造輪子。


文章轉載自公衆號老梁寫代碼,有更好的文章歡迎推薦喲~

相關文章
相關標籤/搜索