Android Jetpack(三) 架構

在這裏插入圖片描述

1、數據綁定 - 以聲明方式將可觀察數據綁定到界面元素

數據綁定庫是一種支持庫,藉助該庫,您可以使用聲明性格式(而非程序化地)將佈局中的界面組件綁定到應用中的數據源。

一般調用 findViewById() 來查找 TextView 並將其綁定到 viewModel 變量的 userName 屬性:

findViewById<TextView>(R.id.sample_text).apply {
        text = viewModel.userName
    }

如何在佈局文件中使用數據綁定庫將文本直接分配呢。這樣就無需調用上述代碼。請注意 @{} 語法的使用:

<TextView
        android:text="@{viewmodel.userName}" />

藉助佈局文件中的綁定組件,您可以移除 Activity 中的許多界面框架調用,使其維護起來更簡單、方便。還可以提高應用性能,並且有助於防止內存泄漏以及避免發生 Null 指針異常。

  • 使用數據綁定

請在應用模塊的 build.gradle 文件中添加 dataBinding 元素,如以下示例所示

android {
        ...
        dataBinding {
            enabled = true
        }
    }

Android 數據綁定庫示例

注意:在許多情況下,視圖綁定可簡化實現,提高性能,提供與數據綁定相同的好處。如果您使用數據綁定的主要目的是取代 findViewById() 調用,請考慮改用視圖綁定。

  • 使用視圖綁定- 視圖綁定在 Android Studio 3.6 Canary 11 及更高版本中可用。

視圖綁定功能可按模塊啓用。要在某個模塊中啓用視圖綁定,請將 viewBinding 元素添加到其 build.gradle 文件中,如下例所示:

android {
        ...
        viewBinding {
            enabled = true
        }
    }

如果您希望在生成綁定類時忽略某個佈局文件,請將 tools:viewBindingIgnore=“true” 屬性添加到相應佈局文件的根視圖中:

<LinearLayout
            ...
            tools:viewBindingIgnore="true" >
        ...
    </LinearLayout>

爲某個模塊啓用視圖綁定功能後,系統會爲該模塊中包含的每個 XML 佈局文件各生成一個綁定類。
每個綁定類均包含對根視圖以及具有 ID 的所有視圖的引用。
系統會通過以下方式生成綁定類的名稱:將 XML 文件的名稱轉換爲駝峯式大小寫,並在末尾添加“Binding”一詞。

假設某個佈局文件名爲 result_profile.xml:
<LinearLayout ... >
        <TextView  android:id="@+id/name" />
        <ImageView android:cropToPadding="true" />
        <Button android:id="@+id/button"
                android:background="@drawable/rounded_button" />
    </LinearLayout>

生成的綁定類將名爲 ResultProfileBinding。
此類具有兩個字段:一個是名爲 name 的 TextView,另一個是名爲 button 的 Button。
由於佈局中的 ImageView 沒有 ID,因此綁定類中不存在對它的引用。

每個綁定類還包含一個 getRoot() 方法,用於爲相應佈局文件的根視圖提供直接引用。
在此示例中,ResultProfileBinding 類中的 getRoot() 方法會返回 LinearLayout 根視圖。

要獲取生成的綁定類的實例,您可以調用其靜態 inflate() 方法。
通常情況下,您還會調用 setContentView(),從而將該綁定類的根視圖作爲參數進行傳遞,以使它成爲屏幕上的活動視圖。
在此示例中,您可以在 Activity 中調用 ResultProfileBinding.inflate():

private lateinit var binding: ResultProfileBinding

    @Override
    fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        binding = ResultProfileBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

現在,綁定類的實例可用於引用任何視圖:

binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
    
與使用 findViewById 相比,視圖綁定具有一些很顯著的優點:
  • Null 安全:由於視圖綁定會創建對視圖的直接引用,因此不存在因視圖 ID 無效而引發 Null 指針異常的風險。
    此外,如果視圖僅出現在佈局的某些配置中,則綁定類中包含其引用的字段會使用 @Nullable 標記。

  • 類型安全:每個綁定類中的字段均具有與它們在 XML 文件中引用的視圖相匹配的類型。這意味着不存在發生類轉換異常的風險。
    這些差異意味着佈局和代碼之間的不兼容性可能會導致編譯版本在編譯時(而非運行時)失敗。

視圖綁定和數據綁定的區別

視圖綁定和數據綁定庫均會生成可用於直接引用視圖的綁定類。不過,這兩者之間存在明顯差異:

  • 數據綁定庫僅處理使用 <layout> 代碼創建的數據綁定佈局。
  • 視圖綁定不支持佈局變量或佈局表達式,因此它不能用於在 XML 中將佈局與數據綁定。

2、Lifecycle - 管理您的 Activity 和 Fragment 生命週期

一般是在 Activity 和 Fragment 的生命週期方法中實現依賴組件的操作,這種模式會導致代碼條理性很差而且會擴散錯誤。
通過使用生命週期感知型組件,您可以將依賴組件的代碼從生命週期方法移入組件本身中。

假設我們有一個在屏幕上顯示設備位置的 Activity。常見的實現可能如下所示:

internal class MyLocationListener(
            private val context: Context,
            private val callback: (Location) -> Unit
    ) {

        fun start() {
            // connect to system location service
        }

        fun stop() {
            // disconnect from system location service
        }
    }
    class MyActivity : AppCompatActivity() {
        private lateinit var myLocationListener: MyLocationListener

        override fun onCreate(...) {
            myLocationListener = MyLocationListener(this) { location ->
                // update UI
            }
        }

        public override fun onStart() {
            super.onStart()
            myLocationListener.start()
            // manage other components that need to respond
            // to the activity lifecycle
        }

        public override fun onStop() {
            super.onStop()
            myLocationListener.stop()
            // manage other components that need to respond
            // to the activity lifecycle
        }
    }

在真實的應用中,最終會有太多管理界面和其他組件的調用,以響應生命週期的當前狀態。
管理多個組件會在生命週期方法(如 onStart() 和 onStop())中放置大量的代碼,這使得它們難以維護。

此外,無法保證組件會在 Activity 或 Fragment 停止之前啓動。在我們需要執行長時間運行的操作(如 onStart() 中的某種配置檢查)時尤其如此。這可能會導致出現一種競爭條件,在這種條件下,onStop() 方法會在 onStart() 之前結束,這使得組件留存的時間比所需的時間要長。

androidx.lifecycle 軟件包提供的類和接口可幫助您以彈性和隔離的方式解決這些問題。

Lifecycle 是一個類,用於存儲有關組件(如 Activity 或 Fragment)的生命週期狀態的信息,並允許其他對象觀察此狀態。

//類可以通過向其方法添加註解來監控組件的生命週期狀態。
//然後,您可以通過調用 Lifecycle 類的 addObserver() 方法並傳遞觀察者的實例來添加觀察者
    class MyObserver : LifecycleObserver {

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

        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        fun disconnectListener() {
            ...
        }
    }
    //myLifecycleOwner 對象實現了 LifecycleOwner 接口
   //LifecycleOwner 是單一方法接口,表示類具有 Lifecycle。
    myLifecycleOwner.getLifecycle().addObserver(MyObserver())

下面我們重新寫一下MyLocationListener示例、

我們可以讓 MyLocationListener 類實現 LifecycleObserver,
然後在 onCreate() 方法中使用 Activity 的 Lifecycle 對其進行初始化。

這樣,MyLocationListener 類便可以“自給自足”,這意味着,對生命週期狀態的變化做出響應的邏輯會在 MyLocationListener(而不是在 Activity)中進行聲明。讓各個組件存儲自己的邏輯,可使 Activity 和 Fragment 邏輯更易於管理。

    internal class MyLocationListener(
            private val context: Context,
            private val lifecycle: Lifecycle,
            private val callback: (Location) -> Unit
    ) {

        private var enabled = false

        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        fun start() {
            if (enabled) {
                // connect
            }
        }

        fun enable() {
            enabled = true
//如果 Lifecycle 現在未處於良好的狀態,則應避免調用某些回調。
//例如,如果回調在 Activity 狀態保存後運行 Fragment 事務,就會引發崩潰,因此我們絕不能調用該回調。
//所以,Lifecycle 類允許其他對象查詢當前狀態。
            if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
                // connect if not connected
            }
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        fun stop() {
            // disconnect if connected
        }
    }
    class MyActivity : AppCompatActivity() {
        private lateinit var myLocationListener: MyLocationListener

        override fun onCreate(...) {
            myLocationListener = MyLocationListener(this, lifecycle) { location ->
                // update UI
            }
            Util.checkUserStatus { result ->
                if (result) {
                    myLocationListener.enable()
                }
            }
        }
    }

LocationListener 類可以完全感知生命週期。如果我們需要從另一個 Activity 或 Fragment 使用 LocationListener,只需對其進行初始化。所有設置和拆解操作都由類本身管理,我們建議您使用生命週期感知型組件,可以輕鬆集成這些組件,而無需在客戶端進行手動生命週期管理。

生命週期感知型組件的最佳做法

1.使界面控制器(Activity 和 Fragment)保持精簡。它們不應直接獲取數據,而應使用 ViewModel 獲取,並觀察 LiveData 對象更新UI視圖。

2.設法編寫數據驅動型界面,界面控制器的責任是隨着數據更改而更新視圖,或者將用戶操作通知給 ViewModel。

3.將數據邏輯放在 ViewModel 類中。 ViewModel 應充當界面控制器與應用其餘部分之間的連接器。不過要注意,ViewModel 不負責獲取數據(例如,從網絡獲取)。ViewModel 應調用相應的組件來獲取數據,然後將結果提供給界面控制器。

4.使用 Data Binding 在視圖與界面控制器之間維持乾淨的接口,您可以使視圖更具聲明性,並儘量減少需要在 Activity 和 Fragment 中編寫的更新代碼。

5.如果界面很複雜,不妨考慮創建 presenter 類來處理界面的修改。

6.避免在 ViewModel 中引用 View 或 Activity 上下文。 如果 ViewModel 存在的時間比 Activity 更長(在配置更改的情況下),Activity 將泄露並且不會由垃圾回收器妥善處置。

7.使用 Kotlin 協程管理長時間運行的任務和其他可以異步運行的操作。

生命週期感知型組件的使用場景

生命週期感知型組件可使您在各種情況下更輕鬆地管理生命週期。下面列舉幾個例子:

1.在粗粒度和細粒度位置更新之間切換。在位置應用可見時啓用細粒度位置更新,在應用位於後臺時切換到粗粒度更新。藉助生命週期感知型組件 LiveData,應用可以在用戶使用位置發生變化時自動更新界面。

2.停止和開始視頻緩衝。可儘快開始視頻緩衝,但會推遲播放,直到應用完全啓動。此外,應用銷燬後,您還可以使用生命週期感知型組件終止緩衝。

3.開始和停止網絡連接。可在應用位於前臺時啓用網絡數據的實時更新(流式傳輸),並在應用進入後臺時自動暫停。

4.暫停和恢復動畫可繪製資源。可在應用位於後臺時暫停動畫可繪製資源,並在應用位於前臺後恢復可繪製資源。

處理 ON_STOP 事件

如果 Lifecycle 屬於 AppCompatActivity 或 Fragment,那麼調用 AppCompatActivity 或 Fragment 的 onSaveInstanceState() 時,Lifecycle 的狀態會更改爲 CREATED 並且會分派 ON_STOP 事件。

通過 onSaveInstanceState() 保存 Fragment 或 AppCompatActivity 的狀態後,其界面被視爲不可變,直到調用 ON_START。如果在保存狀態後嘗試修改界面,很可能會導致應用的導航狀態不一致,因此應用在保存狀態後運行 FragmentTransaction 時,FragmentManager 會拋出異常。

LiveData 本身可防止出現這種極端情況,方法是在其觀察者的關聯 Lifecycle 還沒有至少處於 STARTED 狀態時避免調用其觀察者。 在後臺,它會在決定調用其觀察者之前調用 isAtLeast()。

遺憾的是,AppCompatActivity 的 onStop() 方法會在 onSaveInstanceState() 之後調用,這樣就會留下一個缺口,即不允許界面狀態發生變化,但 Lifecycle 尚未移至 CREATED 狀態。

爲防止出現這個問題,beta2 及更低版本中的 Lifecycle 類會將狀態標記爲 CREATED 而不分派事件,這樣一來,即使未分派事件(直到系統調用 onStop()),檢查當前狀態的任何代碼也會獲得實際值。

遺憾的是,此解決方案有兩個主要問題:

在 API 23 及更低級別,Android 系統實際上會保存 Activity 的狀態,即使它的一部分被另一個 Activity 覆蓋。換句話說,Android 系統會調用 onSaveInstanceState(),但不一定會調用 onStop()。這樣可能會產生很長的時間間隔,在此時間間隔內,觀察者仍認爲生命週期處於活動狀態,雖然無法修改其界面狀態。

要向 LiveData 類公開類似行爲的任何類都必須實現由 Lifecycle 版本 beta 2 及更低版本提供的解決方案。

注意:爲了簡化此流程並讓其與較低版本實現更好的兼容性,自 1.0.0-rc1 版本起,當調用 onSaveInstanceState() 時,會將 Lifecycle 對象標記爲 CREATED 並分派 ON_STOP,而不等待調用 onStop() 方法。這不太可能影響您的代碼,但您需要注意這一點,因爲它與 API 26 及更低級別的 Activity 類中的調用順序不符。

示例:

3、LiveData - 在底層數據庫更改時通知視圖

LiveData 是一種可觀察的數據存儲器類。與常規的可觀察類不同,LiveData 具有生命週期感知能力,意指它遵循其他應用組件(如 Activity、Fragment 或 Service)的生命週期。這種感知能力可確保 LiveData 僅更新處於活躍生命週期狀態的應用組件觀察者。

如果觀察者(由 Observer 類表示)的生命週期處於 STARTEDRESUMED 狀態,則 LiveData 會認爲該觀察者處於活躍狀態。LiveData 只會將更新通知給活躍的觀察者。爲觀察 LiveData 對象而註冊的非活躍觀察者不會收到更改通知

LiveData 的優勢

確保界面符合數據狀態

LiveData 遵循觀察者模式。當生命週期狀態發生變化時,LiveData 會通知 Observer 對象,觀察者可以在每次發生更改時更新界面,而不是在每次應用數據發生更改時更新界面。

不會發生內存泄露

觀察者會綁定到 Lifecycle 對象,並在其關聯的生命週期遭到銷燬後進行自我清理。

不會因 Activity 停止而導致崩潰

如果觀察者的生命週期處於非活躍狀態(如返回棧中的 Activity),則它不會接收任何 LiveData 事件。

不再需要手動處理生命週期

界面組件只是觀察相關數據,不會停止或恢復觀察。LiveData 將自動管理所有這些操作,因爲它在觀察時可以感知相關的生命週期狀態變化。

數據始終保持最新狀態

如果生命週期變爲非活躍狀態,它會在再次變爲活躍狀態時接收最新的數據。例如,曾經在後臺的 Activity 會在返回前臺後立即接收最新的數據。

適當的配置更改

如果由於配置更改(如設備旋轉)而重新創建了 Activity 或 Fragment,它會立即接收最新的可用數據。

共享資源

您可以使用單一實例模式擴展 LiveData 對象以封裝系統服務,以便在應用中共享它們。LiveData 對象連接到系統服務一次,然後需要相應資源的任何觀察者只需觀察 LiveData 對象

使用LiveData

1.創建 LiveData 實例以存儲某種類型的數據。這通常在 ViewModel 類中完成。

2.創建可定義 onChanged() 方法的 Observer 對象,該方法可以控制當 LiveData 對象存儲的數據更改時會發生什麼。通常情況下,您可以在界面控制器(如 Activity 或 Fragment)中創建 Observer 對象。

3.使用 observe() 方法將 Observer 對象附加到 LiveData 對象。observe() 方法會採用 LifecycleOwner 對象。這樣會使 Observer 對象訂閱 LiveData 對象,以使其收到有關更改的通知。通常情況下,您可以在界面控制器(如 Activity 或 Fragment)中附加 Observer 對象。

注意:您可以使用 observeForever(Observer) 方法來註冊未關聯 LifecycleOwner 對象的觀察者。在這種情況下,觀察者會被視爲始終處於活躍狀態,因此它始終會收到關於修改的通知。您可以通過調用 removeObserver(Observer) 方法來移除這些觀察者。

當您更新存儲在 LiveData 對象中的值時,它會觸發所有已註冊的觀察者(只要附加的 LifecycleOwner 處於活躍狀態)。
LiveData 允許界面控制器觀察者訂閱更新。當 LiveData 對象存儲的數據發生更改時,界面會自動更新以做出響應。

創建LiveData對象

LiveData 對象通常存儲在 ViewModel 對象中,並可通過 getter 方法進行訪問

class NameViewModel : ViewModel() {

        // Create a LiveData with a String
        val currentName: MutableLiveData<String> by lazy {
            MutableLiveData<String>()
        }

        // Rest of the ViewModel...
    }
    

注意:請確保將用於更新界面的 LiveData 對象存儲在 ViewModel 對象中,而不是將其存儲在 Activity 或 Fragment 中,原因如下:

  • 避免 Activity 和 Fragment 過於龐大。現在,這些界面控制器負責顯示數據,但不負責存儲數據狀態。
  • 將 LiveData 實例與特定的 Activity 或 Fragment 實例分離開,並使 對象在配置更改後繼續存在。

觀察 LiveData 對象

一般在 onCreate() 方法是開始觀察 LiveData 對象的正確着手點,原因如下:

  • 確保系統不會從 Activity 或 Fragment 的 onResume() 方法進行冗餘調用。
  • 確保 Activity 或 Fragment 變爲活躍狀態後具有可以立即顯示的數據。一旦應用組件處於 STARTED 狀態,就會從它正在觀察的 LiveData 對象接收最新值。只有在設置了要觀察的 LiveData 對象時,纔會發生這種情況。
//在傳遞 nameObserver 參數的情況下調用 observe() 後,系統會立即調用 onChanged(),從而提供 mCurrentName 中存儲的最新值。 如果 LiveData 對象尚未在 mCurrentName 中設置值,則不會調用 onChanged()。
 class NameActivity : AppCompatActivity() {

        private lateinit var model: NameViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            // Other code to setup the activity...

            // Get the ViewModel.
            model = ViewModelProviders.of(this).get(NameViewModel::class.java)

            // Create the observer which updates the UI.
            val nameObserver = Observer<String> { newName ->
                // Update the UI, in this case, a TextView.
                nameTextView.text = newName
            }

            // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
            model.currentName.observe(this, nameObserver)
        }
    }

更新 LiveData 對象

LiveData 沒有公開可用的方法來更新存儲的數據。MutableLiveData 類將公開 setValue(T) 和 postValue(T) 方法,如果您需要修改存儲在 LiveData 對象中的值,則必須使用這些方法。
通常情況下會在 ViewModel 中使用 MutableLiveData,然後 ViewModel 只會向觀察者公開不可變的 LiveData 對象。
設置觀察者關係後,您可以更新 LiveData 對象的值(如以下示例中所示),這樣當用戶點按某個按鈕時會觸發所有觀察者:

button.setOnClickListener {
        val anotherName = "John Doe"
        model.currentName.setValue(anotherName)
    }

注意:您必須調用 setValue(T) 方法以從主線程更新 LiveData 對象。如果在 worker 線程中執行代碼,則您可以改用 postValue(T) 方法來更新 LiveData 對象。

將LiveData與Room一起使用

當數據庫更新時,Room 會生成更新 LiveData 對象所需的所有代碼。在需要時,生成的代碼會在後臺線程上異步運行查詢。此模式有助於使界面中顯示的數據與存儲在數據庫中的數據保持同步。

將協程與 LiveData 一起使用

https://developer.android.google.cn/topic/libraries/architecture/coroutines

擴展 LiveData

    class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
        private val stockManager = StockManager(symbol)

        private val listener = { price: BigDecimal ->
            value = price
        }

//LiveData有活躍觀察者時
        override fun onActive() {
            stockManager.requestPriceUpdates(listener)
        }
//LiveData沒有任何活躍觀察者時
        override fun onInactive() {
            stockManager.removeUpdates(listener)
        }
    }
    

LiveData 可以在多個 Activity、Fragment 和 Service 之間共享它們。您可以將 LiveData 類實現爲單一實例

class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
        private val stockManager: StockManager = StockManager(symbol)

        private val listener = { price: BigDecimal ->
            value = price
        }

        override fun onActive() {
            stockManager.requestPriceUpdates(listener)
        }

        override fun onInactive() {
            stockManager.removeUpdates(listener)
        }

        companion object {
            private lateinit var sInstance: StockLiveData

            @MainThread
            fun get(symbol: String): StockLiveData {
                sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
                return sInstance
            }
        }
    }
    

調用:

class MyFragment : Fragment() {

        override fun onActivityCreated(savedInstanceState: Bundle?) {
            StockLiveData.get(symbol).observe(this, Observer<BigDecimal> { price: BigDecimal? ->
                // Update the UI.
            })

        }

轉換LiveData

LiveData 對象分派給觀察者之前對存儲的值進行更改,
根據另一個實例的值返回不同的 LiveData 實例
Lifecycle 軟件包會提供 Transformations 類,該類包括可應對這些情況的輔助程序方法。

val userLiveData: LiveData<User> = UserLiveData()
    val userName: LiveData<String> = Transformations.map(userLiveData) {
        user -> "${user.name} ${user.lastName}"
    }

與 map() 類似,對存儲在 LiveData 對象中的值應用函數,並將結果解封和分派到下游。傳遞給 switchMap() 的函數必須返回 LiveData 對象,如以下示例中所示:

 private fun getUser(id: String): LiveData<User> {
      ...
    }
    val userId: LiveData<String> = ...
    val user = Transformations.switchMap(userId) { id -> getUser(id) }

要實現自定義轉換,您可以使用 MediatorLiveData

合併多個 LiveData 源

MediatorLiveData 是 LiveData 的子類,允許您合併多個 LiveData 源。只要任何原始的 LiveData 源對象發生更改,就會觸發 MediatorLiveData 對象的觀察者。

例如,如果界面中有可以從本地數據庫或網絡更新的 LiveData 對象,則可以向 MediatorLiveData 對象添加以下源:

  • 與存儲在數據庫中的數據關聯的 LiveData 對象。
  • 與從網絡訪問的數據關聯的 LiveData 對象。
    您的 Activity 只需觀察 MediatorLiveData 對象即可從這兩個源接收更新。

示例

## 4、navigation - 處理應用內導航所需的一切

導航是指支持用戶導航、進入和退出應用中不同內容片段的交互。Android Jetpack 的導航組件可幫助您實現導航,無論是簡單的按鈕點擊,還是應用欄和抽屜式導航欄等更爲複雜的模式,該組件均可應對。導航組件還通過遵循一套既定原則來確保一致且可預測的用戶體驗。

5、Paging

分頁庫可幫助您一次加載和顯示一小塊數據。按需載入部分數據會減少網絡帶寬和系統資源的使用量。

使用 PagedList 對象的 LiveData 存儲器加載和顯示數據:

  class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }
    

每個 PagedList 實例都會從對應的 DataSource 對象加載應用數據

    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource object.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }

要詳細瞭解如何將數據加載到 PagedList 對象中,請參閱有關如何加載分頁數據的指南。

示例

界面:

PagedList 類使用 PagedListAdapter 將項加載到 RecyclerView。這些類共同作用,在內容加載時抓取和顯示內容,預取不在視線範圍內的內容以及針對內容更改添加動畫。

要了解詳情,請參閱有關如何顯示分頁列表的指南。

支持不同的數據架構

獲取數據的3種方式:網絡,本地數據庫,網絡+本地數據庫

網絡(Retrofit):

注意:由於不同的應用處理和顯示錯誤界面的方式不同,因此分頁庫的 DataSource 對象不提供任何錯誤處理。如果發生錯誤,請遵循結果回調,並在稍後重試請求。有關此行爲的示例,請參閱 PagingWithNetwork 示例

本地數據庫

設置您的 RecyclerView 以觀察本地存儲空間,最好使用 Room 持久性庫。這樣,無論您何時在應用數據庫中插入或修改數據,這些更改都會自動反映在顯示此數據的 RecyclerView 中。

網絡和數據庫

在開始觀察數據庫之後,您可以使用 PagedList.BoundaryCallback 監聽數據庫中的數據何時耗盡。然後,您可以從網絡中獲取更多項目並將它們插入到數據庫中。如果界面正在觀察數據庫,則您只需執行此操作即可。

處理網絡錯誤

由於服務器不穩定或者網絡異常,如果數據刷新步驟不起作用,您可以提供“重試”按鈕供用戶選擇。如果在數據分頁步驟中發生錯誤,則最好自動重新嘗試分頁請求。

更新現有應用

1.在應用中將 List 對象替換成 PagedList 對象,後者不需要對應用界面結構或數據更新邏輯進行任何更改。
2.使用 CursorAdapter
3.使用 AsyncListUtil 異步加載內容

數據庫示例

使用 LiveData 觀察分頁數據
    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource
        // object, with position-based loading under the hood.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }

    class ConcertActivity : AppCompatActivity() {
        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            val recyclerView = findViewById(R.id.concert_list)
            val adapter = ConcertAdapter()
            viewModel.livePagedList.observe(this, PagedList(adapter::submitList))
            recyclerView.setAdapter(adapter)
        }
    }

    class ConcertAdapter() :
            PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
        fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
            val concert: Concert? = getItem(position)

            // Note that "concert" is a placeholder if it's null.
            holder.bindTo(concert)
        }

        companion object {
            private val DIFF_CALLBACK = object :
                    DiffUtil.ItemCallback<Concert>() {
                // Concert details may have changed if reloaded from the database,
                // but ID is fixed.
                override fun areItemsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert.id == newConcert.id

                override fun areContentsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert == newConcert
            }
        }
    }
    
使用 RxJava2 觀察分頁數據

如果您傾向於使用 RxJava2 而不是 LiveData,則可以改爲創建 ObservableFlowable 對象:

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: Observable<PagedList<Concert>> =
                concertDao.concertsByDate().toObservable(pageSize = 50)
    }

然後,您可以使用以下代碼段中的代碼來開始和停止觀察數據:

class ConcertActivity : AppCompatActivity() {
        private val adapter: ConcertAdapter()
        private lateinit var viewModel: ConcertViewModel

        private val disposable = CompositeDisposable()

        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val recyclerView = findViewById(R.id.concert_list)
            viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            recyclerView.setAdapter(adapter)
        }

        override fun onStart() {
            super.onStart()
            disposable.add(viewModel.concertList
                    .subscribe(adapter::submitList)))
        }

        override fun onStop() {
            super.onStop()
            disposable.clear()
        }
    }

示例

6、Room - 流暢地訪問 SQLite 數據庫

Room 持久性庫在 SQLite 的基礎上提供了一個抽象層,讓用戶能夠在充分利用 SQLite 的強大功能的同時,獲享更強健的數據庫訪問機制。

Room 包含 3 個主要組件:

  • 數據庫:包含數據庫持有者,並作爲應用已保留的持久關係型數據的底層連接的主要接入點。

    使用 @Database 註釋的類應滿足以下條件:

    • 是擴展 RoomDatabase 的抽象類。
    • 在註釋中添加與數據庫關聯的實體列表。
    • 包含具有 0 個參數且返回使用 @Dao 註釋的類的抽象方法。

    在運行時,您可以通過調用 Room.databaseBuilder()Room.inMemoryDatabaseBuilder() 獲取 Database 的實例。

  • Entity:表示數據庫中的表。

  • DAO:包含用於訪問數據庫的方法。

應用使用 Room 數據庫來獲取與該數據庫關聯的數據訪問對象 (DAO)。然後,應用使用每個 DAO 從數據庫中獲取實體,然後再將對這些實體的所有更改保存回數據庫中。最後,應用使用實體來獲取和設置與數據庫中的表列相對應的值。

使用 Room 將數據保存到本地數據庫

基本用法

User

    @Entity
    data class User(
        @PrimaryKey val uid: Int,
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?
    )

UserDao

    @Dao
    interface UserDao {
        @Query("SELECT * FROM user")
        fun getAll(): List<User>

        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        fun loadAllByIds(userIds: IntArray): List<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
               "last_name LIKE :last LIMIT 1")
        fun findByName(first: String, last: String): User

        @Insert
        fun insertAll(vararg users: User)

        @Delete
        fun delete(user: User)
    }

AppDatabase

@Database(entities = arrayOf(User::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

調用數據庫實例:

val db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java, "database-name"
            ).build()

注意:如果您的應用在單個進程中運行,則在實例化 AppDatabase 對象時應遵循單例設計模式。每個 RoomDatabase 實例的成本相當高,而您幾乎不需要在單個進程中訪問多個實例。

如果您的應用在多個進程中運行,請在數據庫構建器調用中包含 enableMultiInstanceInvalidation()。這樣,如果您在每個進程中都有一個 AppDatabase 實例,就可以在一個進程中使共享數據庫文件失效,並且這種失效會自動傳播到其他進程中的 AppDatabase 實例。

示例

博客

7、ViewModel -以注重生命週期的方式管理界面相關的數據

ViewModel 解決的問題:

1、某個 Activity 包含用戶列表。因配置更改而重新創建 Activity 後,新 Activity 必須重新提取用戶列表
2、界面控制器經常需要異步調用,並確保系統在其銷燬後清理這些調用以避免潛在的內存泄露,並且在因配置更改而重新創建對象的情況下,會造成資源的浪費,因爲對象可能需要重新發出已經發出過的調用。
3、如果要求界面控制器也負責從數據庫或網絡加載數據,那麼會使類越發膨脹。

從界面控制器邏輯中分離出視圖數據所有權的做法更易行且更高效。

實現 ViewModel

架構組件爲界面控制器提供了 ViewModel輔助程序類,該類負責爲界面準備數據。 在配置更改期間會自動保留 ViewModel 對象,以便它們存儲的數據立即可供下一個 Activity 或 Fragment 實例使用。
例如,如果您需要在應用中顯示用戶列表,請確保將獲取和保留該用戶列表的責任分配給 ViewModel,而不是 Activity 或 Fragment,如以下示例代碼所示:

   class MyViewModel : ViewModel() {
        private val users: MutableLiveData<List<User>> by lazy {
            MutableLiveData().also {
                loadUsers()
            }
        }

        fun getUsers(): LiveData<List<User>> {
            return users
        }

        private fun loadUsers() {
            // Do an asynchronous operation to fetch users.
        }
    }

然後,您可以從 Activity 訪問該列表,如下所示:

    class MyActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            // Create a ViewModel the first time the system calls an activity's onCreate() method.
            // Re-created activities receive the same MyViewModel instance created by the first activity.

            val model = ViewModelProviders.of(this)[MyViewModel::class.java]
            model.getUsers().observe(this, Observer<List<User>>{ users ->
                // update UI
            })
        }
    }

如果重新創建了該 Activity,它接收的 MyViewModel 實例與第一個 Activity 創建的實例相同。當所有者 Activity 完成時,框架會調用 ViewModel 對象的 onCleared() 方法,以便它可以清理資源。

注意ViewModel 絕不能引用視圖、Lifecycle 或可能存儲對 Activity 上下文的引用的任何類。

ViewModel 的生命週期

ViewModel 對象存在的時間範圍是獲取 ViewModel 時傳遞給 ViewModelProviderLifecycleViewModel 將一直留在內存中,直到限定其存在時間範圍的 Lifecycle 永久消失:對於 Activity,是在 Activity 完成時;而對於 Fragment,是在 Fragment 分離時。

系統首次調用 Activity 對象的 onCreate()方法時請求 ViewModel,系統可能會在 Activity 的整個生命週期內多次調用 onCreate(),ViewModel存在的時間範圍是從您首次請求 ViewMode 直到 Activity 完成並銷燬。

在 Fragment 之間共享數據

AFragment和BFragment數據傳遞,兩個 Fragment 都需要定義接口描述,這兩個 Fragment 都必須處理另一個 Fragment 尚未創建或不可見的情況。

可以使用 ViewModel對象解決這一常見的難點。這兩個 Fragment 可以使用其 Activity 範圍共享 ViewModel來處理此類通信,如以下示例代碼所示:

    class SharedViewModel : ViewModel() {
        val selected = MutableLiveData<Item>()

        fun select(item: Item) {
            selected.value = item
        }
    }

    class MasterFragment : Fragment() {

        private lateinit var itemSelector: Selector

        private lateinit var model: SharedViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            model = activity?.run {
                ViewModelProviders.of(this)[SharedViewModel::class.java]
            } ?: throw Exception("Invalid Activity")
            itemSelector.setOnClickListener { item ->
                // Update the UI
            }
        }
    }

    class DetailFragment : Fragment() {

        private lateinit var model: SharedViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            model = activity?.run {
                ViewModelProviders.of(this)[SharedViewModel::class.java]
            } ?: throw Exception("Invalid Activity")
            model.selected.observe(this, Observer<Item> { item ->
                // Update the UI
            })
        }
    }
    

請注意,這兩個 Fragment 都會檢索包含它們的 Activity。這樣,當這兩個 Fragment 各自獲取 ViewModelProvider 時,它們會收到相同的 SharedViewModel 實例(其範圍限定爲該 Activity)。

此方法具有以下優勢:

  • Activity 不需要執行任何操作,也不需要對此通信有任何瞭解。
  • 除了 SharedViewModel 約定之外,Fragment 不需要相互瞭解。如果其中一個 Fragment 消失,另一個 Fragment 將繼續照常工作。
  • 每個 Fragment 都有自己的生命週期,而不受另一個 Fragment 的生命週期的影響。如果一個 Fragment 替換另一個 Fragment,界面將繼續工作而沒有任何問題。

將加載器替換爲 ViewModel

諸如 [CursorLoader](https://developer.android.google.cn/reference/android/content/CursorLoader) 之類的加載器類經常用於使應用界面中的數據與數據庫保持同步。您可以將 ViewModel 與一些其他類一起使用來替換加載器。使用 ViewModel 可將界面控制器與數據加載操作分離,這意味着類之間的強引用更少

ViewModelRoomLiveData 一起使用可替換加載器。ViewModel 確保數據在設備配置更改後仍然存在。Room 在數據庫發生更改時通知 LiveDataLiveData 進而使用修訂後的數據更新界面。
image.png

將協程與 ViewModel 一起使用

ViewModel 支持 Kotlin 協程。如需瞭解詳情,請參閱將 Kotlin 協程與 Android 架構組件一起使用

示例

如需更多與協程相關的信息,請參閱以下鏈接:

8、WorkManager - 管理您的 Android 後臺作業

當前穩定版:
| 2020 年 1 月 22 日 | 2.3.0 |

dependencies {
      def work_version = "2.3.0"

        // (Java only)
        implementation "androidx.work:work-runtime:$work_version"

        // Kotlin + coroutines
        implementation "androidx.work:work-runtime-ktx:$work_version"

        // optional - RxJava2 support
        implementation "androidx.work:work-rxjava2:$work_version"

        // optional - GCMNetworkManager support
        implementation "androidx.work:work-gcm:$work_version"

        // optional - Test helpers
        androidTestImplementation "androidx.work:work-testing:$work_version"
      }

WorkManager 旨在用於可延遲運行(即不需要立即運行)並且在應用退出或設備重啓時必須能夠可靠運行的任務。例如:

  • 向後端服務發送日誌或分析數據
  • 定期將應用數據與服務器同步

WorkManager 不適用於應用進程結束時能夠安全終止的運行中後臺工作,也不適用於需要立即執行的任務。請查看後臺處理指南,瞭解哪種解決方案符合您的需求。

創建後臺任務

要創建後臺任務,請擴展 Worker 類並替換 doWork() 方法。例如,要創建上傳圖像的 Worker:

  class UploadWorker(appContext: Context, workerParams: WorkerParameters)
        : Worker(appContext, workerParams) {

        override fun doWork(): Result {
            // Do the work here--in this case, upload the images.

            uploadImages()

            // Indicate whether the task finished successfully with the Result
            return Result.success()
        }
    }

doWork() 返回的 Result 會通知 WorkManager 任務是否:

  • 已成功完成:Result.success()
  • 已失敗:Result.failure()
  • 需要稍後重試:Result.retry()

注意Worker 是運行工作的最簡單方式。要了解 Worker 的更多高級選項,請參閱 WorkManager 線程指南

WorkManager 提供了四種不同類型的工作基元:
  • Worker 是最簡單的實現,前面幾節已經有所介紹。WorkManager 會在後臺線程上自動運行它(您可以將它替換掉)。請參閱工作器中的線程處理,詳細瞭解 Worker 中的線程處理。

  • 建議 Kotlin 用戶實現 CoroutineWorkerCoroutineWorker 針對後臺工作公開掛起函數。默認情況下,它們運行默認的 Dispatcher,您可以對其進行自定義。請參閱 CorventineWorker 中的線程處理,詳細瞭解 CoroutineWorker 中的線程處理。

  • 建議 RxJava2 用戶實現 RxWorker。如果您有很多現有異步代碼是用 RxJava 建模的,則應使用 RxWirkers。與所有 RxJava2 概念一樣,您可以自由選擇所需的線程處理策略。請參閱 RxWorker 中的線程處理,詳細瞭解 RxWorker 中的線程處理。

  • ListenableWorkerWorkerCoroutineWorkerRxWorker 的基類。該類專爲需要與基於回調的異步 API(例如 FusedLocationProviderClient)進行交互並且不使用 RxJava2 的 Java 開發者而設計。請參閱 ListenableWorker 中的線程處理,詳細瞭解 ListenableWorker 中的線程處理。

配置運行任務的方式和時間

Worker 定義工作單元,WorkRequest 則定義工作的運行方式和時間。任務可以是一次性的,也可以是週期性的。對於一次性 WorkRequest,請使用 OneTimeWorkRequest,對於週期性工作,請使用 PeriodicWorkRequest

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .build()

WorkRequest 中還可以包含其他信息,例如任務在運行時應遵循的約束、工作輸入、延遲,以及重試工作的退避時間政策。關於這些選項,在定義工作指南中有更詳細的說明。

在本指南中,您將瞭解如何自定義工作請求來處理常見用例:

  • 處理網絡可用性等任務約束
  • 保證任務執行的延遲時間最短
  • 處理任務重試和退避
  • 處理任務輸入和輸出
  • 使用標記對任務進行分組
1、工作約束:

您可以向工作添加 Constraints,以指明工作何時可以運行:


    // Create a Constraints object that defines when the task should run
    val constraints = Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build()

    // ...then create a OneTimeWorkRequest that uses those constraints
    val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>()
            .setConstraints(constraints)
            .build()
2、初始延遲

任務設置爲在加入隊列後至少經過 10 分鐘再運行:

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setInitialDelay(10, TimeUnit.MINUTES)
            .build()
3、重試和退避政策

如果您需要讓 WorkManager 重新嘗試執行您的任務,可以從工作器返回 Result.retry()

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setBackoffCriteria(
                    BackoffPolicy.LINEAR,
                    OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                    TimeUnit.MILLISECONDS)
            .build()
4、定義任務的輸入/輸出

輸入和輸出值以鍵值對的形式存儲在 Data 對象中。下面的代碼展示瞭如何在 WorkRequest 中設置輸入數據。

   // workDataOf (part of KTX) converts a list of pairs to a [Data] object.
    val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setInputData(imageData)
            .build()

Worker 類可通過調用 Worker.getInputData() 訪問輸入參數。

類似地,Data 類可用於輸出返回值。要返回 Data 對象,請將它包含到 Result 的 Result.success() 或 Result.failure() 中,如下所示。

    class UploadWorker(appContext: Context, workerParams: WorkerParameters)
        : Worker(appContext, workerParams) {

        override fun doWork(): Result {

                // Get the input
                val imageUriInput = getInputData().getString(Constants.KEY_IMAGE_URI)
                // TODO: validate inputs.
                // Do the work
                val response = uploadFile(imageUriInput)

                // Create the output of the work
                val outputData = workDataOf(Constants.KEY_IMAGE_URL to response.imageUrl)

                // Return the output
                return Result.success(outputData)

        }
    }
5、標記工作

以下代碼展示瞭如何使用 WorkRequest.Builder.addTag(String) 向任務添加“cleanup”標記:

 val cacheCleanupTask =
            OneTimeWorkRequestBuilder<CacheCleanupWorker>()
        .setConstraints(constraints)
        .addTag("cleanup")
        .build()

將您的任務提交給系統

定義 WorkRequest 之後,您現在可以通過 WorkManager 使用 enqueue() 方法來調度它。

 WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)

執行 Worker 的確切時間取決於 WorkRequest 中使用的約束以及系統優化。WorkManager 的設計目的就是要在這些限制下提供儘可能好的表現。

方法指南

高級概念

遷移指南

示例

  • WorkManagerSample,一個簡單的圖像處理應用
  • Sunflower,這是一款演示應用,演示包括 WorkManager 在內的各種架構組件的最佳做法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章