Разработка
Retrofit + корутины Kotlin: полное руководство для Android-разработчиков
Связка Retrofit и корутин Kotlin преобразила сетевое программирование в Android: теперь это не сложная и «ломкая» задача, а элегантное и поддерживаемое решение. Используя подходы и лучшие практики из этого руководства, вы сможете делать надёжные и масштабируемые сетевые слои, улучшающие и опыт разработки, и производительность приложения.
Когда я только начинал заниматься Android-разработкой, сетевые запросы напоминали настоящий лабиринт: вложенные колбеки, сложности с потоками и куча шаблонного кода. Но теперь ситуация кардинально изменилась — благодаря связке Retrofit и корутин Kotlin взаимодействие с API в Android-приложениях стало совсем другим.
В этом гайде я расскажу всё, что нужно знать об интеграции Retrofit с корутинами Kotlin, и поделюсь практическими советами, которые получил при разработке production-ready Android-приложений.
Почему Retrofit + корутины — это must-have для Android
Прежде чем перейти к коду, давай разберёмся, почему эта комбинация стала стандартом для работы с сетью на Android.
В чём проблема «классического» подхода
Традиционные сетевые запросы на Android были настоящей головной болью:
- Callback hell: вложенные вызовы делали код нечитаемым и неудобным для поддержки
- Управление потоками: нужно было вручную переключать потоки и следить за обновлением UI
- Обработка ошибок: сложное распространение ошибок по цепочке колбеков
- Тестирование: асинхронный код на колбеках сложно тестировать unit-тестами
Как решают проблему корутины
Корутины упрощают жизнь разработчика:
- Последовательный код: асинхронщина пишется как обычный последовательный код
- Структурированная конкуррентность: автоматическое управление жизненным циклом и отменой задач
- Обработка исключений: знакомые блоки try-catch для работы с ошибками
- Тестируемость: удобно тестировать с помощью runTest и TestDispatchers
Настройка проекта
Начинаем с зависимостей. Первым делом, добавь необходимые зависимости в свой build.gradle:
dependencies {
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// ViewModel with coroutines support
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
// Optional: OkHttp for logging
implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
}
Создаём интерфейс для API
Первый шаг — это написать чистый интерфейс для API с использованием suspend-функци:
interface UserApiService {
@GET("users")
suspend fun getUsers(): List<User>
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: Int): User
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): User
@PUT("users/{id}")
suspend fun updateUser(
@Path("id") userId: Int,
@Body user: UpdateUserRequest
): User
@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") userId: Int): Response<Unit>
}
Основные моменты:
- Каждая функция отмечается ключевым словом
suspend - Нет необходимости использовать обёртки
Call<> - Прямые типы возвратов делают API интуитивно понятным
- Для запросов без данных можно возвращать
Response<Unit>
Создаём надёжный слой репозитория
Правильно организованный репозиторий скрывает детали сетевого взаимодействия и предоставляет удобный и чистый интерфейс для ViewModel:
class UserRepository {
private val apiService: UserApiService
init {
val retrofit = Retrofit.Builder()
.baseUrl("<https://jsonplaceholder.typicode.com/>")
.addConverterFactory(GsonConverterFactory.create())
.client(createOkHttpClient())
.build()
apiService = retrofit.create(UserApiService::class.java)
}
private fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
suspend fun fetchUsers(): Result<List<User>> {
return try {
val users = apiService.getUsers()
Result.success(users)
} catch (e: IOException) {
Result.failure(NetworkException("Network error occurred"))
} catch (e: HttpException) {
Result.failure(ServerException("Server error: ${e.code()}"))
} catch (e: Exception) {
Result.failure(UnknownException("Unknown error occurred"))
}
}
suspend fun fetchUser(id: Int): Result<User> {
return try {
val user = apiService.getUser(id)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createUser(request: CreateUserRequest): Result<User> {
return try {
val user = apiService.createUser(request)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// Custom exception classes for better error handling
sealed class ApiException(message: String) : Exception(message)
class NetworkException(message: String) : ApiException(message)
class ServerException(message: String) : ApiException(message)
class UnknownException(message: String) : ApiException(message)
Интеграция с ViewModel
ViewModel — это связующее звено между UI и репозиторием. Вот как правильно использовать их вместе с корутинами:
class UserViewModel(
private val userRepository: UserRepository = UserRepository()
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
fun loadUsers() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
userRepository.fetchUsers()
.onSuccess { userList ->
_users.value = userList
_uiState.value = _uiState.value.copy(
isLoading = false,
error = null
)
}
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
error = exception.message
)
}
}
}
fun createUser(name: String, email: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
val request = CreateUserRequest(name, email)
userRepository.createUser(request)
.onSuccess { newUser ->
// Refresh the list or add the new user
loadUsers()
}
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Failed to create user: ${exception.message}"
)
}
}
}
}
data class UserUiState(
val isLoading: Boolean = false,
val error: String? = null
)
Продвинутые приёмы и лучшие практики
1. Обработка разных типов ответов
Иногда нужно обрабатывать как успешные ответы, так и ошибки HTTP:
suspend fun loginUser(credentials: LoginRequest): Result<LoginResponse> {
return try {
val response = apiService.login(credentials)
if (response.isSuccessful) {
response.body()?.let { loginResponse ->
Result.success(loginResponse)
} ?: Result.failure(Exception("Empty response body"))
} else {
val errorBody = response.errorBody()?.string()
Result.failure(Exception("Login failed: $errorBody"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
2. Реализация логики повторных попыток
Для критически важных операций реализуй механизм повторных запросов:
suspend fun fetchUserWithRetry(userId: Int, maxRetries: Int = 3): Result<User> {
repeat(maxRetries) { attempt ->
try {
val user = apiService.getUser(userId)
return Result.success(user)
} catch (e: IOException) {
if (attempt == maxRetries - 1) {
return Result.failure(e)
}
delay(1000 * (attempt + 1)) // Exponential backoff
}
}
return Result.failure(Exception("Max retries exceeded"))
}
3. Поддержка отмены
Корректно обрабатывай отмену корутин для эффективного управления ресурсами:
class UserViewModel : ViewModel() {
private var fetchJob: Job? = null
fun loadUsers() {
fetchJob?.cancel() // Cancel previous request
fetchJob = viewModelScope.launch {
try {
_uiState.value = _uiState.value.copy(isLoading = true)
val result = userRepository.fetchUsers()
if (isActive) { // Check if coroutine is still active
handleUsersResult(result)
}
} catch (e: CancellationException) {
// Handle cancellation gracefully
throw e
} catch (e: Exception) {
if (isActive) {
handleError(e)
}
}
}
}
}
Тестирование вашей реализации
Тестировать код с корутинами просто, если использовать нужные инструменты:
@ExperimentalCoroutinesApi
class UserRepositoryTest {
private val mockApiService = mockk<UserApiService>()
private val repository = UserRepository(mockApiService)
@Test
fun `fetchUsers returns success when API call succeeds`() = runTest {
// Given
val expectedUsers = listOf(
User(1, "John Doe", "john@example.com"),
User(2, "Jane Smith", "jane@example.com")
)
coEvery { mockApiService.getUsers() } returns expectedUsers
// When
val result = repository.fetchUsers()
// Then
assertTrue(result.isSuccess)
assertEquals(expectedUsers, result.getOrNull())
}
@Test
fun `fetchUsers returns failure when API throws exception`() = runTest {
// Given
coEvery { mockApiService.getUsers() } throws IOException("Network error")
// When
val result = repository.fetchUsers()
// Then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NetworkException)
}
}
Распространённые ошибки и как их избежать
1. Блокировка главного потока
Неправильно:
// This will block the main thread
fun loadUsers() {
runBlocking {
val users = userRepository.fetchUsers()
// Update UI
}
}
Правильно:
// Use appropriate coroutine scope
fun loadUsers() {
viewModelScope.launch {
val users = userRepository.fetchUsers()
// Update UI
}
}
2. Игнорирование отмены корутин
Неправильно:
suspend fun fetchData() {
try {
val data = apiService.getData()
processData(data)
} catch (e: Exception) {
handleError(e) // This catches CancellationException too
}
}
Правильно:
suspend fun fetchData() {
try {
val data = apiService.getData()
processData(data)
} catch (e: CancellationException) {
throw e // Re-throw cancellation
} catch (e: Exception) {
handleError(e)
}
}
Вопросы производительности
1. Пулинг соединений
Настройте OkHttp для максимальной производительности:
private fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
.retryOnConnectionFailure(true)
.build()
}
2. Кеширование ответов
Сделайте кеш для лучшей производительности:
private fun createOkHttpClient(context: Context): OkHttpClient {
val cacheSize = 10 * 1024 * 1024 // 10 MB
val cache = Cache(context.cacheDir, cacheSize.toLong())
return OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control", "public, max-age=60")
.build()
}
.build()
}
Заключение
Связка Retrofit и корутин Kotlin преобразила сетевое программирование в Android: теперь это не сложная и «ломкая» задача, а элегантное и поддерживаемое решение. Используя подходы и лучшие практики из этого руководства, вы сможете делать надёжные и масштабируемые сетевые слои, улучшающие и опыт разработки, и производительность приложения.
Важные выводы:
- Всегда используйте suspend-функции в API-интерфейсах
- Реализуйте грамотную обработку ошибок через обёртки
Result - Используйте
viewModelScopeдля автоматического управления жизненным циклом - Пишите полноценные тесты с помощью
runTest - Корректно обрабатывайте отмену корутин, чтобы избежать утечек ресурсов
Android постоянно развивается, и освоение этих концепций поможет вам оставаться на передовой современных практик разработки мобильных приложений.
-
Аналитика магазинов2 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Интегрированные среды разработки3 недели назад
Chad: The Brainrot IDE — дикая среда разработки с играми и развлечениями
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.45
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.46

