Вы написали 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-функцией и почему приостановка дешевле блокировки — в руководстве по корутинам.