Что такое rememberCoroutineScope и как он используется?
rememberCoroutineScope() возвращает CoroutineScope, привязанный к жизненному циклу composable: он автоматически отменяется при уходе composable из дерева. Используется для ручного запуска корутин из обработчиков событий (onClick, жесты).
rememberCoroutineScope в Jetpack Compose
rememberCoroutineScope() возвращает CoroutineScope, привязанный к точке вызова в композиции. Область автоматически отменяется, когда composable покидает экран (уходит из композиции). Это позволяет запускать корутины из обработчиков событий (кликов, жестов) — мест, где нельзя использовать suspend-функции напрямую.
Отличие от LaunchedEffect
LaunchedEffect— запускается автоматически при входе в композицию и при смене ключей; нельзя вызвать из onClick.rememberCoroutineScope— даёт ручной контроль: запускай корутину тогда, когда пользователь сделал действие.
Базовый пример
@Composable
fun SaveButton(viewModel: FormViewModel) {
val scope = rememberCoroutineScope()
var isSaving by remember { mutableStateOf(false) }
Button(
onClick = {
scope.launch { // запуск из обработчика события
isSaving = true
viewModel.save()
isSaving = false
}
},
enabled = !isSaving
) {
if (isSaving) CircularProgressIndicator(Modifier.size(18.dp))
else Text("Сохранить")
}
}
Показ Snackbar
Типичный паттерн — вызов SnackbarHostState.showSnackbar() из кнопки:
@Composable
fun DeleteItemScreen() {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Button(
modifier = Modifier.padding(padding),
onClick = {
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "Элемент удалён",
actionLabel = "Отмена"
)
if (result == SnackbarResult.ActionPerformed) {
/* отменить удаление */
}
}
}
) {
Text("Удалить")
}
}
}
Жизненный цикл scope
Scope отменяется при уходе composable из дерева. Если тот же composable перерисовывается (рекомпозиция), scope не пересоздаётся — remember сохраняет его между рекомпозициями. Все запущенные корутины продолжают работать до завершения или отмены scope.
Dispatcher по умолчанию
По умолчанию используется Dispatchers.Main.immediate. Для фоновой работы используйте withContext(Dispatchers.IO) внутри корутины, а не передавайте другой dispatcher в rememberCoroutineScope.
scope.launch {
withContext(Dispatchers.IO) {
heavyRepository.load() // фоновый поток
}
// результат применяется на Main
state = result
}
Подводные камни
- Использование
GlobalScope.launchвместоrememberCoroutineScope— корутина продолжает жить после уничтожения экрана. - Множественные клики запускают несколько параллельных корутин — добавляйте debounce или флаг
isLoading. - Изменение State из корутины не в Main-потоке без
withContext(Dispatchers.Main)бросает исключение в старых версиях Compose. - Попытка вызвать suspend-функцию напрямую в
onClick— это не suspend-контекст, код не скомпилируется. - Хранение scope в ViewModel — он привязан к composable, не к ViewModel; используйте
viewModelScopeдля работы в ViewModel. - Передача scope в дочерний composable нарушает инкапсуляцию — лучше передавать лямбды
() -> Unit. - Вызов
rememberCoroutineScopeвне composable-функции вызовет ошибку времени выполнения.
Common mistakes
- Объяснять «rememberCoroutineScope» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Запуск долгой domain-работы из этого scope может отмениться при уходе composable и потерять бизнес-операцию.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «rememberCoroutineScope» своими словами и связывает ее с кодом.
- Называет механизм: Он подходит для реакций на события пользователя, например scroll или snackbar, когда LaunchedEffect не выражает событие.
- Видит production-последствие: Запуск долгой domain-работы из этого scope может отмениться при уходе composable и потерять бизнес-операцию.