androidkotlinandroid-livedataandroid-livedata-transformations

How to transform LiveData<List<Entity>> to LiveData<List<Date>>


My project is an expense tracker where I show a list of Dates under which I have a list of expenses that happened on those dates. I have nested RecyclerViews. Parent RecyclerView is a list of unique dates of all expenses. Child RecyclerView is list of expenses (viewed, of course, under unique dates). My ViewModel has a list of LiveData of ExpenseEntity. The ViewModel has to have a list of LiveData of Date which contains unique dates. I get my list of ExpenseEntity from a Room database.

My main fragments observes the LiveData of ExpenseEntities because then is when I need to update my parent and child recyclerviews.

I cannot figure out how to use Transformations.map to have a live transforming list of unique dates. How should I make sure the LiveData of Dates is always updated once LiveData of ExpenseEntity is updated?

MainActivityViewModel.kt

class MainActivityViewModel(private val expenseDao: ExpenseDao) : ViewModel() {
    val allExpenses : LiveData<List<ExpenseEntity>> = expenseDao.fetchAllExpenses().asLiveData()
    val uniqueDates : LiveData<List<Date>> = Transformations.map(allExpenses) {
        it.map { expense ->
            expense.date!!
        }.distinct()
    }
...
}

ExpensesFragment.kt

val factory = MainActivityViewModelFactory((activity?.application as SimpleExpenseTrackerApp).db.expenseDao())
expensesViewModel = ViewModelProvider(this, factory).get(MainActivityViewModel::class.java)


binding.rvParentExpenseDates.layoutManager = LinearLayoutManager(requireContext())
expensesViewModel.allExpenses.observe(viewLifecycleOwner){ expensesList ->
    if (expensesList.isNotEmpty()){
       binding.rvParentExpenseDates.adapter = expensesViewModel.uniqueDates.value?.let {
           ParentDatesAdapter(it, expensesList) { expenseId ->
               Toast.makeText(requireContext(), "Clicked expense with id: $expenseId", Toast.LENGTH_LONG).show()
                }
            }
       binding.rvParentExpenseDates.visibility = View.VISIBLE
    } else {
       binding.rvParentExpenseDates.visibility = View.GONE
    }
}

ExpenseEntity.kt

@Entity(tableName = "expense-table")
data class ExpenseEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "date-time")
    val dateTime : Date?,
    val date : Date?,
    @ColumnInfo(name = "account-type")
    val accountType : String = "",
    val category : String = "",
    val amount : Double = 0.0,
    val currency : String = "",
    val note : String = ""

)


Solution

  • Per the documentation:

    These methods permit functional composition and delegation of LiveData instances. The transformations are calculated lazily, and will run only when the returned LiveData is observed. Lifecycle behavior is propagated from the input source LiveData to the returned one.

    The issue here is that you never observe the transformed LiveData (uniqueDates) -- you only inspect the value, so the transformation is never applied.

    One option, if you need both together, is to map into a joined view:

    class MainActivityViewModel(private val expenseDao: ExpenseDao) : ViewModel() {
      val allExpenses : LiveData<List<ExpenseEntity>> =
        expenseDao.fetchAllExpenses().asLiveData()
    
      val allAndUniqueDatedExpenses: LiveData<Pair<List<ExpenseEntity>, List<Date>> = 
        Transformations.map(allExpenses) { expenses ->
          expenses to expenses.mapNotNull { it.date }.distinct()
        }
    }
    

    Then simply observe this joined value:

    expensesViewModel.allAndUniqueDatedExpenses.observe(this) { (expenses, dates) ->
      binding.rvParentExpenseDates.adapter = 
        ParentDatesAdapter(dates, expenses) { expenseId ->
          Toast.makeText(...).show()
        }
    }
    

    However, I would argue here you don't really need another LiveData transformation. Simply do the transformation inline:

    expensesViewModel.allExpenses.observe(this) { expenses ->
      val dates = expenses.mapNotNull { it.date }.distinct()
    
      binding.rvParentExpenseDates.adapter =
        ParentDatesAdapter(dates, expenses) { expenseId ->
          Toast.makeText(...).show()
        }
    }