Размер приложения часто играет важную роль в восприятии его пользователями и принятии ими решения о скачивании. Исследования показывают, что чем меньше размер 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
:
Проверка установленных модулей. Метод
SplitInstallManager.getInstalledModules()
позволяет определить, установлены ли уже необходимые модули.Запрос установки модуля. Если модуль не установлен, создаётся объект
SplitInstallRequest
с указанием его имени. Этот запрос передаётся в методSplitInstallManager.startInstall()
, который возвращаетTask<Integer>
, представляющий идентификатор сессии установки. Этот идентификатор может быть использован повторно при последующих запросах установки.Отслеживание прогресса установки. Для мониторинга состояния установки используется метод
SplitInstallManager.registerListener()
, который позволяет зарегистрироватьSplitInstallStateUpdatedListener
. Этот слушатель будет получать обновления о текущем состоянии установки, таком как прогресс, статус и возникшие ошибки.Обработка ошибок. При установке нескольких модулей одновременно через
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
. Это поможет легко обновлять или модифицировать логику загрузки динамических фич без изменения контракта в других частях приложения.
Теперь рассмотрим процесс создания новой динамической фичи подробно:
Создаём новый модуль, который станет dynamic. Например,
:features:awesome_feature
.Создаём модуль
:features:awesome_feature:api
. В нём описывается внешний контракт фичи для всех остальных потребителей в проекте. Это позволит отделить реализацию фичи от её api.Создаём модуль
:features:awesome_feature:impl
. Этот модуль подключает api фичи (:features:awesome_feature:api
) и описывает базовую реализацию фичи, включая UI, связанный с её динамической загрузкой. Важно: в этот модуль не добавляем тяжёлые библиотеки или ресурсы.Добавляем интерфейс для работы с dynamic‑частью в модуль
:features:awesome_feature:impl
(например,interface AwesomeFeatureDynamicApi : DynamicFeatureApi
).Создаём объект реализации динамической части: в модуле
:features:awesome_feature:dynamic
создаём объектobject AwesomeFeatureDynamicImpl : AwesomeFeatureDynamicApi
.Обязательно добавляем новый модуль в
enum DynamicFeature
. Это позволит удобно управлять всеми динамическими фичами проекта.Используем 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
в ваш проект с помощью описанного подхода позволит вам эффективно управлять динамическими модулями и обеспечивать высокое качество пользовательского опыта.