Jetpack compose does not rerender when flow of list changes - android

I have following composable
#Composable
fun ManageCredential(
manageCredentialViewModel: ManageCredentialViewModel
) {
val expandedList by manageCredentialViewModel.expandedList.collectAsState()
val text = "Hello"
Scaffold(
modifier = Modifier.padding(top = 100.dp),
content = { padding ->
Image(
painter = if (expandedList.contains(text)) painterResource(id = R.drawable.drop_down_expand) else painterResource(
id = R.drawable.drop_down_collapse
),
contentDescription = text, modifier = Modifier.padding(padding).clickable {
if (expandedList.contains(text)) {
manageCredentialViewModel.removeFromList(text)
} else {
manageCredentialViewModel.addToList(text)
}
}
)
})
}
The view model is as follows
#HiltViewModel
class ManageCredentialViewModel #Inject constructor(
) : ViewModel() {
private val _expandedList = MutableStateFlow<MutableList<String>>(mutableListOf())
val expandedList = _expandedList.asStateFlow()
fun addToList(value: String) {
_expandedList.value.add(value)
}
fun removeFromList(value: String) {
_expandedList.value.remove(value)
}
}
I just want to toggle the image if a particular text is added to the list but it never happens

For recomposition to be triggered you need to change value of State. What you currently do is adding or removing items from existing list.
You can create SnapshotStateList with mutableStateListOf. Any changes in this list by removing, adding or updating an existing item with a new item, easily can be done with data class copy function, you can trigger recomposition and good thing about it is it only triggers recomposition for the Composable that reads that item such as LazyColumn item.
You can refer this answer also
Jetpack Compose lazy column all items recomposes when a single item update

Related

Jetpack Compose: LazyColumn not updating with MutableList

I am trying to strikethrough a text at the click of a button using Jetpack Compose but the UI is not updating.
I have tried to set a button and on the click of a button, deletedTasks gets updated and if my task is in deletedTasks then it should show up with a strikethrough.
This change only happens manually but I want it to be automatic
I am not sure how to go about this. This is my first time trying Jetpack Compose, any help would be appreciated
#Composable
fun toDoItem(title: String, category: String, task: Task) {
val rememberTask = remember { mutableStateOf(deletedTasks) }
Surface(...) {
Column (...) {
Row {
Column (...) {
if (!deletedTasks.contains(task)){
Text(text = title, style = MaterialTheme.typography.h4.copy(
fontWeight = FontWeight.Bold
))
} else {
Text(text = title, style = MaterialTheme.typography.h4.copy(
fontWeight = FontWeight.Bold
), textDecoration = TextDecoration.LineThrough)
}
}
IconButton(onClick = {
rememberTask.value.add(task)
}) {
Icon(imageVector = Icons.Filled.Check, contentDescription = "Check")
}
}
}
}
}
#Composable
fun recyclerView(tasks: MutableList<Task>) {
LazyColumn (modifier = Modifier.padding(vertical = 10.dp, horizontal = 80.dp)) {
items(items = tasks) {task ->
toDoItem(task.title, task.category, task)
}
}
}
You are using MutableList<>, which is not recommended for any collection use-cases in Jetpack Compose. I would advice changing it to a SnapshotStateList.
Your recyclerView composable would look like this
#Composable
fun recyclerView(tasks: SnapshotStateList<Task>) { ... }
and somewhere where you set things up (e.g ViewModel) would be like
tasks = mutableStateListOf<Task>( ... )
Using SnapshotStateList will guarantee an "update" or re-composition with any normal list operation you do to it such as,
list.add( <new task> )
list.remove( <selected task> )
list[index] = task.copy() <- idiomatic way of udpating a list
There is also SnapshotStateMap which is for Map key~value pair use-case.
But if you are curious as to how you would update your LazyColumn with an ordinary List, you would have to re-create the entire MutableList<Task> so that it will notify the composer that an update is made because the MutableList<Task> now refers to a new reference of collection of tasks.

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
}
},

Jetpack Compose, how to reset LazyColumn position when new data are set?

I'm working on a search page made in Compose with LazyColumn, everything works fine except for the wanted behavior of LazyColumn returing to first item when data changes.
This is my actual implementation of lazy column:
#Composable
fun <DataType : Any> GenericListView(
itemsList: SnapshotStateList<DataType>, // this list comes from the search page viewmodel
modifier: Modifier = Modifier.fillMaxSize(),
spacing: Dp = 24.dp,
padding: PaddingValues = PaddingValues(0.dp),
item: #Composable (DataType) -> Unit
) {
val listState: LazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing),
state = listState,
modifier = modifier.padding(padding)
) {
items(itemsList) {
item(it)
}
}
SideEffect {
Log.i("->->->->->->->", "side effect launched")
coroutineScope.launch {
listState.scrollToItem(0)
}
}
}
As docs says, SideEffect should be called everytime the function is recomposed,
but it appear to be working only in debug mode with breakpoints in SideEffect, otherwise, it works only when the whole page is first created.
I've already tried with LaunchedEffect instead of SideEffect, using itemsList as key, but nothing happened.
Why my code works only in debug mode ?
Or better, an already made working solution to reset position when new data are set ?
SideEffect doesn't work because Compose is not actually recomposing the whole view when the SnapshotStateList is changed: it sees that only LazyColumn is using this state value so only this function needs to be recomposed.
To make it work you can change itemsList to List<DataType> and pass plain list, like itemsList = mutableStateList.toList() - it'll force whole view recomposition.
LaunchedEffect with passed SnapshotStateList doesn't work for kind of the same reason: it compares the address of the state container, which is not changed. To compare the items itself, you again can convert it to a plain list: in this case it'll be compared by items hash.
LaunchedEffect(itemsList.toList()) {
}
You can achieve the mentioned functionality with SideEffect, remember and with some kind of identificator (listId) of the list items. If this identificator changes, the list will scroll to the top, otherwise not.
I have extended your code. (You can choose any type for listId.)
#Composable
fun <DataType : Any> GenericListView(
itemsList: SnapshotStateList<DataType>, // this list comes from the search page viewmodel
modifier: Modifier = Modifier.fillMaxSize(),
spacing: Dp = 24.dp,
padding: PaddingValues = PaddingValues(0.dp),
listId: String? = null,
item: #Composable (DataType) -> Unit
) {
var lastListId: String? by remember {
mutableStateOf(null)
}
val listState: LazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing),
state = listState,
modifier = modifier.padding(padding)
) {
items(itemsList) {
item(it)
}
}
SideEffect {
Log.i("->->->->->->->", "side effect launched")
coroutineScope.launch {
if (lastListId != listId) {
lastListId = listId
listState.scrollToItem(0)
}
}
}
}

Update Jetpack Compose list with scroll position reset

In my app I have list of items shown with LazyColumn. My list can update fields and sometime reorder items. Whenever I have reorder of items I need to show list with first item at the top. I came up with setup like the one below. List scrolls to the top only on the first reorder event. On consecutive list update with reorder = true, my list not scrolling to the top. What is the better approach in Compose to reset list scroll position (or even force rebuilding compose list) by sending state from ViewModel?
class ViewItemsState(val items: List<ViewItem>, val scrollToTop: Boolean)
class ExampleViewModel() : ViewModel() {
var itemsLiveData = MutableLiveData<ViewItemsState>()
}
#Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val itemState by viewModel.itemsLiveData.observeAsState()
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
if (itemState.scrollToTop) {
LaunchedEffect(coroutineScope) {
Log.e("TAG", "TopCoinsScreen: scrollToTop" )
listState.scrollToItem(0)
}
}
LazyColumn(state = listState) {
items(itemState.items) { item ->
ItemCompose(
item.name,
item.value
)
}
}
}
Recomposition is triggered only when a state changes.
Two things to fix in this,
Reset scrollToTop to false once scrolling completes.
Store scrollToTop in the view model as a MutableState, LiveData, Flow, or any other reactive element.
Currently scrollToTop is stored as a boolean in a data class object. Resetting the value will not trigger a Composable recomposition.
Example code with sample data,
class ViewItemsState(
val items: List<String>,
)
class ExampleViewModel : ViewModel() {
private var _itemsLiveData =
MutableLiveData(
ViewItemsState(
items = Array(20) {
it.toString()
}.toList(),
)
)
val itemsLiveData: LiveData<ViewItemsState>
get() = _itemsLiveData
private var _scrollToTop = MutableLiveData(false)
val scrollToTop: LiveData<Boolean>
get() = _scrollToTop
fun updateScrollToTop(scroll: Boolean) {
_scrollToTop.postValue(scroll)
}
}
#Composable
fun ExampleScreen(
viewModel: ExampleViewModel = ExampleViewModel(),
) {
val itemState by viewModel.itemsLiveData.observeAsState()
val scrollToTop by viewModel.scrollToTop.observeAsState()
val listState = rememberLazyListState()
LaunchedEffect(
key1 = scrollToTop,
) {
if (scrollToTop == true) {
listState.scrollToItem(0)
viewModel.updateScrollToTop(false)
}
}
Column {
LazyColumn(
state = listState,
modifier = Modifier.weight(1f),
) {
items(itemState?.items.orEmpty()) { item ->
Text(
text = item,
modifier = Modifier.padding(16.dp),
)
}
}
Button(
onClick = {
viewModel.updateScrollToTop(true)
},
) {
Text(text = "Scroll")
}
}
}

Jetpack Compose #Stable List<T> parameter recomposition

#Composable functions are recomposed
if one the parameters is changed or
if one of the parameters is not #Stable/#Immutable
When passing items: List<Int> as parameter, compose always recomposes, regardless of List is immutable and cannot be changed. (List is interface without #Stable annotation). So any Composable function which accepts List<T> as parameter always gets recomposed, no intelligent recomposition.
How to mark List<T> as stable, so compiler knows that List is immutable and function never needs recomposition because of it?
Only way i found is wrapping like #Immutable data class ImmutableList<T>(val items: List<T>). Demo (when Child1 recomposes Parent, Child2 with same List gets recomposed too):
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeBasicsTheme {
Parent()
}
}
}
}
#Composable
fun Parent() {
Log.d("Test", "Parent Draw")
val state = remember { mutableStateOf(false) }
val items = remember { listOf(1, 2, 3) }
Column {
// click forces recomposition of Parent
Child1(value = state.value,
onClick = { state.value = !state.value })
//
Child2(items)
}
}
#Composable
fun Child1(
value: Boolean,
onClick: () -> Unit
) {
Log.d("Test", "Child1 Draw")
Text(
"Child1 ($value): Click to recompose Parent",
modifier = Modifier
.clickable { onClick() }
.padding(8.dp)
)
}
#Composable
fun Child2(items: List<Int>) {
Log.d("Test", "Child2 Draw")
Text(
"Child 2 (${items.size})",
modifier = Modifier
.padding(8.dp)
)
}
You mainly have 2 options:
Use a wrapper class annotated with either #Immutable or #Stable (as you already did).
Compose compiler v1.2 added support for the Kotlinx Immutable Collections library.
With Option 2 you just replace List with ImmutableList.
Compose treats the collection types from the library as truly immutable and thus will not trigger unnecessary recompositions.
Please note: At the time of writing this, the library is still in alpha.
I strongly recommend reading this article to get a good grasp on how compose handles stability (plus how to debug stability issues).
Another workaround is to pass around a SnapshotStateList.
Specifically, if you use backing values in your ViewModel as suggested in the Android codelabs, you have the same problem.
private val _myList = mutableStateListOf(1, 2, 3)
val myList: List<Int> = _myList
Composables that use myList are recomposed even if _myList is unchanged. Opt instead to pass the mutable list directly (of course, you should treat the list as read-only still, except now the compiler won't help you).
Example with also the wrapper immutable list:
#Immutable
data class ImmutableList<T>(
val items: List<T>
)
var itemsList = listOf(1, 2, 3)
var itemsImmutable = ImmutableList(itemsList)
#Composable
fun Parent() {
Log.d("Test", "Parent Draw")
val state = remember { mutableStateOf(false) }
val itemsMutableState = remember { mutableStateListOf(1, 2, 3) }
Column {
// click forces recomposition of Parent
Child1(state.value, onClick = { state.value = !state.value })
ChildList(itemsListState) // Recomposes every time
ChildImmutableList(itemsImmutableListState) // Does not recompose
ChildSnapshotStateList(itemsMutableState) // Does not recompose
}
}
#Composable
fun Child1(
value: Boolean,
onClick: () -> Unit
) {
Text(
"Child1 ($value): Click to recompose Parent",
modifier = Modifier
.clickable { onClick() }
.padding(8.dp)
)
}
#Composable
fun ChildList(items: List<Int>) {
Log.d("Test", "List Draw")
Text(
"List (${items.size})",
modifier = Modifier
.padding(8.dp)
)
}
#Composable
fun ChildImmutableList(items: ImmutableList<Int>) {
Log.d("Test", "ImmutableList Draw")
Text(
"ImmutableList (${items.items.size})",
modifier = Modifier
.padding(8.dp)
)
}
#Composable
fun ChildSnapshotStateList(items: SnapshotStateList<Int>) {
Log.d("Test", "SnapshotStateList Draw")
Text(
"SnapshotStateList (${items.size})",
modifier = Modifier
.padding(8.dp)
)
}
Using lambda, you can do this
#Composable
fun Parent() {
Log.d("Test", "Parent Draw")
val state = remember { mutableStateOf(false) }
val items = remember { listOf(1, 2, 3) }
val getItems = remember(items) {
{
items
}
}
Column {
// click forces recomposition of Parent
Child1(value = state.value,
onClick = { state.value = !state.value })
//
Child2(items)
Child3(getItems)
}
}
#Composable
fun Child3(items: () -> List<Int>) {
Log.d("Test", "Child3 Draw")
Text(
"Child 3 (${items().size})",
modifier = Modifier
.padding(8.dp)
)
}

Categories

Resources