You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Jetpack Compose中基于Flow的Room查询优化方案咨询

TODO应用交互逻辑优化咨询

需求场景

  • 用户点击顶部栏圆形控件选择日期后,任务列表切换展示该日期的任务;
  • 用户点击复选框或滑动删除任务时,触发数据库操作,UI同步更新状态。

当前实现方案

我在TaskViewModel中采用以下逻辑:

  • 用MutableStateFlow存储选中日期;
  • 通过flatMapLatest操作符将选中日期映射为Room的taskDao.getTasksForDate(latestSelectedDate)查询(返回Flow<List<Task>>);
  • 结合combine生成供UI订阅的readTasksState。

该方案可正常运行,但我认为并非最优,希望得到更合理的优化建议或替代方案。

核心代码

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

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 = {
            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) } }
                }
            }
        }
    }
}

ReadTasksState.kt

package com.pochopsp.dailytasks.domain

import java.util.Date

data class ReadTasksState(
    val tasksForSelectedDate: List<TaskCardDto> = emptyList()
)

优化建议

1. 简化状态组合逻辑

当前_readTasksState是冗余的空状态,仅用于和_tasks组合,完全可以直接将_tasks映射转换为ReadTasksState,减少不必要的Flow层级:

@OptIn(ExperimentalCoroutinesApi::class)
val readTasksState = selectedDate.flatMapLatest { date ->
    taskDao.getTasksForDate(date)
}.map { tasks ->
    ReadTasksState(tasksForSelectedDate = tasksToDtos(tasks))
}.stateIn(
    viewModelScope,
    SharingStarted.WhileSubscribed(5000), // 延长订阅保持时间,避免快速切换时频繁重启Flow
    ReadTasksState()
)

这样可以去掉_readTasksState,代码更简洁,逻辑更直接。

2. 优化日期处理

  • 建议用LocalDate替代Date,它是更适合处理纯日期的类,避免时区问题,Room对LocalDate有良好支持(需添加kotlinx-datetime依赖或Room日期转换器);
  • 对选中日期添加防抖处理,避免用户快速切换日期触发过多数据库查询:
private val _selectedDate = MutableStateFlow(LocalDate.now())
private val selectedDate = _selectedDate.asStateFlow()
    .debounce(300) // 300ms防抖

3. 扩展状态封装

后续若需添加加载、错误状态,可扩展ReadTasksState并在Flow中统一处理:

// 扩展状态类
data class ReadTasksState(
    val tasksForSelectedDate: List<TaskCardDto> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

// ViewModel中处理状态流
@OptIn(ExperimentalCoroutinesApi::class)
val readTasksState = selectedDate.flatMapLatest { date ->
    flow {
        emit(ReadTasksState(isLoading = true))
        try {
            val tasks = taskDao.getTasksForDate(date).first()
            emit(ReadTasksState(tasksForSelectedDate = tasksToDtos(tasks)))
        } catch (e: Exception) {
            emit(ReadTasksState(error = e.localizedMessage))
        }
    }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ReadTasksState(isLoading = true))

UI可根据状态显示加载动画或错误提示,提升体验。

4. 数据库查询优化

  • 给Task表的日期字段添加索引,加速getTasksForDate查询:
@Entity(tableName = "tasks", indices = [Index(value = ["date"])])
data class Task(
    // ... 其他字段
    val date: LocalDate
)

5. 简化ViewModel创建

用扩展函数简化ViewModelProvider.Factory的写法:

// 定义通用扩展函数
inline fun <reified T : ViewModel> viewModelFactory(crossinline factory: () -> T) =
    object : ViewModelProvider.Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return factory() as T
        }
    }

// MainActivity中使用
private val viewModel by viewModels<TaskViewModel> {
    viewModelFactory { TaskViewModel(db.taskDao, db.dayDao) }
}

内容的提问来源于stack exchange,提问作者pochopsp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 05:05:59