How to use MutableStateFlow to search item in list - android

I am learning kotlin flow in android. I want to basically instant search in my list and filter to show in reyclerview. I searched in google and found this amazing medium post. This post is basically search from google. I want to search item in list and show in reyclerview. Can someone guide me how can I start this. I am explanning in more detail
Suppose I have one SearchBox and one Reyclerview which one item abc one, abc two, xyz one, xyz two... etc.
main image when all data is combine
Scenario 1
when I start typing in SearchBox and enter small a or capital A I want to show only two item matching in recyclerview, look like this
Scenario 2
when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this
Any guidance would be great. Thanks
I am adding my piece of code
ExploreViewModel.kt
class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {
val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
var topicSelected: TopicsArea? = TopicsArea.ALL
set(value) {
field = value
handleTopicSelection(field ?: TopicsArea.ALL)
}
private fun handleTopicSelection(value: TopicsArea) {
if (value == TopicsArea.ALL) {
filterAllCategories(true)
} else {
filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
?.topics?.sortedBy { topic -> topic.title }.orEmpty()
}
}
fun filterAllCategories(isAllCategory: Boolean) {
if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
list.add(0, code = TopicsArea.ALL.categoryCode))
} else if (isFirstItemIsAllCategory()) {
list.removeAt(0)
}
filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
}
private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
app:closeIcon="#drawable/ic_cancel"
app:layout_constraintBottom_toTopOf="#+id/exploreScroll"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<HorizontalScrollView
android:id="#+id/exploreScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/searchView">
<com.google.android.material.chip.ChipGroup
android:id="#+id/exploreChips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:singleLine="true"
app:singleSelection="true" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/exploreList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="20dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/exploreScroll" />
</androidx.constraintlayout.widget.ConstraintLayout>
Category.kt
#Parcelize
data class Category(
val id: String? = null,
val title: String? = null,
val code: String? = null,
val topics: List<Topics>? = null,
) : Parcelable
Topics.kt
#Parcelize
data class Topics(
val id: String? = null,
val title: String? = null
) : Parcelable
Dummy data and coming from server
fun categoriesList() = listOf(
Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)
In my view model list is holding above dummy data. And In my recyclerview I am passing the whole object and I am doing flatMap to combine all data into list. Make sure In recyclerview is using Topic and using title property. In Image Abc one, Abc two is holding in Topic. Thanks
After #Tenfour04 suggestion I will go to A2 suggestion because I have already data which converted into flow and passing in my adapter. I am adding my activity code as well.
ExploreActivity.kt
class ExploreActivity : AppCompatActivity() {
private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
val viewModel by viewModel<ExploreViewModel> {
val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
parametersOf(list)
}
var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupView()
}
fun setupView() {
setupSearchView()
setupFilteredTopic()
setupExploreAdapter()
}
private fun setupFilteredTopic() {
lifecycleScope.launchWhenCreated {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.filteredTopics.collect { filteredTopicsList ->
exploreAdapter.submitList(filteredTopicsList)
}
}
}
}
fun setupSearchView() {
binding.searchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
}
}
fun setupExploreAdapter() {
with(binding.exploreList) {
adapter = exploreAdapter
}
}
}
UPDATE 2
ExploreViewModel.kt
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(filteredTopics) { queryText, categoriesList ->
val criteria = queryText.lowercase()
if (criteria.isEmpty()) {
return#combine filteredTopics
} else {
categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
}
}
I am getting error when I set in adapter
fixed
filteredTopics.value

The tutorial you linked has a Flow produced by the SearchView. If you want to keep the search functionality in your ViewModel, you can put a MutableStateFlow in your ViewModel that will be updated by the SearchView indirectly. You can expose a property for updating the query.
There are two different ways this could be done, depending on whether you (A) already have a complete list of your data that you want to query quickly or (B) you want to query a server or your database every time your query text changes.
And then even (A) can be broken up into: (A1) you have a static plain old List, or (A2) your source List comes from a Flow, such as a returned Room flow that is not based on query parameters.
All code below is in the ViewModel class.
A1:
private val allCategories = categoriesList()
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
allCategories.filter { category -> criteria in category.title.lowercase }
}
A2:
In this example I put a simple placeholder flow for the upstream server query. This could be any flow.
private val allCategories = flow {
categoriesList()
}
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(allCategories) { queryText, categoriesList ->
val criteria = queryText.lowercase()
categoriesList.filter { category -> criteria in category.title.lowercase }
}
B
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(500) // maybe bigger to avoid too many queries
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
categoriesList(criteria) // up to you to implement this depending on source
}

Related

Jetpack compose lazy column not recomposing with list

I have a list which is stored inside a Viewmodel via Stateflow.
class FirstSettingViewModel : ViewModel() {
private val _mRoomList = MutableStateFlow<List<InitRoom>>(mutableListOf())
val mRoomList: StateFlow<List<InitRoom>> = _mRoomList
...
I observe the flow via collectAsState(). The LazyColumn consists of Boxes which can be clicked.
val roomList = mViewModel.mRoomList.collectAsState()
Dialog {
...
LazyColumn(...) {
items(roomList.value, key = { room -> room.room_seq}) { room ->
Box(Modifier.clickable {
**mViewModel.selectItem(room)**
}) {...}
}
}
}
When a click event occurs, the viewModel changes the 'isSelected' value via a copied list like this.
fun selectItem(room: InitRoom) = viewModelScope.launch(Dispatchers.IO) {
try {
val cpy = mutableListOf<InitRoom>()
mRoomList.value.forEach {
cpy.add(it.copy())
}
cpy.forEach {
it.isSelected = it.room_seq == room.room_seq
}
_mRoomList.emit(cpy)
} catch (e: Exception) {
ErrorController.showError(e)
}
}
When in an xml based view and a ListAdapter, this code will work well, but in the above compose code, it doesn't seem to recompose the LazyColumn at all. What can I do to re-compose the LazyColumn?
Use a SnapshotStateList instead of an ordinary List
change this,
private val _mRoomList = MutableStateFlow<List<InitRoom>>(mutableListOf())
val mRoomList: StateFlow<List<InitRoom>> = _mRoomList
to this
private val _mRoomList = MutableStateFlow<SnapshotStateList<InitRoom>>(mutableStateListOf())
val mRoomList: StateFlow<SnapshotStateList<InitRoom>> = _mRoomList

click on button using Stateflow works itself without any click

I have merged my code from livedata to stateflow to make some actions. Now I'm using StateFlow to navigate to another fragment, the problem is when I click on the button it is not working and when I used livedata it was worked!.
Here's my code of viewmodel
private val _habitsUIState = MutableStateFlow(HabitsMainUIState())
val habitsUIState: StateFlow<HabitsMainUIState> get() = _habitsUIState
private val _addHabitClickedEvent = MutableStateFlow(false)
val addHabitClickedEvent: StateFlow<Boolean> get() = _addHabitClickedEvent
private val _editHabitLongClickedEvent = MutableStateFlow(HabitUIState())
val editHabitLongClickedEvent: StateFlow<HabitUIState> get() = _editHabitLongClickedEvent
override fun onEditHabitLongClicked(habit: HabitUIState): Boolean { _editHabitLongClickedEvent.value = habit return false
}
fun onAddHabitClicked() {
_addHabitClickedEvent.value = true
}
and here is my code in fragment
private fun observeEvents() {
lifecycleScope.launch {
viewModel.apply {
habitsUIState.collectLatest { habitsMainState ->
habitsAdapter.setItems(habitsMainState.habits)
}
addHabitClickedEvent.collectLatest {
navigateToAddHabitDialog()
}
editHabitLongClickedEvent.collectLatest { habitsUIState ->
navigateToHabitEditingDialog(habitsUIState)
}
}
}
}
My button xml "I know it is not needed here but it is ok"
<ImageButton
android:id="#+id/add_habit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_margin="16dp"
android:layout_marginTop="16dp"
android:background="#drawable/circle_button_style"
android:onClick="#{() -> viewModel.onAddHabitClicked()}"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_add" />
When the code is in the state above, only the data is loaded, but the rest of the code in the lifecycleScope never work as if it has frozen!
When I tried moving each function into its own lifecycleScope (see the code down), they all worked at the same time, but some of them should work when a certain button is pressed.
private fun observeEvents() {
lifecycleScope.launch {
viewModel.habitsUIState.collectLatest { habitsMainState ->
habitsAdapter.setItems(habitsMainState.habits)
}
}
lifecycleScope.launch {
viewModel.addHabitClickedEvent.collectLatest {
navigateToAddHabitDialog()
}
}
lifecycleScope.launch {
viewModel.editHabitLongClickedEvent.collectLatest { habitsUIState ->
navigateToHabitEditingDialog(habitsUIState)
}
}
}
What problem am I facing and how can I solve it? thank you
I have tried using livedata and it was worked!

When using List as State, how to update UI when item`attribute change in Jetpack Compose?

For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())

LiveData in Nested RecyclerView

The fragment I am coding right now is supposed to give the user a calendaric overview of his meal planning schedule. So via date picker, he can choose a time period and the program will show the user which recipes he has chosen for the chosen weekdays.
So I build a nested RecyclerView with the weekdays as parent layer and corresponding recipes as a child layer. The data class for the weekday layer looks like this :
data class Weekday (
val weekday : String,
val listWithRecipes : List<Recipe>?
)
The class for the Recipe entity looks like this:
#Entity(tableName = "Recipe")
#Parcelize
data class Recipe(
#PrimaryKey var recipeName : String,
var description : String?,
var serving : Int,
var preparationTime : Int?
) : Parcelable
The Adapter for the top Recycler View like this :
class MealPlanAdapter(private var mealplan: List<Weekday>) :
RecyclerView.Adapter<MealPlanAdapter.MealPlanViewHolder>(), RecipeAdapter.OnItemClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MealPlanViewHolder {
return MealPlanViewHolder(
DailyMealplanItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun getItemCount() = mealplan.size
override fun onBindViewHolder(holder: MealPlanViewHolder, position: Int) {
val weekday = mealplan[position]
val recipeAdapter = RecipeAdapter(this)
recipeAdapter.submitList(weekday?.listWithRecipes)
holder.dayOfWeek.text = weekday.weekday
val recipeLayoutManager = LinearLayoutManager(holder.recyclerView.context,RecyclerView.VERTICAL, false)
recipeLayoutManager.initialPrefetchItemCount = 4
holder.recyclerView.apply{
layoutManager = recipeLayoutManager
adapter = recipeAdapter
}
}
fun setSchedule(mealplan : List <Weekday>){
this.mealplan = mealplan
notifyDataSetChanged()
}
inner class MealPlanViewHolder(val binding: DailyMealplanItemBinding) :
RecyclerView.ViewHolder(binding.root) {
val recyclerView: RecyclerView = binding.rvRecyclerView
val dayOfWeek: TextView = binding.tvDayOfWeek
}
override fun onItemClick(recipe: Recipe) {
TODO("Not yet implemented")
}
}
Whenever the user changes the time period, the setScheduled() method in the adapter gets called in the fragment.
materialDatePickerStartDate.addOnPositiveButtonClickListener(
MaterialPickerOnPositiveButtonClickListener<Any?> { selection ->
_binding.viewmodel!!.startDateInUTCFormat = selection as Long
_binding.tvStartDate.setText(materialDatePickerStartDate.headerText)
adapter.setSchedule(
_binding.viewmodel!!.returnListWithWeekDaysAndCorrespondingRecipes(
_binding.viewmodel!!.startDateInUTCFormat,
_binding.viewmodel!!.endDateInUTCFormat
)
)
}
)
The viewmodel looks like this :
#HiltViewModel
class MealplanViewModel #Inject constructor(
val mealPlanRepository: MealPlanRepository
) : ViewModel() {
private lateinit var _binding: FragmentMealPlanBinding
var startDateInUTCFormat: Long = System.currentTimeMillis()
var endDateInUTCFormat: Long = System.currentTimeMillis()
fun returnListWithWeekDaysAndCorrespondingRecipes(
startDate: Long,
endDate: Long
): ArrayList<Weekday> {
var startDate = Date(startDateInUTCFormat)
var endDate = Date(endDateInUTCFormat)
var startDateCalendar = dateToCalendar(startDate)
var endDateCalendar = dateToCalendar(endDate)
val calendarDays = createListWithCalendarDates(startDateCalendar, endDateCalendar)
return createListWithWeekDaysAndCorrespondingRecipes(calendarDays)
}
fun dateToCalendar(date: Date): Calendar {
var calInstance = Calendar.getInstance()
calInstance.setTime(date)
return calInstance
}
fun createListWithCalendarDates(
startDateCalendar: Calendar,
endDateCalendar: Calendar
): ArrayList<Calendar> {
var listWithCalendarDates = arrayListOf<Calendar>()
while (startDateCalendar <= endDateCalendar) {
listWithCalendarDates.add(startDateCalendar.clone() as Calendar)
startDateCalendar.add(Calendar.DATE, 1)
}
return listWithCalendarDates
}
fun createListWithWeekDaysAndCorrespondingRecipes(calendarDays: ArrayList<Calendar>): ArrayList<Weekday> {
var dayOfWeekAsString: String
var listWithDaysOfWeeksAndRecipes = arrayListOf<Weekday>()
var flattenedListWithRecipes: List<Recipe>?
for (i in 0 until calendarDays.size) {
var dayOfWeekAsInt = calendarDays[i].get(Calendar.DAY_OF_WEEK)
dayOfWeekAsString = when (dayOfWeekAsInt) {
1 -> "Sunday"
2 -> "Monday"
3 -> "Tuesday"
4 -> "Wednesday"
5 -> "Thursday"
6 -> "Friday"
else -> "Saturday"
}
var calendarDateInString =
transformCalendarDateIntoRequiredStringFormat(calendarDays[i])
var listWithDateAndCorrespondingRecipes: List<MealplanScheduleWithRecipes> =
listOf()
var liveDatalistWithDateAndCorrespondingRecipes =
mealPlanRepository.getMealplanScheduleWithRecipes(calendarDateInString)
liveDatalistWithDateAndCorrespondingRecipes.observeForever() { list ->
listWithDateAndCorrespondingRecipes = list
var listWithRecipes = listWithDateAndCorrespondingRecipes?.map { it.recipes }
flattenedListWithRecipes = listWithRecipes?.flatten()
var wochentag = dayOfWeekAsString
listWithDaysOfWeeksAndRecipes.add(Weekday(dayOfWeekAsString, flattenedListWithRecipes))
}
}
return listWithDaysOfWeeksAndRecipes
}
fun transformCalendarDateIntoRequiredStringFormat(calendarDate: Calendar): String {
var year = calendarDate.get(Calendar.YEAR)
var month = transformCalendarMonthFormatToCorrectMonth(calendarDate)
var day = calendarDate.get(Calendar.DAY_OF_MONTH)
return "$day" + "$month" + "$year"
}
fun transformCalendarMonthFormatToCorrectMonth(calendarDate: Calendar): String {
var monthCalendarFormat = calendarDate.get(Calendar.MONTH)
var monthCorrectFormat = when (monthCalendarFormat) {
0 -> "1"
1 -> "2"
2 -> "3"
3 -> "4"
4 -> "5"
5 -> "6"
6 -> "7"
7 -> "8"
8 -> "9"
9 -> "10"
10 -> "11"
else -> "12"
}
return monthCorrectFormat
}
fun datesAreReasonable(startDate: Long, endDate: Long): Boolean {
return (startDate <= endDate)
}
}
My problem is the list that is passed to the RecyclerView Adapter consists of Weekday objects, which consist of the name of the weekday and the corresponding recipes (see data class "weekday" on top).
In the method "createListWithWeekDaysAndCorrespondingRecipes" in the viewmodel I create this list in a for loop that gets all weekdays between given Dates and their corresponding recipes. However, the recipes are LiveData fetched asynchronously via Room database query while the names of the weekdays are derived synchronously in the main thread. At the end however when I create the Weekday object
(see listWithDaysOfWeeksAndRecipes.add(Weekday(dayOfWeekAsString, flattenedListWithRecipes) at the end of the for loop) I need them together at the same time. I haven't found a way how I can coordinate this successfully. At the moment the logics for adding the object to the list is in the asynchronous "observeForever" block.
See here:
liveDatalistWithDateAndCorrespondingRecipes.observeForever() { list ->
listWithDateAndCorrespondingRecipes = list
var listWithRecipes = listWithDateAndCorrespondingRecipes?.map { it.recipes }
flattenedListWithRecipes = listWithRecipes?.flatten()
var wochentag = dayOfWeekAsString
listWithDaysOfWeeksAndRecipes.add(Weekday(dayOfWeekAsString, flattenedListWithRecipes))
}
This creates wrong results, probably because the coordination between main thread and the observer thread doesn't work.
If I however take the logics of adding out of the observer block, the list with recipes will give me null, because of the asynchronous character of the query.
I know that I described the problem very badly. Maybe still someone got a grasp of it and can help?
You should try to avoid using observeForever, I expect you are using this inside a fragment or an activity which actually has a lifecyclescope that your observer can use.
Your observer should look something like this
liveDataList.observe(viewLifecycleOwner, { list ->
// The way I do it at the moment I just set the recyclerViews adapter and layoutManager here
// This is not the best way to do it, so please keep that in mind
recyclerView.apply {
adapter = MyAdapter(list)
layoutManager = LinearLayoutManager(requireContext())
}
})
// Or if used inside an activity
liveDataList.observe(this, {})
This way your observer will be attached to your lifecycle and "die" together with your view. Whenever that list changes, you will show all entities in the recyclerView. HOWEVER, when you use an Array together with LiveData the LiveData object never "updates" when you just add something to that value, since the array only is a memory reference to the start of the array.
To counter this whenever you add something to your array you need to refresh the LiveData object in order to trigger an update and all observers.
myLiveDataObject.value.add(someOtherObject)
myLiveDataObject.value = myLiveDataObject.value
myLiveDataObject.value = myLiveDataObject.value triggers all observers that there has been a change, annoying I know
If you use it inside a viewHolder or adapter simply pass the lifecycle along with the list
I am also quite new to kotlin, keep that in mind and I guarantee you there is a better way to do this, but hope it helps

Livedata don't update compose state when list item property has changed

My problem is that live data observer is triggered Observer<T> { state.value = it } with the correct data but compose doesn't kick on recompose. Only when I add an item all changes are propagated. There must some checking on the list itself if it has changed. I guess it doens't compare list items.
#Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
val lifecycleOwner = LifecycleOwnerAmbient.current
val state = remember { mutableStateOf(initial) }
onCommit(this, lifecycleOwner) {
val observer = Observer<T> { state.value = it }
observe(lifecycleOwner, observer)
onDispose { removeObserver(observer) }
}
return state
}
val items: List<TrackedActivityWithMetric> by vm.activities.observeAsState(mutableListOf())
LazyColumnForIndexed(
items = items,
Modifier.padding(8.dp)
) { index, item ->
....
MetricBlock(item.past[1], item.activity.id )
}
So behind the scenes there must be some kind hash comparing mechanism preventing rendering same item twice (More elabored answer wanted). The incorrect rendering was caused by property which was not in TrackedActivityWithMetric data class constructor.
Jetpack Compose does not work well with MutableList, you need to use a List and do something like this:
var myList: List<MyItem> by mutableStateOf(listOf())
private set
for adding an item:
fun addItem(item: MyItem) {
myList = myList + listOf(myItem)
}
for editing an item:
fun editItem(item: MyItem) {
val index = myList.indexOf(myItem)
myList = myList.toMutableList().also {
it[index] = myItem
}
}

Categories

Resources