Search code examples
androidkotlinandroid-roomkotlin-coroutineskotlin-flow

Update Entity in room as isCompleted and get all data using Flow problem


I have a Habits app that the user can add new habits with different types and mark the habit as completed/notCompleted.

The Habits fragment displays habits with two different habits types "Positive" and "Negative" using chips, so When The user checks the Positive chip the recyclerView gets the positive habits and the same thing with the negative chip.

Here's How this operation work.

This is my DAO Code

@Query("SELECT * FROM HABIT_TABLE WHERE type = :type ORDER BY isCompleted ASC")
fun getAllHabitsByType(type: String): Flow<List<HabitEntity>>

@Query("UPDATE HABIT_TABLE SET isCompleted = :isCompleted WHERE habitId = :habitId")
suspend fun updateHabitByCompleted(habitId: Long, isCompleted: Boolean)

And In my Repository, I map from "HabitEntity" to "HabitModel". So the function should return Flow<List< Habit>>.

override fun getAllHabitsByType(type: HabitType): Flow<List<Habit>> {
    return channelFlow {
        dao.getAllHabitsByType(type.pathName).collect { habits ->
            send(habitMapper.map(habits))
        }
    }
}

override suspend fun updateHabitByCompleted(habit: Habit, isCompleted: Boolean) {
    dao.updateHabitByCompleted(habit.id, isCompleted)
}

I tried to map the flow that returns from dao into the repo function and then emit it to ViewModel but it didn't work, it should collect the data and then send it to ViewModel like the function does above. This is what I did before.

override fun getAllHabitsByType(type: HabitType): Flow<List<Habit>> {
    return flow { 
        dao.getAllHabitsByType(type.pathName).map { 
            emit(habitMapper.map(it))
        }
    }
}

Ok, after that I collect the latest changes in ViewModel and observe them in RecyclerView. Here's my ViewModel function.

private val _habitsList = MutableLiveData<List<Habit>>()
val habitsList: LiveData<List<Habit>> get() = _habitsList

private var currentHabitType = HabitType.POSITIVE

private fun getHabitsByType(habitType: HabitType) {
    viewModelScope.launch {
        repository.getAllHabitsByType(habitType).collectLatest {
            _habitsList.postValue(it)
        }
    }
}

override fun updateHabitByCompleted(habit: Habit, isCompleted: Boolean) {
    viewModelScope.launch {
        repository.updateHabitByCompleted(habit, isCompleted)
        getHabitsByType(currentHabitType)
    }
}

fun onChipTypeClick(habitType: HabitType) {
    currentHabitType = habitType
    getHabitsByType(habitType)
}

And Here's my HabitsFragment.

lateinit var habitsAdapter: HabitsAdapter

private fun initRecyclerVIew() {
    habitsAdapter = HabitsAdapter(emptyList(), viewModel)
    binding.habitsRecyclerView.adapter = habitsAdapter
}

private fun observeEvents() {
    viewModel.apply {
        ....
        habitsList.observe(viewLifecycleOwner) {
            habitsAdapter.setItems(it)
        }
    }
}

Chips XML code In Habits Fragment

<data>

    <variable
        name="viewModel"
        type="com.moataz.mohareb.presentation.habits.viewmodel.HabitsViewModel" />

    <variable
        name="habitType"
        type="com.moataz.mohareb.core.enums.HabitType" />
</data>

<com.google.android.material.chip.ChipGroup
    style="@style/Widget.Material3.Chip.Suggestion"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:selectionRequired="true"
    app:singleSelection="true">

     <com.google.android.material.chip.Chip
         style="@style/ChipStyle"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:checked="true"
         android:onClick="@{() -> viewModel.onChipTypeClick(habitTYpe.POSITIVE)}"
         android:text="@string/good_habit"
         app:chipStrokeColor="@android:color/transparent" />

     <com.google.android.material.chip.Chip
         style="@style/ChipStyle"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:onClick="@{() -> viewModel.onChipTypeClick(habitTYpe.NEGATIVE)}"
         android:text="@string/bad_habit"
         app:chipStrokeColor="@android:color/transparent" />
</com.google.android.material.chip.ChipGroup>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/habits_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="6dp"
        android:orientation="vertical"
        android:overScrollMode="never"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/horizontal_chips_scroll_view"
        tools:listitem="@layout/item_habit" />

And here's the code of the view changes when clicking on CheckBox completed in the habit using databiding.

    @BindingAdapter(value = ["app:changeTextStatue"])
    fun changeTextStatue(textView: TextView, isCompleted: Boolean) {
        if (isCompleted) {
            textView.paintFlags = textView.paintFlags or android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
        } else {
            textView.paintFlags =
                textView.paintFlags and android.graphics.Paint.STRIKE_THRU_TEXT_FLAG.inv()
        }
    }

   @BindingAdapter(value = ["app:changeCheckBoxStatue"])
   fun changeCheckBoxStatue(checkBox: CheckBox, isCompleted: Boolean) {
        checkBox.isChecked = isCompleted == true
    }

The problem that I have

When I select the first chip to display the data and mark it as completed or not, it works fine, and the data updates without any problems. see this video plz to get a full understanding https://youtube.com/shorts/bdRd70Me5nk?feature=share

And If I want to move from the first chip to another to get different habit types without completing any habit, It also works very well. https://youtube.com/shorts/t0Ma0BAE_Tw?feature=share

What If I want to mark a habit as completed and want to move from chip good habits to chip bad habits? And also if I have completed habits and want to move to another chip. Here's the problem in these two Situations. The RecycleView performs a very strange beehive process. https://www.youtube.com/shorts/6juhhWxq6_Y

I have tried to search for this problem for 4 days, but I didn't find any solutions that are useful or any that give me a full understanding of my problem.

Note:

  • I have tried to use the "@UPDATE" query in dao to update the entity and also the same problem happens.
  • I also have tried to change the getAllHabitsByType function from (Flow to suspend in dao), and it works well without any problem when changing a habit from notCompleted to completed and moving to another chip, but of course, I need to call the getAllHabits fun in ViewModel again when update the habit state to update the recyclerview, and the problem when add new habit from another fragment, I need the flow to get the update to my recyclerview at the moment of added new habit **

Here's my project source on github to get a full understanding of my code https://github.com/MoatazBadawy/Mohareb

Thank you.


Solution

  • I have basically the same type of UI in my app so there definitely is a way.

    I think the channelFlow is the main culprit but there is also a lot of callbacks that complicate determining what exactly spams setting the items in the list.

    override fun getAllHabitsByType(type: HabitType): Flow<List<Habit>> {
        return dao.getAllHabitsByType(type.pathName).map { habits ->
            habitMapper.map(habits)
        }
    }
    

    This should definitely work.

    The main benefit of LiveData/Flow is that you do not need to manually update it, it does it automatically. There is/can be no need to call getHabitsByType. You just need to make those inputs Flow/LiveData as well.

    private val currentHabitType = MutableStateFlow(HabitType.POSITIVE)
    
    private val _habitsList = currentHabitType.flatMapLatest { type ->
        repository.getAllHabitsByType(type)
    }
    
    val habitsList: LiveData<List<Habit>> get() = _habitsList.asLiveData()
    

    That should all you need to cover automatic updates from database and from any changes via the chips.