Статья о типичных ошибках, которые допускают программисты при работе с корутинами. Также, в конце статьи я приведу советы Google, как использовать корутины.
ПО ТЕМЕ:
- Как работают корутины в Kotlin
- Современный подход к разработке с использованием Kotlin
- Как сделать код на Kotlin более понятным
Ошибки при использовании корутин и Flow
Использование Flow вместо обычной suspend-функции.
Многие программисты используют Flow как универсальный инструмент для передачи значений даже в тех случаях, когда в нем нет никакого смысла.
Представим себе такую функцию:
1 |
fun getAccountInfo(): Flow<Response> |
По факту она возвращает только одно значение и Flow здесь не нужен. Логичнее было бы превратить эту функцию в обычную suspend-функцию:
1 |
suspend fun getAccountInfo(): Response |
Suspend-функции с параметрами‑колбэками.
Suspend-функции были задуманы, чтобы заменить колбэки, поэтому создавать функции с колбэками в качестве параметров — большая ошибка.
Возьмем, к примеру, следующую функцию:
1 2 3 4 5 |
suspend fun download( url: String, onSuccess: (String) -> Unit, onError: (Throwable) -> Unit ) |
Как в данном случае избавиться от колбэков, но оставить возможность возвратить два разных типа значений? Для этого можно использовать sealed-классы:
1 2 3 4 5 |
suspend fun download(url:String): Result sealed class Result { data class Success(val data:String): Result() data class Error(val error:Throwable): Result() } |
Использование GlobalScope.
Активности, фрагменты, ViewModel, View и другие стандартные классы имеют функции‑расширения, которые можно использовать для запуска корутин. Не стоит использовать GlobalScope, который может привести к утечкам корутин.
Ненужное переключение потоков.
Корутины устроены так, что их очень легко и просто можно переключить на другой поток с помощью смены диспетчера:
1 2 3 |
with(Dispatchers.IO){ ... } |
Однако стоит несколько раз подумать перед тем, как использовать эту возможность. Во‑первых, это усложняет юнит‑тестирование. Во‑вторых, многие фреймворки и библиотеки умеют самостоятельно переключать исполнение между потоками, так что ручное переключение, кроме оверхеда, ничего не даст.
Советы по использованию корутин
Советы Google, как использовать корутины.
Внедряй диспетчеры как зависимости. Благодаря этому юнит‑тестирование станет намного более удобным.
1 2 3 4 5 |
class NewsRepository( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ } } |
Suspend-функции должны быть безопасными для вызова из UI-потока приложения. Если suspend-функция делает сложную ресурсоемкую работу, она должна сама позаботиться о переключении диспетчера. Другими словами, за перемещение работы в фоновый поток должна отвечать сама suspend-функция, а не код, который ее вызывает.
ViewModel должна создавать корутины сама. Вместо того чтобы выставлять наружу suspend-функции, ViewModel должна сама порождать корутины из обычных функций. Такой подход упрощает тестирование и не создает проблем при пересоздании активности.
1 2 3 4 5 6 7 8 9 10 11 12 |
class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(LatestNewsUiState.Loading) val uiState: StateFlow = _uiState fun loadNews() { viewModelScope.launch { val latestNewsWithAuthors = getLatestNewsWithAuthors() _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors) } } } |
Не следует выставлять наружу изменяемые типы данных. Это стандартный пример грамотного ООП‑проектирования, который позволит сделать сопровождение кода более простым.
1 2 3 4 5 |
class LatestNewsViewModel : ViewModel() { val _uiState = MutableStateFlow(LatestNewsUiState.Loading) val uiState: StateFlow = _uiState /* ... */ } |
Уровни данных и бизнес‑логики должны быть доступны через suspend-функции и Flow. Следование этому принципу позволит правильно управлять жизненным циклом приложения, когда жизнью корутин управляет ViewModel, а не классы уровня бизнес‑логики.
1 2 3 4 |
class ExampleRepository { suspend fun makeNetworkRequest() { /* ... */ } fun getExamples(): Flow { /* ... */ } } |
Используй TestCoroutineDispatcher. Следование первому правилу позволит использовать в тестах диспетчер TestCoroutineDispatcher, который выполняет работу сразу, позволяя лучше контролировать исполнение кода.
Избегай использования GlobalScope. GlobalScope приводит к утечкам корутин, усложняет тестирование и отладку кода.
Корутины должны легко убиваться. Корутины построены на идее кооперативной многозадачности. Это значит, что завершение корутины через cancel() не происходит сразу. Корутина должна сама проверить свой статус и завершить работу в случае необходимости. В большинстве случаев делать для этого ничего не нужно, так как все suspend-функции из пакета kotlinx.coroutines (withContext, delay) умеют сами проверять свой статус и реагировать на сигнал завершения. Но иногда все‑таки приходится делать эту работу самому:
1 2 3 4 5 6 |
someScope.launch { for(file in files) { ensureActive() // Проверка флага завершения readFile(file) } } |
Не забывай об исключениях. Исключения лучше перехватывать в теле корутины:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { try { loginRepository.login(username, token) // Notify view user logged in successfully } catch (error: Throwable) { // Notify view login attempt failed } } } } |
Источник: Best practices for coroutines in Android