Jetpack Compose | State status management and interface refresh

We know that the UI composable items in Jetpack Compose (hereinafter referred to as Compose) are described through functions declared by @Composable, such as:

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

The above code describes a static Text, so how to update the UI in Compose?

State and reorganization

The only way for Compose to update the UI is to call the same composable with new parameters. Reorganization occurs when the state in a composable item is updated.

State

mutableStateOf() creates an observable MutableState as follows:

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

When there is any change in value, Compose will automatically arrange reorganization for all composable functions that read value. However, State can only complete reorganization and cannot complete UI updates. This is a bit convoluted. Let’s look at the example directly:

@Composable
funGreeting() {
    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 = "Click to change text")
        }
     }
}

Click the button multiple times and the execution results are as follows:

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

You can see that the reorganization is indeed performed after clicking the Button button, but the text in the Text is not updated accordingly! This is because the state in the combinable item Greeting() is re-initialized every time it is reorganized, resulting in the UI not being updated. Can the value value in State be saved during the next reorganization? The answer is yes! Can be used in conjunction with remember.

remember

Compose stores the value computed by remember in composition memory during initial composition and returns the stored value during reorganization. remember can be used to store both mutable and immutable objects. We modify the above code as follows:

@Composable
funGreeting() {
  //Remember is added in front, and everything else remains unchanged.
  val state = remember { mutableStateOf("init") }
  log("state:${state.value}")
  ...
}

After clicking the Button button:

Results of the:

15:06:04.544 E state:init
//After clicking the Button button:
15:06:07.313 E state:Jetpack Compose

You can see that the UI has been updated successfully.

remember(key1 = resId) { } Control the life cycle of the object cache
@Composable
inline fun <T> remember(
    key1: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

In addition to caching State, remember can be used to store objects or operation results that are expensive to initialize or compute in a composition.

As above, remember can also accept the key parameter. When the key changes, the cached value will be invalidated and the lambda block will be calculated again. This mechanism controls the lifetime of objects in a composition. The advantage of this is that the high-cost operation of object reconstruction will not be performed every time it is reorganized, such as:

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

Even if the above code occurs in frequently reorganized composable items, as long as key1 = resId remains unchanged, then ShaderBrush will not be recreated, thus improving performance.

rememberSaveable and custom Saver
  • remember maintains state after reorganization, but not after configuration changes;
  • If you want to maintain state after configuration changes, you can use rememberSaveable instead;
  • rememberSaveable will automatically save any value that can be saved in the Bundle; if Bundle storage is not supported, the object can be declared as @Parcelize serializable, and if it cannot be serialized, it can also be passed into a custom Saver object.

Example:

//1. Use @Parcelize annotation
//Remember to introduce the apply plugin: 'kotlin-parcelize' plugin
@Parcelize
data class CityParcel(val name: String, val country: String) : Parcelable

data class City(val name: String, val country: String)
//2. MapSaver customizes storage rules to convert objects into a set of values that the system can save to 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 custom storage rules
val CityListSaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

Use them in composable items:

@Composable
funGreeting() {
// 1. If it involves state recovery after configuration changes, use rememberSaveable directly and the value will be stored in the Bundle.
var parcelCity by rememberSaveable {
      mutableStateOf(CityParcel("Beijing", "China"))
}

// 2. If the stored value does not support Bundle, you can declare the Model as @Parcelable or use MapSaver or ListSaver to customize storage rules.
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")
}

Results of the:

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 is used in combination with remember

Generally, MutableState in Compose needs to be used in combination with remember (Coke and chicken wings are a natural pair~). There are three ways to declare MutableState objects in composable items:

val mutableState = remember { mutableStateOf("init0") } //1. Return MutableState<T> type
var value1 by remember { mutableStateOf("init1") } //2. Return T type
val (value2, setValue) = remember { mutableStateOf("init") } //3. Return two values: T, Function1<T, kotlin.Unit>

The second by delegation mechanism is the most commonly used, but it needs to be imported:

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

Several ways for UI to receive reorganized data

Modern Android architecture, whether it is MVVM or MVI, will use ViewModel. In ViewModel, LiveData and Flow are used to operate data, and the UI layer monitors data changes. When the data changes, the UI layer refreshes the UI based on the new data it monitors. That is, data-driven.

The idea of refreshing the UI interface in Compose is the same, except that the obtained data needs to be converted:

  • For LiveData, LiveData needs to be converted to State;
  • For Flow, Flow needs to be converted to State.

Remember that new data must be converted to State format so that Compose can automatically reorganize after state changes.

How to choose Flow.collectAsState() & amp; Flow.collectAsStateWithLifecycle()
//ViewModel layer
class ComposeVModel : ViewModel(){
    //StateFlow UI layer observes data changes through this reference
    private val _wanFlow = MutableStateFlow<List<WanModel>>(ArrayList())
    val mWanFlow: StateFlow<List<WanModel>> = _wanFlow

    //Request data
    fun getWanInfoByFlow(){
        ...
    }
}

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

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun Greeting(vm: ComposeVModel = viewModel()) {
    //2. Convert Flow<T> to State<T>
    val state by vm.mWanFlow.collectAsStateWithLifecycle()
    Column {
       Text(
          text = "$state",
          color = Color.Red,
          modifier = Modifier.fillMaxWidth()
        )
    //1. Click to request data through ViewModel
    Button(onClick = { vm.getWanInfoByFlow() }) {
        Text(text = "Click to change text")
      }
   }
}

Part 1 of the above code makes a network request through Button click, and part 2 is responsible for converting Flow into State. When the data is updated, the combinable items are It can be reorganized so that the entire process is connected. In the Android project, which one should I choose between collectAsState() and collectAsStateWithLifecycle()?

1, collectAsStateWithLifecycle() collects values from a Flow in a lifecycle-aware manner. It represents the latest emitted value through Compose State. Please use this method to collect data flow in Android development. To use collectAsStateWithLifecycle() you must import the library:

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

2.6.0-alpha01 is the lowest version because I am using Compose in projects below AGP7.0. If you need to use a higher version, please modify it yourself~

2, collectAsState() is similar to collectAsStateWithLifecycle(), but is not life cycle aware and is usually used in cross-platform scenarios (Compose also Can be cross-platform). collectAsState can be used within compose-runtime, so no other dependencies are required.

LiveData.obseverAsState()

observeAsState() will start observing this LiveData and automatically convert it to State< when there is data update in LiveData /code>, which triggers the reorganization of composable items.

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

//UI layer
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun Greeting(vm: ComposeVModel = viewModel()) {
   //Convert LiveData<T> to State<T>
   val liveDataState by vm.mWanLiveData.observeAsState()
   ...
}

To use observeAsState() you need to introduce:

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

Note: Google recommends that you always use composable extension functions such as LiveData.observeAsState() in composable items to convert types.

produceState converts the object into State state

produceState launches a coroutine that is scoped to a combination of values that can be pushed to the returned State. Use this coroutine to convert objects into State, such as bringing external subscription-driven state (such as Flow, LiveData, or RxJava) into the composition.

Even though produceState creates a coroutine, it can be used to observe non-pending data sources. To remove a subscription to this data source, use the awaitDispose function.

Take a look at an official example showing how to use produceState to load images from the network:

@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)
        }
    }
}

Android learning notes

Android performance optimization: https://qr18.cn/FVlo89
The underlying principles of Android Framework: https://qr18.cn/AQpN4J
Android car version: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
Android audio and video: https://qr18.cn/Ei3VPD
Jetpack family bucket article (including Compose): https://qr18.cn/A0gajp
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
Flutter article: https://qr18.cn/DIvKma
Eight major bodies of Android knowledge: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview question set in 2023: https://qr18.cn/CgxrRy
Interview questions for Android vehicle development positions: https://qr18.cn/FTlyCJ
Audio and video interview questions: https://qr18.cn/AcV6Ap

syntaxbug.com © 2021 All Rights Reserved.