Site icon AppTractor

Retrofit + корутины Kotlin: полное руководство для Android-разработчиков

Когда я только начинал заниматься Android-разработкой, сетевые запросы напоминали настоящий лабиринт: вложенные колбеки, сложности с потоками и куча шаблонного кода. Но теперь ситуация кардинально изменилась — благодаря связке Retrofit и корутин Kotlin взаимодействие с API в Android-приложениях стало совсем другим.

В этом гайде я расскажу всё, что нужно знать об интеграции Retrofit с корутинами Kotlin, и поделюсь практическими советами, которые получил при разработке production-ready Android-приложений.

Почему Retrofit + корутины — это must-have для Android

Прежде чем перейти к коду, давай разберёмся, почему эта комбинация стала стандартом для работы с сетью на Android.

В чём проблема «классического» подхода

Традиционные сетевые запросы на Android были настоящей головной болью:

Как решают проблему корутины

Корутины упрощают жизнь разработчика:

Настройка проекта

Начинаем с зависимостей. Первым делом, добавь необходимые зависимости в свой 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>
}

Основные моменты:

Создаём надёжный слой репозитория

Правильно организованный репозиторий скрывает детали сетевого взаимодействия и предоставляет удобный и чистый интерфейс для 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: теперь это не сложная и «ломкая» задача, а элегантное и поддерживаемое решение. Используя подходы и лучшие практики из этого руководства, вы сможете делать надёжные и масштабируемые сетевые слои, улучшающие и опыт разработки, и производительность приложения.

Важные выводы:

Android постоянно развивается, и освоение этих концепций поможет вам оставаться на передовой современных практик разработки мобильных приложений.

Источник

Exit mobile version