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 toState
; - For
Flow
,Flow
needs to be converted toState
.
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
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
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