Как последовательно выполнить два запроса с сохранением токена между ними и получить оба ответа одновременно
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Отличный вопрос, который затрагивает ключевые аспекты работы с сетью в 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).