Suspend function should be called only from a coroutine: что это значит

Вы написали suspend fun, вызвали её из обычного кода — и проект не собирается. Студия подчёркивает вызов красным и пишет:

Suspend function 'loadUser' should be called only from a coroutine or another suspend function

В проектах на старой версии компилятора формулировка другая, но смысл тот же:

Suspension functions can be called only within coroutine body

Это не поломка окружения и не отсутствующая зависимость. Компилятор говорит ровно то, что написано: suspend-функцию нельзя вызвать откуда попало.

Почему так

suspend — не пометка «эта функция медленная». Это указание компилятору перестроить функцию так, чтобы её можно было приостановить и продолжить позже. Для этого ей нужен невидимый дополнительный параметр — объект продолжения, который хранит, где именно остановились и что делать дальше.

Обычная функция такой параметр передать не может: у неё его просто нет. Поэтому вызвать suspend-функцию разрешено только оттуда, где этот механизм уже есть, — из другой suspend-функции или из тела корутины.

Четыре способа починить

Первый: сделать вызывающую функцию suspend-функцией. Самый частый и самый правильный ответ. Проблема просто поднимается на уровень выше:

suspend fun showProfile() {
    val user = loadUser()     // теперь можно
    render(user)
}

Рано или поздно подъём упрётся в место, откуда корутину нужно именно запустить. Вот там и пригодятся остальные способы.

Второй: запустить корутину в подходящем скоупе. В Android он у вас почти всегда уже есть:

viewModelScope.launch {   // во ViewModel
    val user = loadUser()
    _state.value = user
}

lifecycleScope.launch {   // в Activity или Fragment
    val user = loadUser()
}

Третий: coroutineScope, если нужно запустить несколько задач и дождаться их. Эта функция сама является suspend-функцией, так что вызывать её можно только из такого же контекста, зато внутри доступны и launch, и async.

Четвёртый: runBlocking — но только в main() и в тестах. Он блокирует поток, и в Android-коде это прямой путь к зависшему интерфейсу. В тестах вместо него лучше runTest.

Отдельный случай: Jetpack Compose

В composable-функции suspend-функцию тоже не вызвать, и причина чуть другая: composable выполняется много раз при каждой рекомпозиции, и запускать в ней загрузку было бы катастрофой. Для этого есть два инструмента:

  • LaunchedEffect(key) — запускает корутину при входе в композицию и перезапускает при смене ключа;
  • rememberCoroutineScope() — даёт скоуп, из которого можно запускать корутины в обработчиках событий, например по нажатию кнопки.
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    LaunchedEffect(Unit) {
        viewModel.load()      // здесь suspend вызывать можно
    }
}

Чего делать не стоит

Два «решения», которые встречаются в ответах на форумах и оба плохие.

GlobalScope.launch { } — компилятор замолчит, но вы получите корутину без родителя, которую нельзя отменить. В Android это утечка: экран закрыт, а работа продолжается.

runBlocking { } внутри обработчика нажатия — интерфейс замрёт ровно на время запроса, и при плохой сети пользователь увидит системное окно «приложение не отвечает».

Короткий ответ

Если вы видите эту ошибку — вопрос не «как заткнуть компилятор», а «откуда в этом месте возьмётся корутина». Во ViewModel ответ viewModelScope, на экране lifecycleScope или LaunchedEffect, в обычной функции — пометить её suspend и поднять вопрос на уровень выше.

Что именно компилятор делает с suspend-функцией и почему приостановка дешевле блокировки — в руководстве по корутинам.