I'm trying to achieve the following behavior on my TODO app:
- when user touches a circle in the top bar a date is selected and the tasks list must change to show tasks of that date
- when user touches a checkbox or swipes to delete a task, an event is triggered to save on DB and the UI must reflect these changes
see below pic for clarification:
I've tried using a Flow which maps latest selected date to the database query of tasks for that date, inside the TaskViewModel:
package com.pochopsp.dailytasks.domain
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pochopsp.dailytasks.data.database.entity.Task
import com.pochopsp.dailytasks.data.database.dao.TaskDao
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.Date
class TaskViewModel(
private val taskDao: TaskDao
): ViewModel() {
private val _selectedDate = MutableStateFlow(Date())
private var selectedDate: StateFlow<Date> = _selectedDate.asStateFlow()
private val _readTasksState = MutableStateFlow(ReadTasksState())
@OptIn(ExperimentalCoroutinesApi::class)
private val _tasks = selectedDate.flatMapLatest {
latestSelectedDate -> taskDao.getTasksForDate(latestSelectedDate)
}
val readTasksState = combine(_readTasksState, _tasks){ readtaskstate, tasks ->
readtaskstate.copy(
tasksForSelectedDate = tasksToDtos(tasks)
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ReadTasksState())
private fun tasksToDtos (tasks: List<Task>): List<TaskCardDto> {
return tasks.map { t -> TaskCardDto(t.id, t.title, t.icon, t.done) }.toList()
}
fun onEvent(event: TaskEvent){
when(event){
is TaskEvent.DeleteTask -> {
viewModelScope.launch {
taskDao.deleteById(event.id)
}
}
is TaskEvent.SetDone -> {
viewModelScope.launch {
taskDao.updateDoneById(event.done, event.id)
}
}
is TaskEvent.SetSelectedDate -> {
_selectedDate.value = event.selectedDate
}
}
}
}
The TaskEvent.DeleteTask, TaskEvent.SetDone and TaskEvent.SetSelectedDate which you see in the TaskViewModel are all triggered by user input on the UI.
This is my MainActivity:
package com.pochopsp.dailytasks
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.room.Room
import com.pochopsp.dailytasks.data.database.Database
import com.pochopsp.dailytasks.domain.TaskCardDto
import com.pochopsp.dailytasks.domain.ReadTasksState
import com.pochopsp.dailytasks.domain.TaskViewModel
import com.pochopsp.dailytasks.presentation.navigation.Destinations
import com.pochopsp.dailytasks.presentation.screen.MainScreen
import com.pochopsp.dailytasks.presentation.theme.DailyTasksTheme
import kotlinx.coroutines.flow.MutableStateFlow
class MainActivity : ComponentActivity() {
private val db by lazy {
Room.databaseBuilder(
applicationContext,
Database::class.java,
"tasks.db"
).fallbackToDestructiveMigration().build()
}
private val viewModel by viewModels<TaskViewModel>(
factoryProducer = {
// needed because our viewmodel has a parameter (in this case the dao interface)
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TaskViewModel(db.taskDao, db.dayDao) as T
}
}
}
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DailyTasksTheme(darkTheme = false) {
val readTasksState by viewModel.readTasksState.collectAsState()
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "main")
{
composable(Destinations.Main.route) { MainScreen(state = readTasksState, onEvent = viewModel::onEvent){ navController.navigate(it.route) } }
// Add more destinations similarly.
}
}
}
}
}
ReadTasksState.kt:
package com.pochopsp.dailytasks.domain
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import java.util.Date
data class ReadTasksState(
val tasksForSelectedDate: List<TaskCardDto> = emptyList()
)
It's like the query taskDao.getTasksForDate(latestSelectedDate), which returns a Flow<List<Task>>, depends itself on a Flow, because the date which it receives in input is stored using a StateFlow<Date>.
It kind of works, but I don't think this is the best way to do this (or even a correct way to do this). Can you give me some piece of advice or suggest me a better approach?
