Jetpack ComposeMiddleTechnical

Что такое 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 и потерять бизнес-операцию.

Sources

Related topics