GetX удобен. Действительно удобен, лаконичен, функционален, выразителен. Но порою его функционала не хватает. В частности, ниже рассматриваются ситуации, когда стандартного инжектирования контроллеров средствами Get недостаточно.
Описание кейсов
Пример 1. Управлять контроллерами в страницах PageView
Архитектура PageView такова, что субстраницы строятся одномоментно в процессе build и при переходах не пересоздаются. Но предположим, что по бизнес-логике необходимо рассматривать эти субстраницы, как отдельные элементы, например инициализировать и отключать таймеры, обновлять поля, открывать и закрывать какие-то ресурсы при переходе между ними. Явно просится прикрутить по контроллеру к каждой из них.
body: PageView( controller: controller.pageController, children: [ HomePage(), // + HomePageController BusinessPage(), // + BusinessPageController ], ),
Вообще, следует начать с того, что непонятно, куда прикручивать инжектирование. Биндить к странице-владельцу PageView? Но теряется контекстность применения, ведь контроллеры управляют данными конкретных субстраниц, а onInit/onReady/onClose контроллеров никак не будут соответствовать моментам переходов. Размещать в build вообще не хочется, неправильно это. В конструкторы субстраниц — тоже не решает проблемы, они не пересоздаются (см. выше). Вообще ни один стандартный подход не решает вопроса.
Из лога видно, что никакой привязки контроллеров не получается.
А вот как это должно быть:
Очевидно, что теперь жизненные циклы контроллеров страниц отслеживаются. Это позволяет управлять ресурсами в контексте бизнес-логики.
Пример 2. Управлять контроллерами в Get.bottomSheet
История похожая. Имеется View, которая переиспользуется в Get.bottomSheet с разными параметрами при разных вызовах….
/// Туда Get.bottomSheet(SubRouterDialog( 'Go to Sub', okCallback: () { Get.back(); Get.to(() => HomeSubPage()); }, cancelCallback: () { Get.back(); }, )); /// ... и обратно Get.bottomSheet(SubRouterDialog( 'Go back to Home', okCallback: () { Get.back(); Get.back(); }, cancelCallback: () { Get.back(); }, color: Colors.green, ));
Get.bottomSheet какой он есть, не в состоянии переинжектировать контроллер, и логика рушится.
А вот как он должен функционировать:
Почему так происходит
Если в двух словах. то виджеты и их контроллеры, участвующие в GetPageRoute, синхронизируют свои жизненные циклы, и все работает из коробки — при смене роута в навигаторе нужные контроллеры заново инициализируются, ненужные удаляются. Для операций вне роутинга это не предусмотрено. А наши кейсы — как раз вне роутинга.
Как с этим бороться
Для решения этих задач я использую небольшую надстройку в виде двух классов на стороне виджета и одного на стороне контроллера.
Statex* — автоинжектирование без ограничений
На стороне виджетов введены 2 класса:
1. StatexWidget — базовый абстрактный класс
Его задача в том, чтобы позади основного дерева виджетов внедрить служебный микровиджет _StatexWidgetInjector (по сути, пустой контейнер). Вся работа производится в этом виджете.
abstract class StatexWidget<T extends StatexController> extends StatelessWidget { /// Базовый конструктор для передачи билдера и свойств. /// Под капотом вызывает Get.put(...), передавая туда билдер, тег /// и флаг permanent. /// Удобен для передачи параметров прямо по месту вызова, но не поддерживает /// принцип Dependency Inversion, так как точный тип контроллера нужно знать /// в точке вызова. /// Если требуется что-то типа Get.lazyPut<Base>(()=>Inherited()), /// то следует воспользоваться конструктором [StatexWidget.find] /// /// ``` /// class BusinessPage extends StatexWidget<_BusinessPageControllerImpl> { /// BusinessPage() : super(() => _BusinessPageControllerImpl(), /// tag: 'TAG', permanent: true, args: {'key': value} ); /// /// ``` const StatexWidget( this.builder, { this.tag, this.permanent = false, this.args = const <String, dynamic>{}, Key? key, }) : super(key: key); /// Конструктор для работы в паре с [Get.lazyPut<Some>(()=>SomeImpl())]. /// Другими словами, для поддержания концепции Dependency Inversion. /// /// [markAsPermanent] используется в случаях, //// когда конструктор `StatexWidget.find` будет вызываться /// в паре с ранее зарегистрированной ленивой фабрикой Get.lazyPut. /// Для Get.lazyPut нельзя сделать контроллер перманентным, /// только возобновляемым при помощи свойства `fenix`. /// Но fenix заново создает контроллер, убивая его состояние. /// В случае создания контроллера [markAsPermanent] передаст /// свое значение в инжектор Get.put(..., permanent = markAsPermanent), /// тем самым создав перманентный контроллер. /// И соответственно, при [dispose] не будет удаления Get.delete<T> /// для этого типа. /// /// ``` /// // Где-то в инжекторе определяем фабрику и параметры, /// // подставляя имплементацию /// Get.lazyPut<HomePageController>( /// () => HomePageControllerImpl(), /// fenix: true, /// ); /// /// // Используем [StatexWidget.find], передавая дополнительные параметры. /// // Если инстанс не существует, он будет создан с нужными параметрами. /// // Иначе будет найден и выдан текущий инстанс. /// class HomePage extends StatexWidget<HomePageController> { /// HomePage({Key? key}) : super.find( /// markAsPermanent: true, /// key: key, /// ); /// /// ``` const StatexWidget.find({ String? tag, bool markAsPermanent = false, Map<String, dynamic> args = const <String, dynamic>{}, Key? key, }) : this(null, tag: tag, permanent: markAsPermanent, args: args, key: key); /// [builder] обязателен для базового конструктора, но не используется /// для [StatexWidget.find] final InstanceBuilderCallback<T>? builder; /// final String? tag; final bool permanent; final Map<String, dynamic> args; T get controller => GetInstance().find<T>(tag: tag); Widget buildWidget(BuildContext context); /// Идея в том, чтобы внедрить виджет-менеджер времени /// жизни контроллера в стек позади основного дерева клиента. @override Widget build(BuildContext context) { // Необходимый стек для внедрения [_StatexWidget] return Stack( fit: StackFit.passthrough, children: [ // Wrapping уменьшает геометрию виджета до минимально возможной Wrap( children: [ _StatexWidgetInjector<T>( builder, tag: tag, permanent: permanent, args: args, ), ], ), buildWidget(context), ], ); } }
2. _StatexWidget + State: Виджет управления состояниями контроллера.
/// Контрольный [StatefulWidget] class _StatexWidgetInjector<T extends StatexController> extends StatefulWidget { _StatexWidgetInjector( InstanceBuilderCallback<T>? builder, { this.tag, this.permanent = false, Map<String, dynamic> args = const <String, dynamic>{}, Key? key, }) : super(key: key) { // Инжектирование контроллера прямо в конструкторе виджета. final inst = GetInstance(); if (builder != null && !inst.isRegistered<T>(tag: tag)) { inst.put(builder(), tag: tag, permanent: permanent); } final c = inst.find<T>(tag: tag); c.args = args; } final String? tag; final bool permanent; @override _StatexWidgetInjectorState<T> createState() => _StatexWidgetInjectorState<T>(); } /// Состояние для [_StatexWidgetInjector] /// Управляет вызовами [onWidgetInitState], [onWidgetDisposed] /// и в случае необходимости, удаляет инстанс контроллера /// из памяти class _StatexWidgetInjectorState<T extends StatexController> extends State<_StatexWidgetInjector<T>> { @override initState() { super.initState(); final wc = GetInstance().find<T>(tag: widget.tag); wc.onWidgetInitState(); } @override void dispose() { final inst = GetInstance(); if (inst.isRegistered<T>(tag: widget.tag)) { final wc = inst.find<T>(tag: widget.tag); wc.onWidgetDisposed(); if (!widget.permanent) { Get.delete<T>(tag: widget.tag); } } super.dispose(); } @override Widget build(BuildContext context) => Container(); }
Это StatefulWidget и поэтому он поддерживает полный жизненный цикл при перестройке дерева, в котором находится, включая initState и dispose. Этим и воспользуемся.
Вот как это работает:
-
Переданный в конструкторе билдер (если есть), создаст нам инстанс контроллера с требуемыми параметрами, или вернет существующий
-
В ином случае мы попытаемся найти инстанс через
GetInstance.find -
Напоследок передадим в инстанс аргументы
-
Далее, в
_StatexWidgetState.initState, вызываетсяonInitWidgetStateдавая возможность произвести нужные действия в момент инициализации дерева -
А в
_StatexWidgetState.disposeмы производим вызовonWidgetDisposed, и при необходимости удаляем контроллер
StatexView.find — теперь и с инверсией зависимостей
Отдельного разговора заслуживает именованный конструктор StatexView.find.
Передача билдера конкретного типа в конструктор не вписывается в Dependency Inversion. Если необходимо управлять имплементациями, подойдет связка Get.lazyPut<Some>(()=>SomeImpl) + StatexView.find().
Это работает так
// [1.] // Где-то в инжекторе определяется имплементация интерфейса, // например, вот так Get.lazyPut<HomePageController>( () => HomePageControllerImpl(), fenix: true, ); // или так Get.lazyPut<HomeSubPageController>( () => HomeSubPageControllerImpl(), tag: HomeSubPageController.someTagForFindStrategy, fenix: true, ); // [2.] // В конструкторе виджета идет обращение к StatexView.find // вот так HomePage({Key? key}) : super.find( markAsPermanent: true, key: key, ); // или так HomeSubPage({Key? key}) : super.find( key: key, tag: HomeSubPageController.someTagForFindStrategy, );
Этого достаточно, чтобы произошел поиск инстанса, создание нужной имплементации в случае неоходимости, и все заверте всего остального цикла работы.
Подготовка контроллера
Чтобы все окончательно заработало, на стороне контролллера введен тип StatexController
abstract class StatexController extends GetxController { final _args = <String, dynamic>{}; Map<String, dynamic> get args => _args; set args(Map<String, dynamic> value) => _args.assignAll(value); /// Вызывается в момент [_StatexWidgetState.initState]. /// Таким образом можно отлавливать момент перехода на страницы /// в [PageView], например void onWidgetInitState() {} /// Вызывается в момент [_StatexWidgetState.dispose]. void onWidgetDisposed() {} }
Конкретно инжектирования там касается только два метода-события, которые вызываются из StatexWidget в нужное время.
Как собрать все это воедино
-
Унаследовать контроллер от
StatexController. Если есть необходимость, переопределить нужные методы -
Унаследовать
WidgetотStatexWidget -
В конструкторе вызвать конструктор суперкласса
-
либо основной — для простого инжектирования
-
либо
super.findдля связки с ленивой инициализацией. Имеет смысл только для инверсии зависимостей
-
-
Вместо
buildреализоватьbuildWidget -
Профит
-
Полный набор классов доступен в gist
-
Пример использования находится здесь (ветка без Statex*, ветка с использованием Statex*)
PS (от 26 июля)
Сегодня закрыли мой issue насчет Get.bottomSheet по поводу отсутствия dispose, так что возможно, что для этого кейса вопрос уже решен (будем проверять). Однако описываемое в статье решение позволяет использовать его универсально для любых виджетов, имеющих детерминированный жизненный цикл.
ссылка на оригинал статьи https://habr.com/ru/articles/568488/
Добавить комментарий