Connect with us

Разработка

Как я сократил время загрузки Android-приложения на 70% с помощью параллельных сетевых вызовов

После реализации параллельных сетевых вызовов с использованием корутин Kotlin тот же экран теперь загружается всего за 1.3 секунды. Вот как я это сделал, и как можете сделать вы.

Опубликовано

/

     
     

В прошлом месяце я отлаживал дашборд пользователя в своём Android-приложении, который загружался мучительно долго — 4.5 секунды. Пользователи просто закрывали экран, и я знал, что нужно что-то менять. Виновники? Последовательные сетевые вызовы, которые без необходимости блокировали друг друга.

После реализации параллельных сетевых вызовов с использованием корутин Kotlin тот же экран теперь загружается всего за 1.3 секунды. Вот как я это сделал, и как можете сделать вы.

Проблема: последовательные сетевые вызовы убивают производительность

Когда я впервые создавал дашборд пользователя, я совершил классическую ошибку. Я извлекал пользовательские данные следующим образом:

// The slow way - everything happens one after another
suspend fun loadUserDashboard(userId: String): UserDashboard {
    val profile = apiService.getUserProfile(userId)     // 1.5 seconds
    val posts = apiService.getUserPosts(userId)         // 1.2 seconds
    val followers = apiService.getUserFollowers(userId) // 1.8 seconds
    return UserDashboard(profile, posts, followers)
    // Total time: ~4.5 seconds
}

Каждый вызов API ждал завершения предыдущего, хотя они были совершенно независимы. Это как стоять в трёх разных очередях в продуктовом магазине, одна за другой, вместо того, чтобы три человека покупали одновременно.

Решение: параллельное выполнение с помощью async/await

Решающим фактором стало понимание того, что эти сетевые вызовы не зависят друг от друга. Я мог запустить все три запроса одновременно и ждать их завершения:

// The fast way - everything happens simultaneously
suspend fun loadUserDashboardParallel(userId: String): UserDashboard {
    // Start all requests immediately (don't wait)
    val profileDeferred = async { apiService.getUserProfile(userId) }
    val postsDeferred = async { apiService.getUserPosts(userId) }
    val followersDeferred = async { apiService.getUserFollowers(userId) }
    // Now wait for all results
    val profile = profileDeferred.await()
    val posts = postsDeferred.await()
    val followers = followersDeferred.await()
    return UserDashboard(profile, posts, followers)
    // Total time: ~1.8 seconds (longest individual call)
}

Что здесь происходит:

  1. async { } немедленно запускает корутину, но не блокирует её
  2. Все три async блока начинают выполняться одновременно
  3. await() извлекает результат, когда он нам действительно нужен

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

Представьте, что вы заказываете еду из трёх разных ресторанов одновременно через приложения доставки, вместо того, чтобы ждать доставки каждого блюда, прежде чем заказать следующее.

Реализация в вашем приложении

Вот как я интегрировал это в свой реальный рабочий код.

Уровень репозитория:

class UserRepository @Inject constructor(
    private val apiService: UserApiService
) {
    suspend fun loadCompleteUserData(userId: String): Result<UserDashboard> {
        return try {
            withContext(Dispatchers.IO) {
                // Start all network calls in parallel
                val profileDeferred = async { apiService.getUserProfile(userId) }
                val postsDeferred = async { apiService.getUserPosts(userId) }
                val followersDeferred = async { apiService.getUserFollowers(userId) }
                val notificationsDeferred = async { apiService.getNotifications(userId) }
                // Collect all results
                val dashboard = UserDashboard(
                    profile = profileDeferred.await(),
                    posts = postsDeferred.await(),
                    followers = followersDeferred.await(),
                    notifications = notificationsDeferred.await()
                )
                Result.success(dashboard)
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Интеграция ViewModel:

class UserDashboardViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    fun loadUserDashboard(userId: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            userRepository.loadCompleteUserData(userId)
                .onSuccess { dashboard ->
                    _uiState.value = UiState.Success(dashboard)
                }
                .onFailure { error ->
                    _uiState.value = UiState.Error(error.message)
                }
        }
    }
}

Обработка ошибок: что происходит при сбое одного вызова?

Меня беспокоил вопрос: «Что, если один из параллельных вызовов даст сбой?».

Вот как я с этим справляюсь.

Вариант 1: Быстрая ошибка (всё или ничего)

suspend fun loadUserDataFailFast(userId: String): UserDashboard {
    val profileDeferred = async { apiService.getUserProfile(userId) }
    val postsDeferred = async { apiService.getUserPosts(userId) }
    // If either fails, the whole operation fails
    return UserDashboard(
        profile = profileDeferred.await(), // Throws exception if failed
        posts = postsDeferred.await()
    )
}

Вариант 2: Постепенная деградация (частичный успех)

suspend fun loadUserDataGraceful(userId: String): UserDashboard {
    val profileDeferred = async {
        try { apiService.getUserProfile(userId) }
        catch (e: Exception) { null }
    }
    val postsDeferred = async {
        try { apiService.getUserPosts(userId) }
        catch (e: Exception) { emptyList() }
    }
    return UserDashboard(
        profile = profileDeferred.await(), // null if failed
        posts = postsDeferred.await()      // empty list if failed
    )
}

Я выбрал подход постепенного снижения производительности, поскольку пользователи лучше увидят частичные данные, чем полный сбой.

Влияние на производительность: цифры

Вот фактическое улучшение производительности, которое я зафиксировал:

  • До: среднее время загрузки 4.2 секунды
  • После: среднее время загрузки 1.4 секунды
  • Улучшение: загрузка на 67% быстрее

Что ещё важнее, улучшились показатели вовлечённости пользователей:

  • Сокращение количества отказов от экрана на 23%
  • Увеличение времени, проведённого на панели управления, на 18%
  • Значительно улучшились отзывы пользователей, в которых упоминается «быстрая» работа приложения

Распространённые ошибки, которых следует избегать

1. Не используйте async для последовательных зависимостей:

// Wrong - second call depends on first
val userDeferred = async { getUser(userId) }
val user = userDeferred.await()
val postsDeferred = async { getUserPosts(user.id) } // Depends on user

2. Не забудьте использовать правильный скоуп:

// Wrong - might leak coroutines
GlobalScope.launch { /* network calls */ }
// Right - tied to component lifecycle
viewModelScope.launch { /* network calls */ }

3. Не забывайте об обработке исключений:

Всегда заключайте параллельные вызовы в блоки try-catch или используйте типы Result для корректной обработки сбоев.

Когда НЕ следует использовать параллельные вызовы

Параллельное выполнение не всегда является решением:

  • Ограничения скорости API: некоторые серверы ограничивают количество одновременных запросов
  • Последовательные зависимости: когда вызову B требуются данные от вызова A
  • Ограничения памяти: слишком много одновременных вызовов могут перегрузить устройство
  • Соображения относительно батареи: параллельные вызовы изначально потребляют больше ресурсов

Дальнейшие действия

После освоения базовых параллельных вызовов рассмотрите следующие продвинутые шаблоны.

Сочетание с Flow для обновлений в реальном времени:

fun observeUserDashboard(userId: String): Flow<UserDashboard> = flow {
    while (currentCoroutineContext().isActive) {
        val dashboard = loadUserDataParallel(userId)
        emit(dashboard)
        delay(30_000) // Refresh every 30 seconds
    }
}

Использование awaitAll() для коллекций:

suspend fun loadMultipleUsers(userIds: List<String>): List<UserProfile> {
    return userIds.map { userId ->
        async { apiService.getUserProfile(userId) }
    }.awaitAll()
}

Итог

Параллельные сетевые вызовы преобразили пользовательский опыт моего приложения за несколько строк кода. Схема проста:

  1. Определите независимые сетевые операции
  2. Оберните их в блоки async { }
  3. Используйте await(), когда вам нужны результаты
  4. Обрабатывайте ошибки должным образом

Ваши пользователи сразу заметят разницу. В условиях современной конкуренции на рынке приложений каждая секунда загрузки имеет значение.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: