Размер приложения часто играет важную роль в восприятии его пользователями и принятии ими решения о скачивании. Исследования показывают, что чем меньше размер APK, тем выше вероятность его установки и тем реже оно оказывается в списке на удаление. Конечно же, это важно для команды Яндекс Go, так как наше приложение непрерывно развивается.

Для внедрения одной из SDK, основанной на Flutter, мы прибегли к механизму Dynamic Feature Delivery (DFD). Но оказалось, что в русскоязычном сегменте информации о DFD крайне мало, поэтому я решил поделиться нашим опытом работы с этим механизмом на Android с Google Play Feature Delivery Library. Также мы нашли способ интегрировать динамические фичи на устройства без Google Play, но это тема для отдельной статьи, поэтому в рамках данного материала я на этом останавливаться не буду.

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

SplitInstallManager: что это и как работает

В центре механизма DFD находится SplitInstallManager, который предоставляет библиотека com.google.android.play:feature-delivery. SplitInstallManager служит интерфейсом для управления динамическими модулями приложения. Он позволяет разработчикам загружать и устанавливать отдельные динамические модули по мере необходимости, что значительно снижает размер основной части приложения.

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

val request = SplitInstallRequest.newBuilder()
    .addModule("dynamic_feature")
    .build()

splitInstallManager.startInstall(request)
    .addOnSuccessListener { sessionId ->
        // Хендлим успешно скачанную и установленную фичу
    }
    .addOnFailureListener { exception ->
        // Хендлим ошибку
    }

Перечислю основные функции SplitInstallManager:

  1. Проверка установленных модулей. Метод SplitInstallManager.getInstalledModules() позволяет определить, установлены ли уже необходимые модули.

  2. Запрос установки модуля. Если модуль не установлен, создаётся объект SplitInstallRequest с указанием его имени. Этот запрос передаётся в метод SplitInstallManager.startInstall(), который возвращает Task<Integer>, представляющий идентификатор сессии установки. Этот идентификатор может быть использован повторно при последующих запросах установки.

  3. Отслеживание прогресса установки. Для мониторинга состояния установки используется метод SplitInstallManager.registerListener(), который позволяет зарегистрировать SplitInstallStateUpdatedListener. Этот слушатель будет получать обновления о текущем состоянии установки, таком как прогресс, статус и возникшие ошибки.

  4. Обработка ошибок. При установке нескольких модулей одновременно через SplitInstallRequest.addModule(...) и последующем запросе установки только одного из них может возникнуть ошибка INCOMPATIBLE_WITH_EXISTING_SESSION. Ошибки до получения идентификатора сессии или во время установки также будут обработаны через addOnFailureListener.

Однако, несмотря на свои преимущества, использование SplitInstallManager связано с определёнными неудобствами. Интерфейс этого класса достаточно громоздкий и требует выполнения нескольких шагов, включая создание запроса на скачивание, обработку различных состояний процесса и управление ошибками. Это усложняет код и затрудняет его поддержку.

Кроме того, интеграция SplitInstallManager в приложения, следующие принципам чистой архитектуры, представляет дополнительную сложность. Чистая архитектура предполагает разделение бизнес‑логики, дата‑слоя и пользовательского интерфейса, что обеспечивает гибкость и лёгкость тестирования. Однако метод startConfirmationDialogForResult в SplitInstallManager нарушает принцип единственной ответственности (Single Responsibility Principle), поскольку требует непосредственного взаимодействия с UI при возникновении статуса SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION. Это смешивает обязанности дата‑слоя и слоя UI, затрудняя его интеграцию в приложение и усложняя тестирование и поддержание кода.

Для преодоления этих трудностей потребуется создание абстракций, которые будут скрывать детали реализации SplitInstallManager, изолируя бизнес‑логику от инфраструктурных аспектов и упрощая тестирование. В следующем разделе мы рассмотрим, как создать такую обёртку и какие методы она должна включать для обеспечения удобства и эффективности.

Реализация обёртки для SplitInstallManager

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

interface DynamicFeatureApi

Для бо́льшей гибкости мы установим обязательное условие: реализации интерфейса DynamicFeatureApi будут представлены через object Kotlin, как рекомендуется в гайдах Google. Это позволит запускать функциональность динамических модулей непосредственно через объект. Такой подход требует добавления соответствующих правил для ProGuard и R8, чтобы сохранить классы и поля, необходимые для корректной работы с рефлексией.

-keep public class * implements com.yandex.go.dynamic.api.DynamicFeatureApi
-keepclassmembers public class * implements com.yandex.go.dynamic.api.DynamicFeatureApi {
  static <fields>;
}

Эти правила позволят сохранить имена объектов, реализующих DynamicFeatureApi, чтобы мы могли успешно обращаться к ним через рефлексию после успешной установки модуля.

Для корректной работы каждой динамической фичи необходимо указать соответствующие настройки в AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:dist="http://schemas.android.com/apk/distribution"
    xmlns:tools="http://schemas.android.com/tools">

  <dist:module
      dist:instant="false"
      tools:node="replace"
      dist:title="@string/dynamic_feature_title">
    <dist:delivery>
      <dist:on-demand />
    </dist:delivery>
    <dist:fusing dist:include="false" />
  </dist:module>
</manifest>

Эти настройки определяют, что фичу можно загрузить по запросу (dist:on-demand) и она не включается в процесс fusing. Это позволяет контролировать установку фичи отдельно от основного APK.

Также создадим класс DynamicFeature, в котором будет храниться уникальный идентификатор для каждой динамической фичи и имя целевого object с указанием его package. Особенно важно, что это имя модуля в gradle, так как именно по нему происходит загрузка нужного динамического модуля. Это позволит однозначно связывать динамические модули с их реализациями, гарантируя правильную инициализацию и использование функциональности после установки.

enum class DynamicFeature(
    val id: String,
    val targetClass: String
) {
    // Перечисление динамических фич в приложении
}

В дальнейшем id из DynamicFeature будет использоваться как уникальный идентификатор для каждой реализации DynamicFeatureApi и для работы с динамическим модулем через DynamicFeatureLoader. Это обеспечит однозначную идентификацию и упрощённое управление динамическими фичами.

Для интеграции динамических фич в многомодульном проекте необходимо пройти несколько этапов, начиная с создания самого модуля и заканчивая настройкой его загрузки, установки и использования.

В нашем примере проект многомодульный, и каждая фича разделена на два модуля: api и impl. Сам DynamicFeatureLoader также будет рассмотрен как отдельная фича, которая будет разделена на такие же два модуля: api и impl. Все переиспользуемые интерфейсы и базовые контракты будут сложены в модуль :features:dynamic:api, что обеспечит единый интерфейс для работы с динамическими модулями. В свою очередь, реализация, включая класс DynamicFeatureLoaderImpl, будет находиться в модуле :features:dynamic:impl. Это поможет легко обновлять или модифицировать логику загрузки динамических фич без изменения контракта в других частях приложения.

Теперь рассмотрим процесс создания новой динамической фичи подробно:

  1. Создаём новый модуль, который станет dynamic. Например, :features:awesome_feature.

  2. Создаём модуль :features:awesome_feature:api. В нём описывается внешний контракт фичи для всех остальных потребителей в проекте. Это позволит отделить реализацию фичи от её api.

  3. Создаём модуль :features:awesome_feature:impl. Этот модуль подключает api фичи (:features:awesome_feature:api) и описывает базовую реализацию фичи, включая UI, связанный с её динамической загрузкой. Важно: в этот модуль не добавляем тяжёлые библиотеки или ресурсы.

  4. Добавляем интерфейс для работы с dynamic‑частью в модуль :features:awesome_feature:impl (например, interface AwesomeFeatureDynamicApi : DynamicFeatureApi).

  5. Создаём объект реализации динамической части: в модуле :features:awesome_feature:dynamic создаём объект object AwesomeFeatureDynamicImpl : AwesomeFeatureDynamicApi.

  6. Обязательно добавляем новый модуль в enum DynamicFeature. Это позволит удобно управлять всеми динамическими фичами проекта.

  7. Используем DynamicFeatureLoader в модуле :features:awesome_feature:impl для загрузки и установки нужных модулей при необходимости.

SplitInstallManager предназначен для проверки установленных модулей и их скачивания при необходимости. Исходя из этого, можно выделить состояния модуля или фичи. Теперь рассмотрим список возможных состояний фичи и покажем схему с переходами между этими состояниями.

Эти состояния охватывают полный жизненный цикл загрузки и установки динамической фичи:

  • Unavailable. Фича недоступна в приложении. Например, её функциональность не предусмотрена для определённых сборок.

  • NotLoaded. Фича доступна для загрузки, но ещё не загружена. Это начальное состояние фичи, которая доступна, но ещё не установлена.

  • Loading. Фича находится в процессе загрузки или установки.

  • Error. Произошла ошибка во время загрузки.

  • Ready. Фича загружена, установлена и готова к использованию.

Таким образом описываем sealed interface DynamicFeatureState:

sealed interface DynamicFeatureState<out T : DynamicFeatureApi> {
    class Unavailable<T : DynamicFeatureApi> : DynamicFeatureState<T>
    class NotLoaded<T : DynamicFeatureApi> : DynamicFeatureState<T>
    class Loading<T : DynamicFeatureApi> : DynamicFeatureState<T>
    class Error<T : DynamicFeatureApi>(val error: Throwable) : DynamicFeatureState<T>
    class Ready<T : DynamicFeatureApi>(val instance: T) : DynamicFeatureState<T>
}

Далее определим основные методы для нашей обёртки вокруг SplitInstallManager. Эти методы обеспечат удобный интерфейс для работы с динамическими функциями и модулями.

Наша обёртка должна включать следующую функциональность:

interface DynamicFeatureLoader {
    fun <T : DynamicFeatureApi> feature(feature: DynamicFeature): DynamicFeatureState<T>
    fun <T : DynamicFeatureApi> featureFlow(feature: DynamicFeature): Flow<DynamicFeatureState<T>>
    fun <T : DynamicFeatureApi> startDownload(feature: DynamicFeature): Deferred<T?>
    fun prefetch(feature: DynamicFeature)
}

Эти методы охватывают ключевые сценарии использования:

  • feature — позволяет получить текущее состояние динамической фичи;

  • featureFlow — обеспечивает возможность подписки на изменения состояния фичи в процессе её загрузки и установки;

  • startDownload — запускает загрузку фичи и позволяет дождаться завершения загрузки, а также отслеживать состояние фичи через подписчиков featureFlow.

  • prefetch — загрузка фичи в фоне без возможности отслеживания процесса загрузки.

Если способ использования и назначение метода startDownload понятны, то необходимость метода prefetch не так очевидна. Этот метод позволяет отложить установку модуля до момента, когда приложение будет неактивно (метод deferredInstall у SplitInstallManager). Документация описывает это как «best‑effort when the app is in the background». На практике модуль загружается, когда приложение закрыто и Google Play устанавливает обновления. Отслеживать процесс в этом случае невозможно, так как он выполняется при неактивном приложении.

Теперь приступим к реализации интерфейса DynamicFeatureLoader. В частности, создадим его реализацию DynamicFeatureLoaderImpl, которую сделаем синглтоном, чтобы централизованно обрабатывать загрузку всех динамических фич в приложении. Для хранения состояния используем хеш‑мапу featureStateFlowsMap, где ключом будет DynamicFeature, а значением — мутабельный StateFlow с состоянием DynamicFeatureState<DynamicFeatureApi>.

@Singleton
class DynamicFeatureLoaderImpl @Inject constructor(
  private val context: Context,
  private val scope: CoroutineScope
) : DynamicFeatureLoader {

    private val featureStateFlowsMap = mutableMapOf<DynamicFeature, MutableStateFlow<DynamicFeatureState<DynamicFeatureApi>>>()

    private val splitInstallManager: SplitInstallManager by lazy {
        SplitInstallManagerFactory.create(context)
    }
}

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

override fun <T : DynamicFeatureApi> feature(feature: DynamicFeature): DynamicFeatureState<T> {
    return obtainFeatureStartState(feature)
}

private fun <T : DynamicFeatureApi> obtainFeatureStartState(feature: DynamicFeature): DynamicFeatureState<T> {
    val possibleClass = getDynamicFeatureClass(feature)
    if (possibleClass != null && feature.id in splitInstallManager.installedModules) {
        return DynamicFeatureState.Ready(getFeatureInstance(feature, possibleClass))
    }

    return DynamicFeatureState.NotLoaded()
}

private fun <T : DynamicFeatureApi> getFeatureInstance(feature: DynamicFeature, targetClass: Class<*>): T {
    val instanceField = targetClass.declaredFields.find { it.name == "INSTANCE" && it.type == targetClass }
        ?: throw IllegalStateException("Invalid feature entry point. ${feature.targetClass} must be a Kotlin object")

    return instanceField.get(null) as T
}

private fun getDynamicFeatureClass(feature: DynamicFeature): Class<*>? = try {
    Class.forName(feature.targetClass)
} catch (ex: ClassNotFoundException) {
    null
} catch (ex: LinkageError) {
    null
}

Метод featureFlow возвращает Flow состояний фичи, на который можно подписаться. Это удобно для отслеживания процесса загрузки и реагирования на изменения состояния в реальном времени. Использование Flow упрощает работу с асинхронными событиями загрузки в реактивном стиле.

override fun <T : DynamicFeatureApi> featureFlow(feature: DynamicFeature): Flow<DynamicFeatureState<T>> {
    return flow {
        emitAll(internalFeatureStateFlow(feature))
    }
}

private suspend fun <T : DynamicFeatureApi> internalFeatureStateFlow(feature: DynamicFeature): MutableStateFlow<DynamicFeatureState<T>> {
    return mapMutex.withLock {
        featureStateFlowsMap.computeIfAbsent(feature) {
            MutableStateFlow(obtainFeatureStartState<T>(feature))
        } as MutableStateFlow<DynamicFeatureState<T>>
    }
}

Метод startDownload инициирует загрузку динамической фичи и возвращает результат в виде Deferred. Основная задача при реализации этого метода — работа с SplitInstallStateUpdatedListener, который позволяет отслеживать статус загрузки и установки динамической фичи в режиме реального времени. Поскольку DynamicFeatureLoaderImpl использует корутины, мы будем использовать suspendCancellableCoroutine для оборачивания вызова метода splitInstallManager.startInstall и регистрации SplitInstallStateUpdatedListener. Начнём с реализации логики статусов динамических фич, соответствующей ранее описанным переходам.

override fun <T : DynamicFeatureApi> startDownload(feature: DynamicFeature): Deferred<T?> {
    return scope.async {
      try {
        val featureStateFlow = internalFeatureStateFlow<T>(feature)
        val shouldStartDownload = startDownloadingMutex.withLock {
          val currentValue = featureStateFlow.value
          currentValue is DynamicFeatureState.NotLoaded || currentValue is DynamicFeatureState.Error
        }

        if (shouldStartDownload) {
          downloadFeature(feature, featureStateFlow)
        }

        val terminalState = featureStateFlow.first { it is DynamicFeatureState.Ready || it is DynamicFeatureState.Error }
        if (terminalState is DynamicFeatureState.Ready) {
          terminalState.instance
        } else {
          null
        }
      } catch (throwable: Throwable) {
        if (throwable is CancellationException) {
          throw throwable
        }

        internalFeatureStateFlow<T>(feature).emit(DynamicFeatureState.Error(throwable))
        null
      }
    }
}

Теперь реализуемТеперь реализуем метод downloadFeature. Поскольку у нас есть MutableStateFlow для каждой динамической фичи в featureStateFlowsMap, сигнатура метода будет следующей:

private suspend fun <T : DynamicFeatureApi> downloadFeature(
    feature: DynamicFeature,
    featureState: MutableStateFlow<DynamicFeatureState<T>>
) {
    // Реализация
}

В этом методе мы будем использовать SplitInstallStateUpdatedListener для отслеживания статусов установки и загрузки фичи. Один из неочевидных моментов — обработка статуса SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION, который требует вызова splitInstallManager.startConfirmationDialogForResult, передавая одним из аргументов Activity. Поскольку DynamicFeatureLoaderImpl работает на уровне нашего приложения, оптимальным решением для получения Activity будет использование WeakReference.

Реализация слушателя будет выглядеть следующим образом
private fun <T : DynamicFeatureApi> getInstallStateListener(
    feature: DynamicFeature,
    onSuccess: (T) -> Unit,
    onError: (Throwable) -> Unit,
    featureState: MutableStateFlow<DynamicFeatureState<T>>,
    getCurrentSessionId: () -> Int
): SplitInstallStateUpdatedListener {
    return SplitInstallStateUpdatedListener { state ->
      // Пропускаем все апдейты состояния, не относящиеся к нашей сессии, — skip other requests state update
      if (state.sessionId() != getCurrentSessionId()) return@SplitInstallStateUpdatedListener

      val newState: DynamicFeatureState<T>? = when (state.status()) {
        SplitInstallSessionStatus.UNKNOWN -> null
        SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> SplitHelper.activity?.get()?.let {
          showConfirmationDialog(state, it, feature)
          null
        }

        SplitInstallSessionStatus.PENDING,
        SplitInstallSessionStatus.DOWNLOADING,
        SplitInstallSessionStatus.DOWNLOADED,
        SplitInstallSessionStatus.INSTALLING -> loading()

        SplitInstallSessionStatus.INSTALLED -> {
          val instance = getFeatureInstance<T>(feature, getDynamicFeatureClass(feature)!!)
          onSuccess.invoke(instance)
          DynamicFeatureState.Ready(instance)
        }

        SplitInstallSessionStatus.FAILED -> {
          onError.invoke(DynamicFeatureDownloadingException())
          DynamicFeatureState.Error(DynamicFeatureDownloadingException())
        }

        SplitInstallSessionStatus.CANCELING -> loading()
        SplitInstallSessionStatus.CANCELED -> {
          onError.invoke(DynamicFeatureCanceledException())
          DynamicFeatureState.Error(DynamicFeatureCanceledException())
        }

        else -> null
      }

      if (newState != null) {
        featureState.value = newState
      }
    }
}

А тут соберём метод downloadFeature
private suspend fun <T : DynamicFeatureApi> downloadFeature(
    feature: DynamicFeature,
    featureState: MutableStateFlow<DynamicFeatureState<T>>
) {
    suspendCancellableCoroutine { continuation ->
      val request = SplitInstallRequest.newBuilder()
        .addModule(feature.id)
        .build()

      var currentSessionId = 0
      val installStateListener = getInstallStateListener(
        feature = feature,
        onSuccess = { _ ->
          continuation.resume(Unit)
        },
        onError = {
          continuation.resume(Unit)
        },
        featureState = featureState,
        getCurrentSessionId = { currentSessionId }
      )
      splitInstallManager.registerListener(installStateListener)

      continuation.invokeOnCancellation {
        splitInstallManager.unregisterListener(installStateListener)
        if (!splitInstallManager.installedModules.contains(feature.id)) {
          splitInstallManager.cancelInstall(currentSessionId)
          featureState.tryEmit(DynamicFeatureState.Error(DynamicFeatureCanceledException()))
        }
      }

      splitInstallManager.startInstall(request)
        .addOnSuccessListener { sessionId ->
          currentSessionId = sessionId
        }
        .addOnFailureListener { e ->
          featureState.value = DynamicFeatureState.Error(e)
        }
    }
}

Метод prefetch используется для предварительной загрузки динамической фичи в фоновом режиме. Он не позволяет отслеживать прогресс обновления, как это делает SplitInstallStateUpdatedListener, но избавляет от необходимости обрабатывать статус SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION.

override fun prefetch(feature: DynamicFeature) {
    splitInstallManager.deferredInstall(listOf(feature.id))
        .addOnSuccessListener {
            // do something
        }
        .addOnFailureListener {
            // do something
        }
}

Этот набор методов позволяет удобно работать с динамическими фичами, следуя принципам чистой архитектуры. Обёртка скрывает детали реализации SplitInstallManager, предоставляя разработчикам простой и понятный интерфейс для управления загрузкой и установкой динамических модулей приложения.

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

SplitCompat.installActivity(activity)
SplitInstallHelper.updateAppInfo(activity) // если это instant app

Эти методы необходимы, чтобы динамическая фича корректно интегрировалась в приложение. SplitCompat.installActivity(activity) обновляет контекст activity, позволяя модулю работать корректно, а SplitInstallHelper.updateAppInfo(activity) обновляет информацию о приложении. Только после выполнения этих действий можно безопасно обращаться к object‑точке входа динамической фичи.

Реализация и тестирование DynamicFeatureLoader

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

Google предоставляет полезный инструмент для тестирования под названием FakeSplitInstallManager. Он позволяет эмулировать процесс загрузки и установки динамических фич, что упрощает локальное тестирование. Вы можете использовать FakeSplitInstallManager, чтобы проверить, как приложение обрабатывает различные сценарии установки, включая успешные и ошибочные состояния. Однако стоит отметить, что единственный метод для эмуляции ошибок в этом классе — setShouldNetworkError, что ограничивает возможности тестирования.

Ещё одна важная проблема заключается в несовместимости FakeSplitInstallManager с динамическими фичами, реализованными на базе Flutter. При попытке протестировать такую динамическую фичу будем стабильно получать краш приложения с ошибкой вида:

[ERROR:flutter/runtime/dart_vm_data.cc(20)] VM snapshot invalid and could not be inferred from settings.
[ERROR:flutter/runtime/dart_vm.cc(270)] Could not set up VM data to bootstrap the VM from.
[ERROR:flutter/runtime/dart_vm_lifecycle.cc(85)] Could not create Dart VM instance.

Раз за разом мы будем получать ошибку запуска виртуальной машины для Dart. При этом ошибка не воспроизводится в сборке с прод‑окружением с использованием боевой версии SplitInstallManager. К сожалению, это проблема, решения для которой Google не предлагает. Такая особенность делает тестирование динамических фич, использующих Flutter, сложным.

Для решения этой задачи и обеспечения гибкости тестирования можно создать собственный инструмент — EmulationSplitInstallManager, который будет выступать связующим звеном между SplitInstallManager и тестами. Это позволит легко эмулировать процессы скачивания и установки модулей. В результате реализация DynamicFeatureLoaderImpl будет выглядеть следующим образом:

@Singleton
class DynamicFeatureLoaderImpl @Inject constructor(
  private val context: Context,
  private val scope: CoroutineScope
) : DynamicFeatureLoader {

    private val featureStateFlowsMap = mutableMapOf<DynamicFeature, MutableStateFlow<DynamicFeatureState<DynamicFeatureApi>>>()

    private val splitInstallManager: SplitInstallManager by lazy {
        when (val type = emulationType()) {
            null -> SplitInstallManagerFactory.create(context)
            EmulationType.GOOGLE_FAKE -> FakeSplitInstallManagerFactory.create(context)
            EmulationType.EMULATION_SUCCESS,
            EmulationType.EMULATION_INSTALL_ERROR,
            EmulationType.EMULATION_DOWNLOAD_ERROR -> EmulationSplitInstallManager(context, type)
        }
    }
}

Стоит отметить, что внутренняя реализация EmulationSplitInstallManager заслуживает отдельной статьи, поэтому в рамках текущего материала мы не будем рассматривать её детально. Принципиальное отличие использования этого менеджера заключается в том, что динамическая фича поставляется сразу вместе с приложением. Процесс скачивания и установки эмулируется с возможностью намеренно вызывать ошибки на разных этапах, что делает тестирование Flutter динамических модулей более гибким и удобным для команды QA.

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


В этой статье мы подробно рассмотрели процесс управления динамическими фичами в Android‑приложениях с использованием SplitInstallManager, предоставленного Google. Мы разобрали основные аспекты реализации, такие как установка модулей и управление их состоянием, а также разработали удобный интерфейс DynamicFeatureLoader для взаимодействия с механизмом динамической загрузки модулей в приложении.

В частности, мы обсудили создание обёрток для работы с SplitInstallManager, что позволило нам выделить важные методы, такие как startDownload и prefetch, и упростить управление состояниями динамических фич. Переход от обычного использования SplitInstallManager к более гибкому и удобному интерфейсу DynamicFeatureLoader позволил проще интегрировать динамические фичи в приложения, соответствующие принципам чистой архитектуры.

Одним из ключевых этапов нашего подхода стало тестирование. Мы рассмотрели инструменты для локального тестирования, включая FakeSplitInstallManager, и выявили его ограничения, особенно в контексте приложений, использующих Flutter. В качестве решения этой проблемы мы предложили подход с созданием собственного EmulationSplitInstallManager, который предоставляет гибкие возможности для эмуляции различных сценариев и состояний.

Таким образом, мы построили полноценное решение для управления динамическими фичами, которое не только соответствует требованиям современного приложения, но и легко тестируется и поддерживается. Интеграция SplitInstallManager в ваш проект с помощью описанного подхода позволит вам эффективно управлять динамическими модулями и обеспечивать высокое качество пользовательского опыта.

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