← Назад к вопросам

Как последовательно выполнить два запроса с сохранением токена между ними и получить оба ответа одновременно

2.4 Senior🔥 151 комментариев
#Многопоточность и асинхронность#Сетевое взаимодействие

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос, который затрагивает ключевые аспекты работы с сетью в Android: последовательные (зависимые) запросы, управление токенами аутентификации и композицию ответов. Решение будет основываться на современных подходах с использованием Kotlin Coroutines и популярных библиотек, таких как Retrofit с OkHttp.

Основная идея: выполнить первый запрос (например, аутентификацию), извлечь токен из ответа, автоматически добавить его в заголовки второго запроса, а затем объединить результаты обоих запросов.

1. Архитектура решения и зависимости

Создадим:

  • ApiService: Интерфейс Retrofit для описания запросов.
  • AuthInterceptor: Перехватчик OkHttp для динамической вставки токена.
  • Repository: Слой, который будет управлять последовательностью.

Предположим, что первый запрос (login) возвращает объект AuthResponse, содержащий токен, а второй запрос (getUserData) требует этот токен в заголовке Authorization.

build.gradle.kts (Module:app):

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

2. Настройка Retrofit с AuthInterceptor

AuthInterceptor — сердце механизма. Он будет перехватывать каждый исходящий запрос и добавлять в него актуальный токен, если тот доступен.

import okhttp3.Interceptor
import okhttp3.Response

class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {

    interface TokenManager {
        var token: String?
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()

        // Создаем новый запрос, добавляя заголовок с токеном, если он есть.
        val newRequest = originalRequest.newBuilder()
            .header("Authorization", tokenManager.token?.let { "Bearer $it" } ?: "")
            .build()

        return chain.proceed(newRequest)
    }
}

Настройка Retrofit клиента:

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object RetrofitClient {

    // Простая in-memory реализация TokenManager. В реальном приложении
    // используйте SharedPreferences, DataStore или SecureStorage.
    private val tokenManager = object : AuthInterceptor.TokenManager {
        override var token: String? = null
    }

    private val authInterceptor = AuthInterceptor(tokenManager)

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(authInterceptor)
        .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val apiService: ApiService by lazy { retrofit.create(ApiService::class.java) }

    // Функция для обновления токена в менеджере (будет вызвана после логина).
    fun updateToken(newToken: String) {
        tokenManager.token = newToken
    }
}

3. Определение API и моделей данных

import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

interface ApiService {
    // 1-й запрос: Аутентификация
    @POST("auth/login")
    suspend fun login(@Body credentials: LoginRequest): Response<AuthResponse>

    // 2-й запрос: Получение данных, требующих токен
    @GET("user/profile")
    suspend fun getUserProfile(): Response<UserProfile>
}

// Модели данных
data class LoginRequest(val email: String, val password: String)
data class AuthResponse(val accessToken: String, val refreshToken: String)
data class UserProfile(val id: String, val name: String, val email: String)

4. Репозиторий: Последовательное выполнение и комбинация ответов

Здесь мы используем suspend-функции и корутины. Ключевой момент — сначала выполняем логин, сохраняем токен в RetrofitClient, а затем делаем зависимый запрос. Для возврата обоих результатов одновременно используем data class-обертку.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException

class UserRepository {

    // Общий результат, содержащий ответы обоих запросов.
    data class CombinedUserData(
        val authResponse: AuthResponse,
        val userProfile: UserProfile
    )

    suspend fun loginAndFetchProfile(
        email: String,
        password: String
    ): Result<CombinedUserData> = withContext(Dispatchers.IO) {
        try {
            // 1. Выполняем первый запрос
            val loginResponse = RetrofitClient.apiService.login(LoginRequest(email, password))
            if (!loginResponse.isSuccessful) {
                throw IOException("Login failed: ${loginResponse.code()}")
            }
            val authData = loginResponse.body()!!

            // 2. КРИТИЧЕСКИ ВАЖНЫЙ ШАГ: Сохраняем полученный токен.
            // Теперь AuthInterceptor автоматически добавит его в следующий запрос.
            RetrofitClient.updateToken(authData.accessToken)

            // 3. Выполняем второй запрос (токен уже в заголовке).
            val profileResponse = RetrofitClient.apiService.getUserProfile()
            if (!profileResponse.isSuccessful) {
                throw IOException("Failed to fetch profile: ${profileResponse.code()}")
            }
            val profileData = profileResponse.body()!!

            // 4. Возвращаем комбинированный результат.
            Result.success(CombinedUserData(authData, profileData))

        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

5. Использование во ViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class UserViewModel(private val repository: UserRepository) : ViewModel() {

    private val _uiState = MutableStateFlow<Result<UserRepository.CombinedUserData>?>(null)
    val uiState: StateFlow<Result<UserRepository.CombinedUserData>?> = _uiState

    fun performSequentialRequests(email: String, password: String) {
        viewModelScope.launch {
            _uiState.value = Result.loading() // Состояние загрузки
            val result = repository.loginAndFetchProfile(email, password)
            _uiState.value = result
        }
    }
}

Итог и ключевые моменты

  • Последовательность: Использование suspend функций гарантирует, что второй запрос (getUserProfile) начнется только после успешного получения и обработки токена из первого (login).
  • Сохранение токена: TokenManager в связке с AuthInterceptor обеспечивает прозрачную и автоматическую инъекцию токена во все последующие запросы. Это чистое и масштабируемое решение.
  • Возврат обоих ответов: Создание общего data class CombinedUserData позволяет легко передать оба набора данных во ViewModel и UI.
  • Обработка ошибок: Использование Result-обертки и корректная проверка isSuccessful у Response обеспечивают надежную обработку сетевых ошибок и ошибок сервера (коды 4xx, 5xx).
  • Безопасность: В продакшн-коде никогда не храните токен в памяти как в упрощенном примере. Используйте EncryptedSharedPreferences, Security DataStore или AccountManager для безопасного хранения.

Этот паттерн является отраслевым стандартом для работы с аутентифицированными запросами в Android и легко адаптируется под более сложные сценарии (например, с автоматическим обновлением истекшего токена с помощью Refresh Token).