Классический подход к работе с базой данных в Android выглядит так: отправил запрос, получил результат, обновил UI. Понадобились свежие данные — отправил ещё один запрос. Добавил запись — снова запросил весь список. Удалил — запросил. Изменил — запросил. Схема «запрос-ответ» работает, но заставляет разработчика вручную синхронизировать данные с интерфейсом.
Room предлагает другой подход. Вместо разовых запросов — потоки. DAO-метод возвращает Flow, и Room автоматически эмиттит новое значение каждый раз, когда данные в таблице меняются. Интерфейс подписывается на этот поток и обновляется сам, без повторных запросов.
Два способа работы с Room
В Room есть чёткое разделение: suspend-функции для разовых операций и Flow для наблюдения.
@Dao
interface TaskDao {
// Разовые операции: suspend
@Insert
suspend fun insert(task: TaskEntity)
@Delete
suspend fun delete(task: TaskEntity)
@Query("UPDATE tasks SET isDone = :isDone WHERE id = :id")
suspend fun setDone(id: Int, isDone: Boolean)
// Наблюдение: Flow
@Query("SELECT * FROM tasks ORDER BY createdAt DESC")
fun observeAll(): Flow<List<TaskEntity>>
@Query("SELECT COUNT(*) FROM tasks WHERE isDone = 1")
fun observeDoneCount(): Flow<Int>
}
Методы insert(), delete(), setDone() выполняются один раз и завершаются. Их можно вызвать из корутины — например, из viewModelScope.launch {}.
Методы observeAll() и observeDoneCount() не завершаются. Они возвращают холодный Flow, который при каждом collect() начинает наблюдение за таблицей. Изменилась таблица tasks — Room выполняет запрос заново и эмиттит новый результат. Удалили задачу — поток автоматически выдаст обновлённый список. Добавили — тоже.
Обратите внимание: методы, возвращающие Flow, не помечены как suspend. Они не приостанавливают выполнение — они создают поток, на который кто-то потом подпишется.
Entity: простая таблица
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val isDone: Boolean = false,
val createdAt: Long = System.currentTimeMillis()
)
ViewModel: подогреваем поток
Знакомая схема: репозиторий (или DAO напрямую, для простоты) возвращает холодный Flow, ViewModel превращает его в горячий StateFlow.
Заметьте: методы addTask(), toggleDone(), removeTask() ничего не возвращают. Они просто изменяют данные в базе. А за обновление интерфейса отвечают потоки tasks и doneCount — Room сам уведомит их об изменениях.
Это принципиальный сдвиг мышления. В императивном подходе вы бы написали:
// Императивно: вставил → перезагрузил
fun addTask(title: String) {
viewModelScope.launch {
dao.insert(TaskEntity(title = title))
_tasks.value = dao.getAll() // ручное обновление
}
}
В реактивном подходе цепочка короче и надёжнее: вставил → Room автоматически оповестил Flow → UI обновился. Ручная синхронизация не нужна.
Composable: подписка и отображение
@Composable
fun TaskScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
val doneCount by viewModel.doneCount.collectAsStateWithLifecycle()
var newTitle by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize().padding(16.dp)) {
Text("Выполнено: $doneCount из ${tasks.size}", fontSize = 18.sp)
Spacer(Modifier.height(12.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = newTitle,
onValueChange = { newTitle = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Новая задача") },
singleLine = true
)
Spacer(Modifier.width(8.dp))
Button(onClick = {
if (newTitle.isNotBlank()) {
viewModel.addTask(newTitle.trim())
newTitle = ""
}
}) {
Text("Добавить")
}
}
Spacer(Modifier.height(12.dp))
LazyColumn {
items(tasks, key = { it.id }) { task ->
TaskRow(
task = task,
onToggle = { viewModel.toggleDone(task) },
onDelete = { viewModel.removeTask(task) }
)
}
}
}
}
Ни одной строчки кода для перезагрузки данных. Добавил задачу — она появилась. Удалил — исчезла. Переключил статус — счётчик обновился. Всё работает через поток.
Когда Flow, а когда suspend
Правило простое:
Если результат нужен один раз (вставка, удаление, обновление, разовая выборка по id) — используйте suspend.
Если результат нужен постоянно (список на экране, счётчик, статистика) — используйте Flow.
Не стоит оборачивать в Flow всё подряд. Метод getById(id), который вызывается один раз при открытии экрана, прекрасно работает как suspend. Но список задач, который должен обновляться в реальном времени, — это Flow.
Room + Flow — мощная комбинация. База данных становится источником истины: все изменения проходят через неё, а интерфейс реагирует автоматически. Это основа реактивной архитектуры Android-приложения, которая избавляет от ручной синхронизации и снижает количество багов.