When mutableStateOf is updated recomposition does not get occurred - android

I have basic composable as in the following:
var timer by remember { mutableStateOf(0) }
Column {
Text(text = timer.toString())
Button(onClick = { timer++ }) {
Text(text = "Add")
}
}
When button is clicked, internal state which is timer will be increased by one and since it has been used in the Text component, recomposition will be occurred. But if I have LazyColumn instead of Column as in the following:
var timer by remember { mutableStateOf(0) }
LazyColumn {
item(contentType = "timer") {
Text(text = timer.toString())
}
item {
Button(onClick = { timer++ }) {
Text(text = "Add")
}
}
}
Recomposition does not occur, Do not we expect it to be recomposed since the state has been changed?

When a State is read it triggers recomposition in nearest scope.
First example:
var timer by remember { mutableStateOf(0) }
SideEffect {
Log.i("info", "xxxx")
}
LogCompositions("JetpackCompose.app", "Composable scope")
LazyColumn {
item(contentType = "timer") {
LogCompositions("JetpackCompose.app", "Item Text Scope")
Text(text = timer.toString())
}
item {
LogCompositions("JetpackCompose.app", "Item Button Scope")
Button(onClick = { timer++ }) {
LogCompositions("JetpackCompose.app", "Button Scope")
Text(text = "Add")
}
}
}
When the screen is displayed you can find:
Compositions: Composable scope 0
Compositions: Item Text Scope 0
Compositions: Item Button Scope 0
Compositions: Button Scope 0
I xxxx
When the Button "Add" is clicked only the item with the Text is recomposed.
Compositions: Item Text Scope 1
It happens because it recompose the scope that are reading the values that changes: Text(text = timer.toString()).
Using a Column instead of a LazyColumn:
var timer by remember { mutableStateOf(0) }
SideEffect {
Log.i("info", "xxxx")
}
LogCompositions("JetpackCompose.app", "Composable scope")
Column {
LogCompositions("JetpackCompose.app", "Column Scope")
Text(text = timer.toString())
Button(onClick = { timer++ }) {
LogCompositions("JetpackCompose.app", "Button Scope")
Text(text = "Add")
}
}
When the screen is diplayed:
Compositions: Composable scope 0
Compositions: Column Scope 0
Compositions: Button Scope 0
I xxxx
When the Button "Add" is clicked all the composable is recomposed:
Compositions: Composable scope 1
Compositions: Column Scope 1
I xxxx
Also in this case it happens because it recompose the scope that are reading the values that changes: Text(text = timer.toString()).
The scope is a function that is not marked with inline and returns Unit.
The Column is an inline function and it means that Column doesn't have an own recompose scopes.
To log composition use this composable:
class Ref(var value: Int)
// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
#Composable
inline fun LogCompositions(tag: String, msg: String) {
if (BuildConfig.DEBUG) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
Log.d(tag, "Compositions: $msg ${ref.value}")
}
}

I do not know what do you mean by saying Recomposition does not occur. The text is updating in both peace of code you have in the question

Related

How to trigger JetPack recomposition when state is re-assigned it's current value

The below code works as desired: the canvas gets recomposed each time the user either clicks the canvas itself or clicks the topBar icon, no matter how many times or in what order. In addition, the state variable value reveals something I want to know: where the user clicked. (Values 0 and 1 mean the icon was clicked and values 2 and 3 mean the canvas).
However, if the canvasState and iconState variables are set to their respective V1 functions instead of the V2 functions, then clicking the canvas or icon multiple times in a row is not detected. Apparently this is because the V1 functions can re-assign the same value to the state variable, unlike the V2 functions.
Since I'm using the neverEqualPolicy(), I thought I didn't have to assign a different value to the state variable to trigger a recompose. As a noob to Kotlin and Compose, what am I misunderstanding?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
#Composable
fun MyApp() {
var state by remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
val canvasStateV1 = { state = 0 }
val iconStateV1 = { state = 2 }
val canvasStateV2 = { state = if (state == 0) { 1 } else { 0 } }
val iconStateV2 = { state = if (state == 2) { 3 } else { 2 } }
val iconState = iconStateV2
val canvasState = canvasStateV2
Scaffold(
topBar = { TopBar(canvasState) },
content = { padding ->
Column(Modifier.padding(padding)) {
Screen(state, iconState)
}
}
)
}
#Composable
fun TopBar(iconState: () -> Unit) {
TopAppBar(
title = { Text("This is a test") },
actions = {
IconButton(onClick = { iconState() }) {
Icon(Icons.Filled.AddCircle, null)
}
}
)
}
#Composable
fun Screen(state: Int, canvasState: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.aspectRatio(ratio = 1f)
.background(color = MaterialTheme.colors.onSurface)
.pointerInput(Unit) {
detectTapGestures(
onTap = { canvasState() },
)
}
) {
Canvas(
modifier = Modifier.fillMaxSize().clipToBounds()
) {
Log.d("Debug", "Canvas: state = $state")
}
}
}
}
I didn't know other things to try to get the neverEqualPolicy() to work as expected.
I think the main reason for this is because the function Screen() is skippable. If you add the state as a MutableState instead of the Int itself, you will see that the Log.d gets called each time the state value gets updated. Same goes for merging the Screen() function into Column in MyApp
Compose analyses each function during build time. The screen functions receives an integer value, this is an immutable value, so the function itself becomes skippable.
To analyse which function is skippable/stable (and which is not), you can run a report during the build phase
This repo shows how
EDIT:
In this example you have two buttons, one changes the value, one just sets the same value. When setting the same value, you only see the Log.d of the local recomposition. When changing the state value, you see two log lines. the local and external both go through the recomposition.
#Composable
fun StackOverflowApp() {
var state by remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
Column() {
Button(onClick = { state = state }) {
Text(text = "State same value")
}
Button(onClick = { state += 1 }) {
Text(text = "State up")
}
Text(text = "[local] current State = $state")
Log.d("TAG","Recomposition local")
ExternalText(state)
}
}
/**
* A skippable function
*
* restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ExternalText(
stable state: Int
)
*/
#Composable
fun ExternalText(state: Int){
Text(text = "[external] current State = $state")
Log.d("TAG","Recomposition external")
}
You can also pass the MutableState instead of the int value itself, when you pass the mutableState, the neverEqualPolicy is still in play. Each interaction fires both log lines
#Composable
fun StackOverflowApp() {
var state = remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
Column() {
Button(onClick = { state.value = state.value }) {
Text(text = "State same value")
}
Button(onClick = { state.value += 1 }) {
Text(text = "State up")
}
Text(text = "[local] current State = ${state.value}")
Log.d("TAG","Recomposition internal")
ExternalText(state)
}
}
#Composable
fun ExternalText(state: MutableState<Int>){
Text(text = "[external] current State = ${state.value}")
Log.d("TAG","Recomposition external")
}

Jetpack recomposition behavior

When state changes recomposition should fire. In code sample one state changes but recomposition doesn't fire but in code sample two state changes onButtonClick and recomposition fires.
How to make code sample one to recompose on state change?
Below code does not fire recomposition
#Composable
fun doSomething(){
val context = LocalContext.current
val scope = rememberCoroutineScope()
var shouldDo by remember{ mutableStateOf(false) }
LaunchedEffect(context){
scope.launch(Dispatchers.Default){
//Fetch a data from dataSource
//then change the state
withContext(Dispatchers.Main){
shouldDo = true
}
}
}
//Process the data when #shouldDo state changes to true
if(shouldDo){
Log.e("=======================", "shouldDo: $shouldDo")
}
}
But this code fire recomposition upon Button Click
#Composable
fun doSomeOtherThing() {
var shouldDo by remember{ mutableStateOf(false) }
if(shouldDo){
Log.e("=======================", "shouldDo: $shouldDo")
}
Box(modifier = Modifier.fillMaxSize()){
Button(
modifier = Modifier.align(Alignment.Center),
onClick = {
shouldDo = true
}
) {
Text(text = "Button")
}
}
}
How to make code sample one to recompose on state change?
You should use a SideEffect to mutate state during first composition in order for these changes to take "effect" :
#Composable
fun doSomething() {
var shouldDo by remember { mutableStateOf(false) }
if (shouldDo) {
Log.e("=======================", "shouldDo: $shouldDo")
}
SideEffect {
shouldDo = true
}
}
Further reading : https://developer.android.com/jetpack/compose/side-effects

Clean TextField when BottomSheetScaffold collapse on Jetpack Compose

I'm having a little trouble adding a form inside a Bottom sheet because every time I open the bottomSheet, the previous values continue there. I'm trying to make something like this
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheet() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
},
sheetPeekHeight = 0.dp
) {
Button(onClick = {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.expand()
}
}) {
Text(text = "Expand")
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun Form(
onSaveFoo: (String) -> Unit
) {
var foo by remember { mutableStateOf("") }
Column {
Button(onClick = {
onSaveFoo(foo)
}) {
Text(text = "Save")
}
OutlinedTextField(value = foo, onValueChange = { foo = it })
}
}
There is a way to "clean" my form every time the bottom sheet collapses without manually setting all values to "" again?
Something like the BottomShettFragment. If I close and reopen the BottomSheetFragment, the previous values will not be there.
Firstly, they say that it is better to control your state outside of a composable function (in a viewmodel) and pass it as a parameter.
You may clear the textField value, when you decide to collapse your bottomSheet, for example in onSaveFoo function.
Add a MutableStateFlow to your viewmodel, subscribe to its updates via collectAsState extension in your composable. You can get a viewmodel by a composable function viewModel(ViewModelClass::class.java).
In onSaveFoo function update your state with new string or empty string if that's the behaviour you want to achieve. State updates should happen inside viewmodel. So create a method in your viewmodel to update your state and call it when you want to collapse your bottomsheet to clear the text contained in your state.
And another thing, remember saves the value across recompositions. The value will be lost only if your Composable is removed from the Composition. It will happen if you change the content of your bottomSheet.
Something like this:
sheetContent = {
if(bottomSheetScaffoldState.bottomSheetState.isExpanded){
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}else{
Spacer(modifier=Modifier.height(16.dp).background(Color.White)//or some other composable
}
},

How can Android Studio launch the inline fun <T> key()?

The Code A is from the offical sample project here.
The Code B is from Android Studio source code.
I have searched the article about the function key by Google, but I can't find more details about it.
How can Android Studio launch the inline fun <T> key()? Why can't the author use Code C to launch directly?
Code A
key(detailPost.id) {
LazyColumn(
state = detailLazyListState,
contentPadding = contentPadding,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxSize()
.notifyInput {
onInteractWithDetail(detailPost.id)
}
) {
stickyHeader {
val context = LocalContext.current
PostTopBar(
isFavorite = hasPostsUiState.favorites.contains(detailPost.id),
onToggleFavorite = { onToggleFavorite(detailPost.id) },
onSharePost = { sharePost(detailPost, context) },
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
)
}
postContentItems(detailPost)
}
}
Code B
#Composable
inline fun <T> key(
#Suppress("UNUSED_PARAMETER")
vararg keys: Any?,
block: #Composable () -> T
) = block()
Code C
LazyColumn(
state = detailLazyListState,
contentPadding = contentPadding,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxSize()
.notifyInput {
onInteractWithDetail(detailPost.id)
}
) {
stickyHeader {
val context = LocalContext.current
PostTopBar(
isFavorite = hasPostsUiState.favorites.contains(detailPost.id),
onToggleFavorite = { onToggleFavorite(detailPost.id) },
onSharePost = { sharePost(detailPost, context) },
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
)
}
postContentItems(detailPost)
}
From key documentation:
key is a utility composable that is used to "group" or "key" a block of execution inside of a composition. This is sometimes needed for correctness inside of control-flow that may cause a given composable invocation to execute more than once during composition.
It also contains several examples, so check it out.
Here is a basic example of the usefulness of it. Suppose you have the following Composable. I added DisposableEffect to track its lifecycle.
#Composable
fun SomeComposable(text: String) {
DisposableEffect(text) {
println("appear $text")
onDispose {
println("onDispose $text")
}
}
Text(text)
}
And here's usage:
val items = remember { List(10) { it } }
var offset by remember {
mutableStateOf(0)
}
Button(onClick = {
println("click")
offset += 1
}) {
}
Column {
items.subList(offset, offset + 3).forEach { item ->
key(item) {
SomeComposable(item.toString())
}
}
}
I only display two list items, and move the window each time the button is clicked.
Without key, each click will remove all previous views and create new ones.
But with key(item), only the disappeared item disappears, and the items that are still on the screen are reused without recomposition.
Here are the logs:
appear 0
appear 1
appear 2
click
onDispose 0
appear 3
click
onDispose 1
appear 4
click
onDispose 2
appear 5

Android Compose: can composables store more than a single state?

from the official docs (Managing State)
Composable functions can store a single object in memory by using the remember composable. A value computed by remember is stored in the composition during initial composition, and that stored value is returned during recomposition. You can use remember to store both mutable and immutable objects.
so a single remember per composable, but I've found code online that used more than a single state in a composable, actually from an official source :DropdownMenu
#Composable
fun DropdownDemo() {
var expanded by remember { mutableStateOf(false) }
var selectedIndex by remember { mutableStateOf(0) }
val items = listOf("A", "B", "C", "D", "E", "F")
val disabledValue = "B"
Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
Text(items[selectedIndex],modifier = Modifier.fillMaxWidth().clickable(onClick = { expanded = true }).background(
Color.Gray))
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.fillMaxWidth().background(
Color.Red)
) {
items.forEachIndexed { index, s ->
DropdownMenuItem(onClick = {
selectedIndex = index
expanded = false
}) {
val disabledText = if (s == disabledValue) {
" (Disabled)"
} else {
""
}
Text(text = s + disabledText)
}
}
}
}
}
the code works fine, but it stores two states, doesn't it ?!
I assume that documentation meant, that using one remember you can store one object, not that each composable can't have multiple remember values.

Categories

Resources