Русский
Русский
English
Статистика
Реклама

Android dev

Детальный разбор навигации в Flutter

24.07.2020 14:12:14 | Автор: admin

Навигация во Flutter


image


Flutter набирает популярность среди разработчиков. Большенство подходов впостроении приложений уже устоялись иприменяются ежедневно вразработке E-commerce приложений. Тема навигации опускают навторой или третий план. Какой API навигации предоставляет Фреймворк? Какие подходы выработаны? Как использовать эти подходы иначто они годятся?


Введение


Начнём с того, что такое навигация? Навигация это метод который позволяет перемещаться между пользовательским интерфейсом с заданными параметрами.
К примеру в IOS мире организовывает навигацию UIViewController, а в Android Navigation component. А чтопредоставляет Flutter?



Экраны в Flutter называются route. Для перемещениями между route существует класс Navigator который имеющий обширный API для реализации различных видов навигации.



Начнём спростого.Навигация нановый экран(route) вызывается методом push() который принимает всебя один аргумент это Route.


Navigator.push(MaterialPageRoute(builder: (BuildContext context) => MyPage()));

Давайте детальнее разберёмся ввызове метода push:


Navigator Виджет, который управляет навигацией.
Navigator.push() метод который добавляет новый route виерархию виджетов.
MaterialPageRoute() Модальный route, который заменяет весь экран адаптивным кплатформе переходом анимации.
builder обязательный аргумент конструктора MaterialPageRoute, который возвращает пользовательский интерфейс Фреймворк для отрисовки.
[MyPage](https://miro.medium.com/max/664/1Xm96KtLeIAAMtAYWcr1-MA.png)* пользовательский интерфейс реализованный при помощи Stateful/Stateless Widget


Возвращение на предыдущий route


Для возвращения с экрана на предыдущий необходимо использовать метод pop().


Navigator.pop();

Переда данных между экранами


Данные на новый экран передаются через конструктор при создании экрана в builder методе. Этот принцип работает в методах Navigator, где нужно реализовать метод build.


Navigator.push(context, MaterialPageRoute(builder: (context) => MyPage(someData: data)));

В примере продемонстрирована передача данных в класс MyPage (в этом классе хранится пользовательский интерфейс).


Для того чтобы передать данные на предыдущий экран нужно вызвать метод pop() и передать опциональным аргументом туда данные.


Navigator.pop(data);


Состояние виджета Navigator, который вызван внутри одного из видов MaterialApp/CupertinoApp/WidgetsApp. State отвечает за хранение истории навигации и предоставляет API для управления историей.
Базовые методы навигации повторяют структуру данных Stack. В диаграмме можно наблюдать методы и "call flow" NavigatorState.


http://personeltest.ru/aways/habrastorage.org/webt/5w/dg/nb/5wdgnb-tjlngub4c8y4rlpqkeqi.png


Императивный vs Декларативный подход в навигации


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


Давайте на простом примере:


Императивный подход , отвечает на вопрос как?
Пример: Я вижу, что тот угловой столик свободен. Мы пойдём туда и сядем там.


Декларативный подход, отвечает на вопрос что?
Пример: Столик для двоих, пожалуйста.


Для более глубокого понимания разницы советую прочитать эту статью Imperative vs Declarative Programming


Императивная навигация


Вернёмся к реализации навигации. В императивном подходе описывается детали работы в вызывающем коде. В нашем случае это поля Route. В Flutter много типов route, например MaterialPageRoute и CupertinoPageRoute. Например в CupertinoPageRoute задаётся title, или settings.


Пример:


Navigator.push(    context,    CupertinoPageRoute<T>(        title: "Setting",        builder: (BuildContext context) => MyPage(),        settings: RouteSettings(name:"my_page"),    ),);

Этот код и знания о новом route будут хранитьсяв ViewModel/Controller/BLoC/ У этот подхода существует недостаток.


Представим что потребовалось внести изменения в конструкторе в MyPage или в CupertinoPageRoute. Нужно искать каждый вызов метода push в проекте и изменять кодовую базу.


Вывод:


Этот подход не имеет единообразный подход к навигации, и знание о реализации route проникает в бизнес логику.

Декларативная навигация


Принцип декларативной навигации заключается в использовании декларативного подхода в программировании. Давайте разберём на примере.


Пример:


Navigator.pushNamed(context, '/my_page');

Принцип императивной навигации выглядит куда проще. Говорите "Отправь пользователя на экран настроек" передавая путь одним из аргументов навигации.
Для хранении реализации роста в самом Фреймворке предусмотрен механизм у MaterialApp/CupertinoApp/WidgetsApp. Это 2 колбэка onGenerateRoute и onUnknownRoute отвеспющие за хранение деталей реализации.


Пример:


MaterialApp(    onUnknownRoute: (settings) => CupertinoPageRoute(      builder: (context) {                return UndefinedView(name: settings.name);            }    ),  onGenerateRoute: (settings) {    if (settings.name == '\my_page') {      return CupertinoPageRoute(                title: "MyPage",                settings: settings,        builder: (context) => MyPage(),      );    }        // Тут будут описание других роутов  },);

Разберёмся подробнее в реализации:
Метод onGenerateRoute данный метод срабатывает когда был вызван Navigator.pushNamed(). Метод должен вернуть route.
Метод onUnknownRoute срабатывает когда метод onGenerateRoute вернул null. должен вернуть дефолтный route, по аналогии с web сайтами 404 page.


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

Диалоговые и модальные окна


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


Методы для вызова диалоговых и модальных окон:


  • showAboutDialog
  • showBottomSheet
  • showDatePicker
  • showGeneralDialog
  • showMenu
  • showModalBottomSheet
  • showSearch
  • showTimePicker
  • showCupertinoDialog
  • showDialog
  • showLicensePage
  • showCupertinoModalPopup

Эти методы покрывают базовые потребности разработчиков которые хотят работать с окнами. Если не хватает этого API, тогда стоит разобраться как эти методы работают.


Как работает это под капотом?


Давайте рассмотрим исходный код одного из методов, например showGeneralDialog.


Исходный код:


Future<T> showGeneralDialog<T>({  @required BuildContext context,  @required RoutePageBuilder pageBuilder,  bool barrierDismissible,  String barrierLabel,  Color barrierColor,  Duration transitionDuration,  RouteTransitionsBuilder transitionBuilder,  bool useRootNavigator = true,  RouteSettings routeSettings,}) {  assert(pageBuilder != null);  assert(useRootNavigator != null);  assert(!barrierDismissible || barrierLabel != null);  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(    pageBuilder: pageBuilder,    barrierDismissible: barrierDismissible,    barrierLabel: barrierLabel,    barrierColor: barrierColor,    transitionDuration: transitionDuration,    transitionBuilder: transitionBuilder,    settings: routeSettings,  ));}

Давайте детальнее разберёмся в устройстве этого метода. showGeneralDialog вызывает метод push у NavigatorState с _DialogRoute(). Нижнее подчёркивание обозначает что этот класс приватный и используется только в пределах области видимости в которой сам описан, то есть в пределах этого файла.


Диалоговые и модальные окна которые отображаются при помощи глобальных методов это кастомные route которые реализованы разработчиками Фреймворка.

Типы route в фреймворке


Теперь понятно что"every thins is a route", то есть что связанное с навигацией. Давайте взглянем на то, какие route уже реализованы в Фреймворке.


Два основных route в Flutter это PageRoute и PopupRoute.


PageRoute Модальный route, который заменяет весь экран.
PopupRoute Модальный route, который накладывает виджет поверх текущего route.


Реализации PageRoute:


  • MaterialPageRoute
  • CupertinoPageRoute
  • _SearchPageRoute
  • PageRouteBuilder

Реализации PopupRoute:


  • _ContexMenuRoute
  • _DialogRoute
  • _ModalBottomSheetRoute
  • _CupertinoModalPopupRoute
  • _PopupMenuRoute

Реализация PopupRoute приватна и скрыты для внешних потребителей, но роуты используют глобальные методы для показа диалоговых и модальных окон.


Вывод:


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

Best practices


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


Начнём с того что мы сделаем некий сервис который будет будет соблюдать следующим аспектам:


  • Декларативный вызов навигации.
  • Отказ от использования BuildContext для навигации (Это критично если сервис навигации будет вызываться в компонентах, в которых нет возможности получить BuildContext).
  • Модульность. Можно вызвать любой route, CupertinoPageRoute, BottomSheetRoute, DialogRoute и т.д.

Для нашего сервиса навигации нам понадобится интерфейс:


abstract class IRouter {  Future<T> routeTo<T extends Object>(RouteBundle bundle);  Future<bool> back<T extends Object>({T data, bool rootNavigator});  GlobalKey<NavigatorState> rootNavigatorKey;}

Разберём методы:
routeTo - выполняет навигацию на новый экран.
back возвращает на предыдущий экран.
rootNavigatorKey GlobalKey умеющий вызывать методы NavigatorState.

После того как мы сделали интерфейс навигации, давайте сделаем реализацию этого интерфейса.


class Router implements IRouter {    @override  GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();    @override  Future<T> routeTo<T>(RouteBundle bundle) async {   // Push logic here  }    @override  Future<bool> back<T>({T data, bool rootNavigator = false}) async {        // Back logic here    }}

Супер, теперь нам нужно реализовать метод routeTo().


@override  Future<T> routeTo<T>(RouteBundle bundle) async {        assert(bundle != null, "The bundle [RouteBundle.bundle] is null");    NavigatorState rootState = rootNavigatorKey.currentState;    assert(rootState != null, 'rootState [NavigatorState] is null');    switch (bundle.route) {      case "/routeExample":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: RouteExample(),          ),        );      case "/my_page":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: MyPage(),          ),        );      default:        throw Exception('Route is not found');    }  }

Данный метод вызывает у root NavigatorState (который описан в WidgetsApp) метод push и конфигурирует его относительно RouteBundle который приходит одним из аргументов в данный метод.


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


enum ContainerType {  /// The parent type is [Scaffold].  ///  /// In IOS route with an iOS transition [CupertinoPageRoute].  /// In Android route with an Android transition [MaterialPageRoute].  ///  scaffold,  /// Used for show child in dialog.  ///  /// Route with [DialogRoute].  dialog,  /// Used for show child in [BottomSheet].  ///  /// Route with [ModalBottomSheetRoute].  bottomSheet,  /// Used for show child only.  /// [AppBar] and other features is not implemented.  window,}class RouteBundle {  /// Creates a bundle that can be used for [Router].  RouteBundle({    this.route,    this.containerType,  });  /// The route for current navigation.  ///  /// See [Routes] for details.  final String route;  /// The current status of this animation.  final ContainerType containerType;}

enum ContainerType тип контейнера, котрый будет задаваться декларативно из вызываемого кода.
RouteBundle класс-холдер данных отвечающих конфигурацию нового route.


Как вы могли заметить у я использовал метод _buildRoute. Именно он отвечает за то, кой тип route будет вызван.


Route<T> _buildRoute<T>({@required RouteBundle bundle, @required Widget child}) {    assert(bundle.containerType != null, "The bundle.containerType [RouteBundle.containerType] is null");    switch (bundle.containerType) {      case ContainerType.scaffold:        return CupertinoPageRoute<T>(          title: bundle.title,          builder: (BuildContext context) => child,          settings: RouteSettings(name: bundle.route),        );      case ContainerType.dialog:        return DialogRoute<T>(          title: '123',          settings: RouteSettings(name: bundle.route),          pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {                        return child;                    },        );      case ContainerType.bottomSheet:        return ModalBottomSheetRoute<T>(          settings: RouteSettings(name: bundle.route),          isScrollControlled: true,          builder: (BuildContext context) => child,        );      case ContainerType.window:        return CupertinoPageRoute<T>(          settings: RouteSettings(name: bundle.route),          builder: (BuildContext context) => child,        );      default:        throw Exception('ContainerType is not found');    }  }

Думаю что в этой функции стоит рассказать о ModalBottomSheetRoute и DialogRoute, которые использую. Исходный код этих route позаимствован из раздела Material исходного кода Flutter.


Осталось сделать метод back.


@overrideFuture<bool> back<T>({T data, bool rootNavigator = false}) async {    NavigatorState rootState = rootNavigatorKey.currentState;  return await (rootState).maybePop<T>(data);}

Ну и конечно перед использованием сервиса необходимо передать rootNavigatorKey в App следующим образом:


MaterialApp(    navigatorKey: widget.router.rootNavigatorKey,    home: Home());

Кодовая база для нашего сервиса готова, давайте вызовем наш route. Для этого создадим инстанс нашего сервиса и каким-либо образом "прокинуть" в объект, который будет вызывать этот инстанс, например при помощи Dependency Injection.



router.routeTo(RouteBundle(route: '/my_page', containerType: ContainerType.window));

Теперь мы имеем единый подход к навигации, позволяющий решить вышеперечисленные проблемы:


  • Декларативный вызов навигации
  • Отказ от BuildContext по средствам GlobalKey
  • Модульность достигнута возможностью конфигурирования route относительно имени пути и контейнера для View

Итог


В Фреймворке Flutter существуют различные методы для навигации, которые дают преимущества и недостатки.


Ну и конечно полезные ссылки:
Мой телеграм канал
Мои друзья Flutter Dev Podcast
Вакансии Flutter разработчиков

Подробнее..

Конфигурация многомодульных проектов

26.08.2020 14:06:49 | Автор: admin

Предыстория


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

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

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



Первая итерация вынос версий библиотек


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

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

Скорее всего, вы уже видели такой код в проекте. В нем нет никакой магии, это просто одно из расширений Gradle под названием ExtraPropertiesExtension. Если кратко, то это просто Map<String, Object>, доступный по имени ext в объектe project, а все остальное работа как будто с объектом, блоки конфигурации и прочее магия Gradle. Примеры:
.gradle .gradle.kts
// creationext {  dagger = '2.25.3'  fabric = '1.25.4'  mindk = 17}// usageprintln(dagger)println(fabric)println(mindk)

// creationval dagger by extra { "2.25.3" }val fabric by extra { "1.25.4" }val minSdk by extra { 17 }// usageval dagger: String by extra.propertiesval fabric: String by extra.propertiesval minSdk: Int by extra.properties


Что мне нравится в этом подходе: он крайне простой и помогает версиям не разъезжаться. Но у него есть минусы: надо следить, чтобы разработчики использовали версии из этого набора, и это не сильно упрощает создание новых модулей, потому что всё равно приходится копировать еще много чего.

Кстати, подобного эффекта можно добиться, используя gradle.properties вместо ExtraPropertiesExtension, только будьте осторожны: ваши версии можно будет переопределить при сборке с помощью -P флагов, а если вы обращаетесь к переменной просто по имени в groovy-cкриптах, то gradle.properties заменят и их. Пример с gradle.properties и переопределением:

// grdle.propertiesoverriden=2// build.gradleext.dagger = 1ext.overriden = 1// module/build.gradleprintln(rootProject.ext.dagger)   // 1println(dagger)                   // 1println(rootProject.ext.overriden)// 1println(overriden)                // 2

Вторая итерация project.subprojects


Моя любознательность, помноноженная на нежелание копировать код и разбираться с настройкой каждого модуля, привела меня к следующему шагу: я вспомнил, что в корневом build.gradle есть блок, который генерируется по умолчанию allprojects.

allprojects {    repositories {        google()        jcenter()    }}

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

Пример конфигурации модулей через project.subprojects
subprojects { project ->    afterEvaluate {        final boolean isAndroidProject =            (project.pluginManager.hasPlugin('com.android.application') ||                project.pluginManager.hasPlugin('com.android.library'))        if (isAndroidProject) {            apply plugin: 'kotlin-android'            apply plugin: 'kotlin-android-extensions'            apply plugin: 'kotlin-kapt'                        android {                compileSdkVersion rootProject.ext.compileSdkVersion                                defaultConfig {                    minSdkVersion rootProject.ext.minSdkVersion                    targetSdkVersion rootProject.ext.targetSdkVersion                                        vectorDrawables.useSupportLibrary = true                }                compileOptions {                    encoding 'UTF-8'                    sourceCompatibility JavaVersion.VERSION_1_8                    targetCompatibility JavaVersion.VERSION_1_8                }                androidExtensions {                    experimental = true                }            }        }        dependencies {            if (isAndroidProject) {                // android dependencies here            }                        // all subprojects dependencies here        }        project.tasks            .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)            .all {                kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()            }    }}


Теперь для любого модуля с подключенным плагином com.android.application или com.android.library мы можем настраивать что угодно: подключаемые плагины, конфигурации плагинов, зависимости.

Все было бы отлично, если бы не пара проблем: если мы захотим в модуле переопределить какие-то параметры, заданные в subprojects, то у нас это не получится, потому что конфигурация модуля происходит до применения subprojects (спасибо afterEvaluate). А еще если мы захотим не применять это автоматическое конфигурирование в отдельных модулях, то в блоке subprojects начнет появляться много дополнительных проверок. Поэтому я стал думать дальше.

Третья итерация buildSrc и plugin


До этого момента я уже несколько раз слышал про buildSrc и видел примеры, в которых buildSrc использовали как альтернативу первому шагу из этой статьи. А еще я слышал про gradle pluginы, поэтому стал копать в этом направлении. Все оказалось очень просто: у Gradle есть документация по разработке кастомных плагинов, в которой все написано.

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

Код плагина
import org.gradle.api.JavaVersionimport org.gradle.api.Pluginimport org.gradle.api.Projectclass ModulePlugin implements Plugin<Project> {    @Override    void apply(Project target) {        target.pluginManager.apply("com.android.library")        target.pluginManager.apply("kotlin-android")        target.pluginManager.apply("kotlin-android-extensions")        target.pluginManager.apply("kotlin-kapt")        target.android {            compileSdkVersion Versions.sdk.compile            defaultConfig {                minSdkVersion Versions.sdk.min                targetSdkVersion Versions.sdk.target                javaCompileOptions {                    annotationProcessorOptions {                        arguments << ["dagger.gradle.incremental": "true"]                    }                }            }            // resources prefix: modulename_            resourcePrefix "${target.name.replace("-", "_")}_"            lintOptions {                baseline "lint-baseline.xml"            }            compileOptions {                encoding 'UTF-8'                sourceCompatibility JavaVersion.VERSION_1_8                targetCompatibility JavaVersion.VERSION_1_8            }            testOptions {                unitTests {                    returnDefaultValues true                    includeAndroidResources true                }            }        }        target.repositories {            google()            mavenCentral()            jcenter()                        // add other repositories here        }        target.dependencies {            implementation Dependencies.dagger.dagger            implementation Dependencies.dagger.android            kapt Dependencies.dagger.compiler            kapt Dependencies.dagger.androidProcessor            testImplementation Dependencies.test.junit                        // add other dependencies here        }    }}


Теперь конфигурация нового проекта выглядит как applyplugin:ru.yandex.money.module и все. Можно вносить свои дополнения в блок android или dependencies, можно добавлять плагины или настраивать их, но главное, что новый модуль конфигурируется одной строкой, а его конфигурация всегда актуальна и продуктовому разработчику больше не надо думать про настройку.

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

Важный момент: если вы используете android gradle plugin ниже 4.0, то некоторые вещи очень сложно сделать в kotlin-скриптах по крайней мере, блок android проще конфигурировать в groovy-скриптах. Там есть проблема с тем, что некоторые типы недоступны при компиляции, а groovy динамически типизированный, и ему это не важно =)

Дальше standalone plugin или монорепо


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

Первый вариант standalone plugin для gradle. После третьего шага это уже не так сложно: надо создать отдельный проект, перенести туда код и настроить публикацию.

Плюсы: плагин можно шарить между несколькими проектами, что упростит жизнь не в одном проекте, а в экосистеме.

Минусы: версионирование при обновлении плагина придется обновлять и проверять его работоспособность в нескольких проектах сразу, а это может занять время. Кстати, на эту тему у моих коллег из бэкенд-разработки есть отличное решение, ключевое слово modernizer инструмент, который сам ходит по репозиториям и обновляет зависимости. Не буду на этом долго задерживаться, пусть лучше они сами расскажут.

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

Итого


В новом проекте делай сразу третий шаг buildSrc и plugin проще будет всем, тем более, что код я приложил. А второй шаг project.subprojects используй для того, чтобы подключать общие модули между собой.

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

Перевод Практическое руководство по использованию Hilt с Kotlin

09.12.2020 18:14:11 | Автор: admin

Будущих учащихся на курсе "Android Developer. Professional" приглашаем посетить открытый урок на тему "Пишем Gradle Plugin"


А также делимся переводом полезной статьи.


Простой способ использовать внедрение зависимостей в приложениях для Android

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

Настройка Hilt

Чтобы настроить Hilt в своем приложении, сначала выполните указания из руководства: Установка Gradle Build.

После установки всех необходимых элементов и подключаемых модулей, чтобы использовать Hilt, задайте своему классуApplicationаннотацию@HiltAndroidApp. Больше ничего делать не нужно, а также не нужно вызывать Hilt напрямую.

Определение и внедрение зависимостей

При написании кода, в котором используется внедрение зависимостей, есть два основных компонента, которые следует учитывать:

  1. Классы, имеющие зависимости, которые вы собираетесь внедрить.

  2. Классы, которые могут быть внедрены как зависимости.

Они не являются взаимоисключающими: во многих случаях класс одновременно является внедряемым и имеет зависимости.

Как сделать зависимость внедряемой

Чтобы в Hilt сделать объект внедряемым, необходимо указать для Hilt способ создания экземпляра этого объекта. Такие инструкции называютсяпривязками.

Есть три способа определения привязки в Hilt.

  1. Добавить к конструктору аннотацию@Inject

  2. Использовать@Bindsв модуле

  3. Использовать@Providesв модуле

Добавление к конструктору аннотации@Inject

У любого класса может быть конструктор с аннотацией@Inject, что позволяет использовать его в качестве зависимости в любом месте проекта.

Использование модуля

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

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

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

Модули устанавливаются вкомпонент Hilt, который указывается с помощью аннотации@InstallIn. Я дам более подробное объяснение ниже.

Вариант 1: использовать@Binds, чтобы создать привязку для интерфейса

Если вы хотите использовать в своем кодеOatMilk, когда требуетсяMilk, создайте абстрактный метод внутри модуля и задайте ему аннотацию@Binds. Обращаю внимание, чтобы этот вариант работал, сам OatMilk должен быть внедряемым. Для этого его конструктору необходимо задать аннотацию@Inject.

Вариант 2: создать функцию-фабрику с помощью@Provides

Когда экземпляр нельзя сконструировать напрямую, можно создать поставщик. Поставщик это функция-фабрика, которая возвращает экземпляр объекта.

В качестве примера можно привести системную службу, скажемConnectivityManager, которую необходимо получить из контекста.

ОбъектContextявляется внедряемым по умолчанию, если задать ему аннотацию@ApplicationContextлибо@ActivityContext.

Внедрение зависимости

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

  1. Как параметры конструктора

  2. Как поля

Как параметры конструктора

Если пометить конструктор аннотацией @Inject, Hilt внедрит все параметры в соответствии с привязками, которые вы определили для этих типов.

Как поля

Если класс является точкой входа, указанной с использованием аннотации@AndroidEntryPoint (подробнее об этом рассказано в следующем разделе), внедрены будут все поля, помеченные аннотацией@Inject.

Поля, помеченные аннотацией@Inject,должны быть общедоступными. Также удобно пометить их модификатором lateinit, чтобы не пришлось обеспечивать поддержку ими пустого значения, поскольку перед внедрением они имеют исходное значениеnull.

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

Прочие важные понятия

Точка входа

Помните, я сказал, что во многих случаях класс создается путем внедренияиимеет зависимости, внедренные в него? В некоторых случаях у вас будет класс, который несоздаетсяпутем внедрения зависимости, но имеет зависимости, внедренные в него. Хорошим примером этого являются активности, которые в стандартной ситуации создаются платформой Android, а не библиотекой Hilt.

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

Точка входа Android

Большинство ваших точек входа будут так называемымиточками входа Android:

  • Activity

  • Fragment

  • View

  • Service

  • BroadcastReceiver

Если это так, такой точке входа следует задать аннотацию@AndroidEntryPoint.

Другие точки входа

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

ViewModel

ViewModel это особый случай: экземпляры этого класса не создаются напрямую, так как они должны создаваться платформой, при этом он также не является точкой входа Android. Вместо этого с классамиViewModel используют специальную аннотацию@ViewModelInject, которая позволяет Hilt внедрять в них зависимости, когда они создаются с помощью выраженияby viewModels(). Это похоже на то, как@Injectработает для других классов.

Если вам требуется доступ к состоянию, сохраняемому в классе ViewModel, внедритеSavedStateHandleв качестве параметра конструктора, добавив аннотацию@Assisted.

Чтобы использовать@ViewModelInject, вам нужно будет добавить еще несколько зависимостей. Дополнительные сведения см. в статье: Интеграции Hilt и Jetpack.

Компоненты

Каждый модуль устанавливается внутрикомпонента Hilt, который указывается с помощью аннотации@InstallIn(<компонент>). Компонент модуля используется главным образом для предотвращения случайного внедрения зависимости не в том месте. Например, аннотация @InstallIn(ServiceComponent.class) не позволит использовать привязки и поставщики, имеющиеся в соответствующем модуле, внутри активности.

Кроме того, использование привязки можно ограничить пределами компонента, в котором находится модуль. Что приводит меня к

Области

По умолчанию у привязок нет областей. В приведенном выше примере это означает, что каждый раз, когда вы внедряетеMilk, вы получаете новый экземплярOatMilk. Если добавить аннотацию@ActivityScoped,область использования привязки будет ограничена пределами ActivityComponent.

Теперь, когда у модуля есть область действия, Hilt будет создавать только одинOatMilkна экземпляр активности. Кроме того, этот экземплярOatMilkбудет привязан к жизненному циклу этой активности он будет создаваться при вызовеonCreate()активности и уничтожаться при вызовеonDestroy()активности.

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

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

Область зависит от компонента, в который установлен ваш модуль. Например,@ActivityScopedможно применить только к привязкам, находящимся внутри модуля, который установлен внутриActivityComponent.

Область также определяет жизненный цикл внедренных экземпляров: в данном случае одиночный экземплярMilk, используемыйFridgeиLatteActivity, создается, когдаonCreate()вызывается дляLatteActivity, и уничтожается в егоonDestroy(). Это также означает, что нашMilkне переживет изменение конфигурации, поскольку при этом вызываетсяonDestroy()для активности. Преодолеть это можно путем использования области с более длительным жизненным циклом, например@ActivityRetainedScope.

Список имеющихся областей, компонентов, которым они соответствуют, и жизненных циклов, которым они следуют, приведен в статье:Компоненты Hilt.

Внедрение поставщика

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

Внедрение поставщика можно использовать независимо от того, чем является зависимость и как она внедряется. Любой объект, который можно внедрить, можно обернуть вProvider<>, чтобы для него использовалось внедрение поставщика.

Фреймворки внедрения зависимостей (такие как Dagger иGuice) обычно применяются в крупномасштабных, сложных проектах. В то же время библиотека Hilt, будучи простой в освоении и настройке, предоставляет все возможности Dagger в пакете, который можно использовать в приложениях любого типа, независимо от размеров кодовой базы.

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


Узнать подробнее о курсе "Android Developer. Professional"

Записаться на открытый урок "Пишем Gradle Plugin"


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

ЗАБРАТЬ СКИДКУ

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru