Android App Bundle — это новый формат публикации для Android-приложений, заменяющий монолитный APK. Сам пакет приложений непосредственно не устанавливается. Вместо этого Google Play генерирует оптимизированные APK для каждого устройства из пакета приложений. По сравнению с монолитным вариантом, APK, генерируемые из пакетов, как правило, намного меньше. Опыт разработки также упрощается, поскольку вам не нужно управлять и версировать несколько APK для разных конфигураций устройств в каждом выпуске, что экономит много времени.

Динамика Android App Bundle просто невероятна. Более 450 000 приложений и игр в Google Play используют пакеты приложений в продакшене, что составляет более 30% активных установок. Приложения, переключающиеся на пакеты, в среднем экономят размер на 16% по сравнению с использованием универсального APK. Эта экономия размера привела к тому, что партнеры отметили до 11% увеличения количества установок.

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

  • большие функции, которые используются только небольшим процентом пользователей;
  • специальные аппаратные или программные возможности, такие как доставка AR-модуля на совместимые устройства;
  • конкретные версии Android;
  • доставка больших библиотек с ограниченным сроком службы, которые могут быть установлены и удалены, если больше не нужны.

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

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

Самый последний и самый значимый

Наше видение пакетов приложений идет еще дальше. Мы работаем над динамическим фреймворком с большим количеством опций.

Эти опции включают в себя In-App Update, которое позволяет запускать и завершать обновления из приложения. Мы также работали над настраиваемой доставкой ресурсов, позволяющей пакету приложений включать пакеты ресурсов. Эта функция может быть особенно полезна разработчикам игр, которые хотят упаковать большие игровые ресурсы в двоичный файл в виде одного артефакта. Позже мы представили несколько вариантов доставки этих ресурсов пользователям.

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

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

Наконец, теперь есть внутренний общий доступ к приложениям, это упрощает тестирование пакетов и динамическую доставку. Данный механизм дает быстрый способ поделиться своими приложениями для локального тестирования. То есть Google Play устанавливает тестовое приложение на устройство точно так же, как если бы оно было выпущено официально. Все, что вам нужно сделать, — это загрузить пакет в Google Play, поделиться URL-адресом с тестировщиками, которые затем используют его для установки. Разработчики положительно отзываются о данном механизме доставки. Любой сотрудник вашей компании может быть загрузчиком приложения для внутреннего совместного использования без доступа к консоли Play.

Если у вас есть другие артефакты, загруженные на консоль Play, вы также сможете получить ссылки на установку. Для пакетов приложений перейдите в раздел для исследования пакета (bundle explore) и переключитесь на старую версию, а затем скопируйте ссылку установки. 

Наконец, мы добавили класс FakeSplitInstallManager для Play Core. Это позволяет тестировать приложение с динамическими функциями в автономном режиме. Обычно, когда вы загружаете функцию on-demand в приложение, SplitInstallManager запрашивает Play Store, чтобы установить разбиения для нее и вам требуется дожидаться, пока они загрузятся. Используя FakeSplitInstallManager, ваше приложение устанавливает разбиения, необходимые локально, в автономном режиме. Не нужно ждать их доставки из Play и установки. Это позволяет легко перебирать динамические функции на ранних стадиях процесса разработки без необходимости быть в сети и ждать Play Store. Вы все еще можете переключиться на SplitInstallManager и провести онлайн тестирование с внутренним общим доступом к приложениям. Это доступно в последней версии Play Core.

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

Теперь, когда у вас есть обзор обновлений Android App Bundle, давайте более внимательно рассмотрим функции и модульность в приложении.

Зависимости функции от функции

Сначала вспомним одну из ключевых возможностей пакетов приложений: динамические модули.

Допустим, у вас есть приложение с тремя различными функциями. Одна из них предлагает поддержку камеры (зеленый), вторая — поддержку видео (оранжевый), а третья — поддержку платежей (синий). Цель модульной обработки приложения состоит в том, чтобы разбить его на динамические функции для разделения кода. Модулизация также означает, что разные пользователи могут получить разные части приложения: пользователь 1 может использовать поддержку камеры, пользователь 2 может быть немного более продвинутым и использовать поддержку камеры и видео, а пользователь 3 может использовать функции камеры и оплаты.

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

Статистика Google Play показывает, что на каждые 3 мегабайта уменьшающегося размера приложения конверсии могут увеличиваться до 1%.

Есть и другие преимущества, которые вы получаете от модульной обработки приложения. Первый — это ускоренное инкрементное время сборки из-за возможности создавать только подмножество модулей. Это означает, что вы тратите меньше времени на сборку приложения и больше времени на его разработку. Еще одним преимуществом является логическое разделение кода. Например, если одна команда хочет работать с функцией камеры, а другая — с функцией видео, то между ними будет меньше зависимостей.

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

Динамическая доставка функций

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

  • Загрузка при установке, когда пользователь устанавливает приложение и динамические функции автоматически загружаются на его устройство.
  • Загрузка по необходимости, при которой динамические функции загружаются, когда приложение их запрашивает.
  • Условная доставка, которая зависит от конфигурации устройств пользователей. Например, устройство, поддерживающее AR, установит динамическую функцию поддержки AR, а устройства без поддержки AR — нет.

Вы также можете использовать все эти механизмы для мгновенной доставки приложений и игр — это то, когда вы доставляете небольшую (до 10 Мбайт) версию приложения, которую пользователь загружает, выбрав “Try Now” в Play Store. Однако там, где вы отмечаете свои мгновенные включенные динамические функции для at-install доставки, они устанавливаются автоматически, когда пользователь выбирает установку. Кроме того, если вы отмечаете функции для on-demand доставки и используете их в at-install доставке, вы должны предложить пользователю загрузить их из установленного приложения, так как они не будут загружаться автоматически при использовании им функции “мгновенное приложение” (“instant app”).

Зависимость базового APK

Изначально динамические функции зависели от базового APK. Приведем пример с нашим приложением. Если функции камеры и видео полагались на общее программное обеспечение для обработки изображений, то оно должно было находиться в APK.

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

С помощью Android Studio 4.0 вы можете определить динамические функции, которые зависят от других. Таким образом, в примере приложения вы можете извлечь весь код обработки изображений в динамическую функцию камеры.

Теперь пользователям, которые хотят использовать функцию оплаты, не нужно будет устанавливать APK-файл, включающий ПО для обработки изображений. Это означает, что размер станет меньше, и, возможно, конверсии улучшатся.

Чтобы начать использовать эту функцию, установите Android Studio 4.x Canary/Бета.

После того как вы разработали свои функции, укажите их в списке в файле базового приложения build.gradle. Если вы настраиваете зависимости существующих функций, вам не нужно вносить изменений.

// app/build.gradle
android {
    ...
    dynamicFeatures = [":camera", ":video", ":payment"]
}

Затем определите зависимости в файле build.gradle для каждой функции. Если вы делаете это для существующих функций, просто добавьте зависимость от :camera в файл с видео-функциями build.gradle.

// оплата/build.gradle 
dependencies {
  implementation project(':app')
}

// камера/build.gradle
dependencies {
  implementation project(':app')
}

// видео/build.gradle 
dependencies {
  implementation project(':app')
  implementation project(':camera')
}

Как только это будет сделано, Android Studio проанализирует дерево зависимостей, готовое к использованию в новых возможностях зависимостей Android Studio Canary. Однако, прежде чем вы сможете использовать это, вы должны добавить флаг функции. Для этого перейдите в меню Help > Edit Custom VM Options, добавьте rundebug.feature.on.feature, сохраните изменения и перезагрузите Android Studio.

В пункте Run > Edit Configuration и в разделе Run/Debug Configuration вы можете определить различные конфигурации установки для тестирования.

Если вы выберете функцию видео, то увидите, что для нее требуется камера. Таким образом, когда вы тестируете видео, Android Studio автоматически выберет камеру и установит ее при запуске теста. После отмены камеры, функция видео также будет отменена.

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

Навигация по приложению

Традиционный способ навигации — это использование API фреймворка, который предоставляет 2 варианта:

  • Начать activity с помощью Intent, что является самым простым способом запуска нового экрана для приложения.
  • Использование supportFragmentManager для замены фрагментов по мере их необходимости, чтобы перейти с одного экрана на другой.

Компонент архитектуры навигации Jetpack упрощает навигацию по экранам. Этот компонент включает в себя редактор навигации, в котором вы определяете пункты назначения, а затем управляете путями навигации. Навигация, определенная в редакторе, заполняет файл res/navigation/graph.xml, устраняющий необходимость в рукописном коде: хотя вы все еще можете вносить ручные правки.

Вы должны написать немного кода в layout.xml — файле для ссылки на NavHostFragment. Вы устанавливаете его в качестве фрагмента по умолчанию и ссылаетесь на созданный вами ID графика вручную либо с помощью редактора навигации.

<fragment
  android:id="@+id/nav_host_fragment"
  android:name="androidx.navigation.fragment.NavHostFragment"
  app:navGraph="@navigation/nav_graph" 
  … />

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

Навигация по динамическим функциям

Пакеты приложений и динамические модули функций изменили способ перехода приложений от базового модуля к функции. Приложение может иметь несколько модулей, установленных сразу же или позже, использовав один из настраиваемых вариантов доставки, упомянутых выше. Это означает, что модуль может не быть установлен, когда приложение указывает на него. Это вызывает проблемы для навигационного компонента, поскольку библиотека ожидает, что модуль будет находиться на устройстве, когда приложение перейдет к нему.Библиотеки Dynamic Feature Navigator (DFN) решают эту проблему.

DFN — это набор библиотек AndroidX, которые строятся поверх динамических функций, навигационных компонентов и библиотеки Play Core. Библиотека теперь доступна в альфа-версии.

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

1. В файле layout.xml замените NavHostFragment на DynamicNavHostFragment, новый класс, который обеспечивает базовую реализацию и самый простой способ взаимодействия с DFN.

До

<fragment
  android:id="@+id/nav_host_fragment"
  android:name="androidx.navigation.fragment.NavHostFragment"
  app:navGraph="@navigation/nav_graph" 
  … />

После

<fragment
  android:id="@+id/nav_host_fragment"
  android:name="androidx.navigation.dynamicfeatures.
  fragment.DynamicNavHostFragment"
  app:navGraph="@navigation/nav_graph"
  … />

2. Внесите изменения в свой навигационный график. Этот процесс идет параллельно с тем, когда вы объявляете модули имен в файлах build.gradle, чтобы сообщить приложению, какие модули установлены. В этом случае для каждой функции вы добавляете имя модуля и назначение каждой функции, чтобы динамический навигатор функций знал, где и как ее установить.

<navigation>
  <fragment
    app:moduleName="featureA"
    android:name="full.path.to.MyFragment"/>
  <activity
    app:moduleName="featureB"
    android:name="full.path.to.MyActivity"/>
</navigation>

С помощью этих изменений вы можете перейти от базового модуля, загрузить и установить новый, а также запустить его по мере необходимости. И все это без прикосновения к какому-либо другому коду. Навигатор заботится о ходе выполнения состояния установки и всех промежуточных состояниях за вас.

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

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

ProgressDestination проверяет, установлен ли модуль. Если модуль установлен, навигатор непосредственно и прозрачно перемещается к месту назначения. Если модуль не установлен, DFN загружает и устанавливает функцию, а затем переходит в целевую.

Вы также можете настроить этот процесс с помощью API, предоставленного расширением AbstractProgressFragment. Просто передайте ему ID макета, который хотите показать. Затем переопределите функцию onProgress, которая прозрачно вызывает API Play Core, чтобы приложение могло отображать прогресс пользователям. Существуют также функции для переопределения состояний выполнения, которые можно обработать.

abstract class AbstractProgressFragment(layoutId: Int) {
  abstract fun onProgress(status: Int, bytesDownloaded: Long, bytesTotal: Long)
  abstract fun onCancelled()
  abstract fun onFailed(errorCode: Int)
}

Чтобы задать пользовательский фрагмент progress, откройте graph.xml и добавьте идентификатор фрагмента в качестве progressFragment.

<navigation
  …
  app:progressFragment="@id/progress">
  <fragment
    android:id="@+id/progress"
    android:name="path.to.MyProgressFragment" />
</navigation>

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

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

Чтобы добиться этого, передайте DynamicInstallMonitor в Extras к вызову .navigate() и проверьте DynamicInstallMonitor.installRequired, чтобы определить, требуется ли установка. Если возвращается false, приложение может перейти к модулю. В противном случае вы подписываетесь на статус установки, чтобы получать актуальные данные из SplitInstallSessionStates. Вы также проверяете, находится ли установка в конечном состоянии, чтобы узнать, не завершилась ли она неудачно или была отменена и приложение не может перейти к функции. Чтобы использовать SplitInstallSessionStates, создайте InstallMonitor и передайте его в DynamicExtras. Затем DynamicExtras передаются в функцию навигации, где вы можете подписаться на текущие данные и наблюдать состояния разделения.

val extras = DynamicExtras(installMonitor = installMonitor)
navigate(destination, args, navOptions, extras)
installMonitor.status.observe(this, object : Observer {
  override fun onChanged(state: SplitInstallSessionState?) {
    …
  }
}

Функция динамического навигатора библиотеки полностью настраиваема через navigation-dynamic-features-core AndroidX, обеспечивающее все API навигатора и API для навигации activity с динамическими функциями и фрагментированными навигациями с динамическими функциями.

Начните с версии фрагмента, потому что это дает самый простой способ взаимодействия с навигатором динамических объектов.

Для начала добавьте зависимости в файл build.gradle:

dependencies {
  def nav_version = "2.3.0-alpha04"
  api "androidx.navigation:navigation-fragment-ktx:$nav_version"
  api "androidx.navigation:navigation-ui-ktx:$nav_version"
  api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
}

И когда вы обнаружите баг или другая проблема в снэпшоте будет иметь запрос функции, сообщите об этом goo.gle/navigation-bug.

Библиотека расширений Kotlin для Play Core

Не каждое приложение использует навигационный компонент, а также не каждая динамическая функция имеет UI, фрагмент или activity, например. Тут появляется библиотека расширений Kotlin для Play Core. Благодаря своей структуре, она не заменяет основной артефакт Play Core, а строится поверх него (она использует те же самые API под капотом)

Опираясь на существующий API, мы воспользовались возможностью упростить его и помочь вам разобраться в правильных процессах и рекомендуемых шаблонах использования API. Это делается путем использования мощи корутин Kotlin. Для иллюстрации вот оригинальный асинхронный API Play Core:

manager.startInstall(request): Task

Большинство методов в SplitInstallManager возвращаются немедленно с задачей, которая устанавливает обратные вызовы. Например, addOnSuccessListener позволяет прослушивать работу, которая должна быть завершена, чтобы вы могли работать дальше. Вам также нужно добавить прослушиватель сбоев, с которым вы будете получать исключения и обрабатывать любые сбои.

Итак, как это работает в Play Core KTX? Это один и тот же вызов, но он имеет несколько отличий.

Во-первых, нет никаких уродливых обратных вызовов. Этот вызов является последовательным: он возвращает результат с ID сеанса вместо передачи его через задачу и обратные вызовы. Эта функция на самом деле является функцией приостановки, поэтому она должна работать в корутине. Функция приостанавливается, но не блокируется, так что можно безопасно вызывать ее из основного потока. Кроме того, она возвращает результат, который можно присвоить значению.

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

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

// ПЕРЕД ЗАПУСКОМ ЗАПРОСА УСТАНОВКИ
manager.registerListener(listener)

...

val listener = SplitInstallStateUpdatedListener { state ->
  if (state.sessionId == sessionId) {
    when (state.status()) {
      // ОТВЕТ СОБЫТИЯМ ПРОГРЕССА
    }
  }
}

...

// ОЧИСТКА ПОСЛЕ ВЫХОДА ИЗ ОБЛАСТИ
manager.unregisterListener(listener)

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

Познакомьтесь с API под названием Flow, который является частью библиотеки корутин Kotlin. В этом примере запрашивается поток, который выдает события состояния об установке разделений из API Play Core.

viewModelScope.launch {
    manager.requestProgressFlow()
           .collect { state -> ... }
}

Функция collect на Flow также приостанавливается, это означает, что она будет работать в корутине. Важное свойство: корутины поддерживают отмену. В библиотеках KTX для AndroidX вы получаете расширения для моделей представления, activity и фрагментов. Эти расширения предоставляют вам области для запуска ваших корутин до тех пор, пока ваша модель представления (или фрагмент, или действие) активна, поэтому вы будете продолжать получать события.

Как только пользователь покинет этот экран и область действия будет отменена, мы отменяем Flow, очищаем все прослушиватели, и вы больше не получите никаких событий после завершения жизненного цикла. Кроме того, Flow и библиотеки корутин Kotlin имеют множество встроенных операторов, поэтому вы можете работать с потоками событий более удобным способом.

Здесь, например, я фильтрую поток событий только для обновлений интересующего меня модуля:

viewModelScope.launch {
    manager.requestProgressFlow()
           .filter { "myModule" in it.moduleNames }
           .collect { state -> … }
}

Это гораздо лучший способ работы с этим API.

Процесс установки модуля довольно сложен — он проходит через множество различных этапов. Как вы узнаете, как разработчик, какие из них важны, от каких вам нужен дескриптор для продолжения установки, а какие просто влияют на пользовательский интерфейс, но не являются существенными для завершения установки?

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

private val listener = SplitInstallStateUpdatedListener(
    onRequiresConfirmation = { state -> // ← ТРЕБУЕМЫЕ АРГУМЕНТЫ
     // УПРАВЛЕНИЕ ДИАЛОГОМ ПОДТВЕРЖДЕНИЯ
    },
    onInstalled = {
       // УПРАВЛЕНИЕ УСТАНОВЛЕННЫМ СОСТОЯНИЕМ
    },
    onDownloading = { state -> // ← АРГУМЕНТЫ ПО ВЫБОРУ
        // ПОКАЗАТЬ ПРОГРЕСС
    },
    …
)

Для обязательных состояний, которые вам нужно обработать (например, подтверждение от пользователя для загрузки большого модуля) вам нужно предоставить эти аргументы. И конечно, все остальные состояния являются необязательными аргументами, поэтому вы можете обрабатывать их и показывать в своем UI.

Заключение 

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

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

Использование в полной мере динамических функций пакетов приложений для обеспечения минимизации загрузки и on-device размера приложений и игр поможет добиться лучшей конверсии и снизить вероятность удаления приложения.

Дополнительные сведения см. в разделе Android App Bundle на веб-сайте разработчиков Android.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Wojtek Kaliciński: Navigating your way around customizable delivery

Предыдущая статьяString, StringBuilder и StringBuffer: понимаете ли вы разницу?
Следующая статьяНовый взгляд на старые истины: принцип «Не повторяйся!» (DRY)