Jetpack Compose | State狀態管理及界面刷新

我們知道Jetpack Compose(以下簡稱Compose)中的 UI 可組合項是通過@Composable 聲明的函數來描述的,如:

@Composable
fun Greeting() {
    Text(
        text = "init",
        color = Color.Red,
        modifier = Modifier.fillMaxWidth()
     )
}

上面的代碼描述的是一個靜態的 Text,那么如何讓 Compose 中的UI更新呢?

狀態和重組

Compose 更新UI的唯一方法是通過新參數調用同一可組合項。可組合項中的狀態更新時,就會發生重組。

State狀態

mutableStateOf() 會創建可觀察的 MutableState<T>,如下:

@Stable
interface MutableState<T> : State<T> {
    override var value: T
}

當value有任何變化時,Compose 會自動為讀取 value 的所有可組合函數安排重組。但是靠State只能完成重組,并不能完成UI更新,說的有點繞,直接來看示例:

@Composable
fun Greeting() {
    val state = mutableStateOf("init")
    log("state:${state.value}")//Logcat
       
    Column {
       Text(
           text = state.value,
           color = Color.Red,
           modifier = Modifier.fillMaxWidth()
        )
       Button(onClick = { state.value = "Jetpack Compose" }) {
          Text(text = "點擊更改文本")
        }
     }
}

image.png

多次點擊按鈕,執行結果如下:

14:25:34.493  E  state:init
14:25:35.919  E  state:init
14:25:37.365  E  state:init
......

可以看到點擊Button按鈕后確實執行重組了,但是Text中的文本并沒有相應更新!這是因為每次進行重組時,可組合項Greeting() 中的 state 又被重新初始化了,導致UI并沒有更新。能不能在下次進行重組時保存State<T>中的value值呢,答案是肯定的!可以結合 remember 來使用。

remember

Compose 會在初始組合期間將由 remember 計算的值存儲在組合內存中,并在重組期間返回存儲的值。remember 既可用于存儲可變對象,又可用于存儲不可變對象。我們將上面的代碼修改如下:

@Composable
fun Greeting() {
  //前面加了remember,其他都不變
  val state = remember { mutableStateOf("init") }
  log("state:${state.value}")
  ......
}

點擊 Button 按鈕后:

image.png

執行結果:

15:06:04.544  E  state:init
//點擊Button按鈕后:
15:06:07.313  E  state:Jetpack Compose

可以看到UI 成功的更新了。

remember(key1 = resId) { } 控制對象緩存的生命周期
@Composable
inline fun <T> remember(
    key1: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

除了緩存 State 狀態之外,還可以使用 remember 將初始化或計算成本高昂的對象或操作結果存儲在組合中。

如上,remember 還可以接受key參數,當key發生變化,緩存值會失效并再次對 lambda 塊進行計算。這種機制可控制組合中對象的生命周期。這樣帶來的好處是不會在每次重組時都進行對象重建高成本操作,如:

val bitmap = remember(key1 = resId) {
       ShaderBrush(BitmapShader(ImageBitmap.imageResource(res, resId).asAndroidBitmap(),
                  Shader.TileMode.REPEAT, Shader.TileMode.REPEAT
 ))}

上述代碼即使發生在頻繁重組的可組合項中,只要 key1 = resId 不變,那么ShaderBrush 就不會重新創建,從而提高了性能。

rememberSaveable 與自定義Saver
  • remember 在重組后保持狀態,但不會在配置更改后保持狀態;
  • 如果想在配置更改后保持狀態,可以使用 rememberSaveable 代替;
  • rememberSaveable 會自動保存可保存在 Bundle 中的任何值;如果不支持Bundle存儲,可以將對象聲明為 @Parcelize 可序列化,如果不能序列化,還可以將其傳入自定義 Saver 對象。

示例:

//1、使用@Parcelize注解
//記得引入 apply plugin: 'kotlin-parcelize'插件
@Parcelize
data class CityParcel(val name: String, val country: String) : Parcelable

data class City(val name: String, val country: String)
//2、MapSaver自定義存儲規則,將對象轉換為系統可保存到 Bundle 的一組值。
val CityMapSaver = run {
    val nameKey = "Beijing"
    val countryKey = "China"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}
//3、ListSaver自定義存儲規則
val CityListSaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

可組合項中使用它們:

@Composable
fun Greeting() {
// 1、如果涉及到配置更改后的狀態恢復,直接使用rememberSaveable,會將值存儲到Bundle中
var parcelCity by rememberSaveable {
      mutableStateOf(CityParcel("Beijing", "China"))
}

// 2、如果存儲的值不支持Bundle,可以將Model聲明為@Parcelable 或者使用MapSaver、ListSaver自定義存儲規則
var mapSaverCity by rememberSaveable(stateSaver = CityMapSaver) {
      mutableStateOf(City("Beijing", "China"))
}

var listSaverCity by rememberSaveable(stateSaver = CityListSaver) {
      mutableStateOf(City("Beijing", "China"))
}

log("parcelCity: $parcelCity")
log("mapSaverCity: $mapSaverCity")
log("listSaverCity: $listSaverCity")
}

執行結果:

17:35:36.810  E  parcelCity: CityParcel(name=Beijing, country=China)
17:35:36.810  E  mapSaverCity: City(name=Beijing, country=China)
17:35:36.810  E  listSaverCity: City(name=Beijing, country=China)

State與 remember結合使用

一般Compose中 MutableState 都是需要跟 remember 組合使用(可樂配雞翅,天生是一對~),在可組合項中聲明 MutableState 對象的方法有三種:

val mutableState = remember { mutableStateOf("init0") } //1、返回MutableState<T>類型
var value1 by remember { mutableStateOf("init1") } //2、返回T類型
val (value2, setValue) = remember { mutableStateOf("init") } //3、返回兩個值分別為:T,Function1<T, kotlin.Unit>

第二種的by委托機制是最常用的,不過需要導入:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

UI 接收重組數據的幾種方式

現代 Android 架構不管是 MVVM 還是 MVI ,都會用到ViewModel,在ViewModel中通過LiveData、Flow去操作數據,并在UI 層監聽數據變化,當數據變化時,UI 層根據監聽到的新數據做UI刷新,也就是數據驅動。

Compose中的 UI 界面刷新思路是一樣的,只不過需要將得到的數據進行一下轉換而已:

  • 對于 LiveData,需要將 LiveData<T> 轉換為 State<T>
  • 對于 Flow,需要將 Flow<T> 轉換為 State<T>

記住必須將新數據轉換為 State<T>格式,這樣 Compose 才可以在狀態發生變化后自動重組

Flow.collectAsState() & Flow.collectAsStateWithLifecycle()如何選擇

//ViewModel層
class ComposeVModel : ViewModel(){
    //StateFlow UI層通過該引用觀察數據變化
    private val _wanFlow = MutableStateFlow<List<WanModel>>(ArrayList())
    val mWanFlow: StateFlow<List<WanModel>> = _wanFlow

    //請求數據
    fun getWanInfoByFlow(){
        ......
    }
}

//UI層
import androidx.lifecycle.viewmodel.compose.viewModel

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun Greeting(vm: ComposeVModel = viewModel()) { 
    //2、將 Flow<T> 轉換成 State<T>
    val state by vm.mWanFlow.collectAsStateWithLifecycle()
    Column {
       Text(
          text = "$state",
          color = Color.Red,
          modifier = Modifier.fillMaxWidth()
        )
    //1、點擊通過ViewModel請求數據
    Button(onClick = { vm.getWanInfoByFlow() }) {
        Text(text = "點擊更改文本")
      }
   }
}

上述代碼1處通過Button點擊進行網絡請求,2處負責將 Flow<T> 轉換成 State<T>,當數據有更新時,可組合項就可以進行重組,這樣整個流程就串起來了。在Android 項目中,collectAsState()collectAsStateWithLifecycle() 該選擇哪個使用呢?

1collectAsStateWithLifecycle() 會以生命周期感知型方式從 Flow 收集值。它通過 Compose State 表示最新發出的值,在 Android 開發中請使用這個方法來收集數據流。使用collectAsStateWithLifecycle()必須引入庫:

implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01"

image.png

2.6.0-alpha01是最低版本,因為我是在AGP7.0以下的項目中使用Compose,如需使用更高版本,自行修改吧~

2collectAsState()collectAsStateWithLifecycle() 類似,但不是生命周期感知的,通常用于跨平臺的場景下(Compose也可以跨平臺)。collectAsState 可在 compose-runtime 中使用,因此不需要其他依賴項。

LiveData.obseverAsState()

observeAsState() 會開始觀察此 LiveData<T>,并在LiveData<T>有數據更新時,自動將其轉換為State<T> ,進而觸發可組合項的重組。

//ViewModel層
val mWanLiveData = MutableLiveData<List<WanModel>>()

//UI層
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun Greeting(vm: ComposeVModel = viewModel()) {
   //將 LiveData<T> 轉換成 State<T>
   val liveDataState by vm.mWanLiveData.observeAsState()
   ......
}

使用obseverAsState()需要引入:

implementation "androidx.compose.runtime:runtime-livedata:1.1.1"

注:谷歌建議務必在可組合項中使用 LiveData<T>.observeAsState() 等可組合擴展函數轉換類型。

produceState 將對象轉換為 State 狀態

produceState 會啟動一個協程,該協程將作用域限定為可將值推送到返回的 State 的組合。使用此協程將對象轉換為 State 狀態,例如將外部訂閱驅動的狀態(如 Flow、LiveData 或 RxJava)引入組合。

即使 produceState 創建了一個協程,它也可用于觀察非掛起的數據源。如需移除對該數據源的訂閱,請使用 awaitDispose 函數。

看一個官方的示例,展示了如何使用 produceState 從網絡加載圖像:

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容