Запрос Android Jetpack Compose Room в зависимости от Flow

Я пытаюсь добиться следующего поведения в своем приложении TODO:

  • когда пользователь касается кружка на верхней панели, выбирается дата, и список задач должен измениться, чтобы отображать задачи на эту дату
  • когда пользователь касается флажка или проводит пальцем по экрану, чтобы удалить задачу, запускается событие для сохранения в базе данных, и пользовательский интерфейс должен отражать эти изменения.

см. рисунок ниже для пояснения:

Я попробовал использовать Flow, который сопоставляет последнюю выбранную дату с запросом к базе данных задач на эту дату внутри 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
            }
        }
    }
}

TaskEvent.DeleteTask, TaskEvent.SetDone и TaskEvent.SetSelectedDate, которые вы видите в TaskViewModel, запускаются пользователем в пользовательском интерфейсе.

Это моя основная активность:

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

ЧитатьTasksState.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()
)

Это похоже на то, что запрос taskDao.getTasksForDate(latestSelectedDate), который возвращает Flow<List<Task>>, сам зависит от Flow, поскольку дата, которую он получает на входе, сохраняется с помощью StateFlow<Date>.

Это вроде как работает, но я не думаю, что это лучший способ сделать это (или даже правильный способ). Можете ли вы дать мне совет или предложить лучший подход?

0
0
147
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основание потока базы данных на основе другого потока не только нормально, но и является предпочтительным способом сделать это.

У вас есть много лишних свойств в вашей модели представления, но если вы удалите _selectedDate, selectedDate, _readTasksState и _tasks, все может быть так просто:

private val selectedDate = MutableStateFlow(Date())

val readTasksState: StateFlow<ReadTasksState> = selectedDate
    .flatMapLatest { latestSelectedDate ->
        taskDao.getTasksForDate(latestSelectedDate)
    }
    .mapLatest { tasks ->
        ReadTasksState(
            tasksForSelectedDate = tasksToDtos(tasks),
        )
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ReadTasksState())

readTasksState теперь представляет собой поток, который строится следующим образом:

  1. Все начинается с потока selectedDate (ранее называвшегося _selectedDate).
  2. Затем этот поток переключается на то, что taskDao.getTasksForDate возвращается. Это еще один поток, который генерирует новое значение при каждом изменении задачи в базе данных на определенную дату. Вот как работает flatMapLatest: он переключается с одного потока на другой в зависимости от содержимого первого потока.
  3. Теперь содержимое потока базы данных преобразуется из List<Task> в ReadTasksState с помощью mapLatest. mapLatest меняет только контент, в отличие от flatMapLatest, который переключает весь поток на новый поток.
  4. Наконец, результирующий поток передается в StateFlow.

Теперь, когда в пользовательском интерфейсе активируется одна из кнопок даты, selectedDate обновляется (1.), что запускает flatMapLatest. Это принимает измененную дату в качестве входных данных и возвращает другой поток, результат DAO (2.). Затем этот поток преобразуется (3.) и превращается в StateFlow (4.).

С другой стороны, когда в пользовательском интерфейсе установлен только флажок задачи, первый поток (1.) не затрагивается, дата остается той же. Кроме того, flatMapLatest (2.) не выполняется повторно, он не переключает потоки снова, он остается в том же потоке, который taskDao.getTasksForDate ранее возвращался для данной даты (что нормально, поскольку эта дата не изменилась). Однако меняется то, что установленный флажок меняет что-то в базе данных. И это запускает поток, в котором мы сейчас находимся, для выдачи нового значения. Это запускает (3.), когда новый список задач принимается и преобразуется в новый объект ReadTasksState. Наконец, следует (4.).

По сути, вы просто хотите readTasksState стать трансформированным (mapLatest) taskDao.getTasksForDate. Поскольку для этого нужен параметр даты, вам придется использовать flatMapLatest в потоке с этой датой.

Кстати, сбор потоков в Compose должен выполняться с помощью collectAsStateWithLifecycle из зависимости gradle androidx.lifecycle:lifecycle-runtime-compose, чтобы Compose мог автоматически отписываться от потока, когда действие приостанавливается и т. д.

Спасибо за ваш ответ и предложение по CollectAsState, я изменю его, как вы мне сказали. Я рад, что это работает, но не могли бы вы объяснить, почему? Если первым потоком, который должен быть «запущен», является _selectedDate, почему он срабатывает, когда задача также изменяется (например, когда я касаюсь флажка)? Разве он не должен срабатывать только тогда, когда я нажимаю дни на верхней панели? Я действительно не могу понять, как поток selectedDate запускается, когда я редактирую задачу.

pochopsp 13.04.2024 08:03

Я обновил свой ответ, чтобы объяснить, как разные потоки взаимодействуют друг с другом. Я также исправил ошибку в своем коде и внес некоторые небольшие изменения (пожалуйста, сравните их, просмотрев изменения).

Leviathan 13.04.2024 13:41

Ваше редактирование принесло большую пользу, теперь все стало яснее. Спасибо

pochopsp 13.04.2024 14:09

Другие вопросы по теме