Last active
May 2, 2022 17:51
-
-
Save ValeriusGC/e6b967842be8cab42d0131ba51781cc4 to your computer and use it in GitHub Desktop.
Расширение возможностей инжектирования контроллеров для GetX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/widgets.dart'; | |
import 'package:get/get.dart'; | |
import 'package:rxdart/rxdart.dart' as rx; | |
/// Автоинжектор для связки времени жизни одного контроллера с одним виджетом | |
/// | |
/// Является заменителем `...binding: BindingsBuilder.put(() => T())` | |
/// или `Get.put(()=>T())` | |
/// в случаях, когда требуется автоматически привязать время жизни контроллера | |
/// к имплементациям виджетов, не участвующих в GetPageRoutes | |
/// | |
/// Примеры: | |
/// | |
/// 1. PageView.childs[Widget1(), Widget2(), ...] | |
/// 2. Get.bottomSheet(Widget()) | |
/// | |
/// Эти виджеты не участвуют в роутинге, и их контроллеры ничего не знают | |
/// об их инициализации или удалении. | |
/// | |
/// Автоинжектор [StatexWidget] позволяет решить эту проблему. | |
/// | |
/// Плюсы: | |
/// - Автоматическое создание/подтягивание контроллера в момент создания объекта | |
/// Вьюхи и как следствие: | |
/// - Можно использовать простое создание вьюх как в PageView, | |
/// а контроллеры сами подтянутся | |
/// - Можно использовать вместо биндинга | |
/// - Можно комбинировать именованный рутинг и создание напрямую через фабрики | |
/// -- последнее позволяет гибко настраивать поведение рутинга | |
/// при помощи аргументов | |
/// - Можно использовать теги | |
/// - Можно использовать permanent | |
/// | |
/// Как это работает: | |
/// | |
/// 1. Унаследовать контроллер от StatexController | |
/// 2. Отнаследоваться нужным виджетом от StatexWidget | |
/// 3. Вызвать конструктор базового типа и передать туда фабрику | |
/// построения контроллера или именованный конструктор `super.find` | |
/// если где-то во внешнем инжекторе присутствует фабрика данного | |
/// контроллера | |
/// 4. Реализовать [StatexWidget.buildWidget] вместо [Widget.build] | |
/// | |
/// ``` | |
/// [1] | |
/// | |
/// class _HomeSubPageController extends StatexController {} | |
/// | |
/// [2] | |
/// class HomeSubPage extends StatexWidget<_HomeSubPageController> { | |
/// [3.a] | |
// HomeSubPage({Key? key}) : super(() => _HomeSubPageController(), key: key); | |
/// [3.b] | |
// HomeSubPage({Key? key}) : super.find(); | |
// | |
/// [4] | |
// @override | |
// Widget buildWidget(BuildContext context) => Container(); | |
// } | |
/// ``` | |
/// | |
/// Этого достаточно чтобы методы контроллера `onInit/onReady/onClose` стали | |
/// вызываться вовремя. | |
/// | |
/// Если хочется отлавливать моменты инициализации и удаления виджета, | |
/// необходимо переопределить методы [StatexController.onWidgetInitState] | |
/// [StatexController.onWidgetDisposed], которые вызываются | |
/// в моменты [State.initState], [State.disposed]. | |
/// | |
/// Дополнительные параметры. | |
/// Можно передавать [StatexWidget.tag] & [StatexWidget.permanent] в конструкторе, | |
/// тем самым настраивая стандартное поведение, как в Getx. | |
/// | |
/// ``` | |
/// /// Контроллер будет перманентным | |
/// class HomePage extends StatexWidget<_HomePageController> { | |
/// HomePage({Key? key}) | |
/// : super( | |
/// () => _HomePageController(), | |
/// permanent: true, | |
/// key: key, | |
/// ); | |
/// | |
/// /// Контроллер с тегом | |
/// class BusinessPage extends StatexWidget<_BusinessPageController> { | |
// BusinessPage() : super(() => _BusinessPageController(), tag: '_\$myTag'); | |
/// | |
/// /// Просто настройка свойств контроллера при каждой имплементации. | |
/// class SubRouterDialog extends StatexWidget<_SubRouterDialogController> { | |
// SubRouterDialog(this.title, | |
// {Function()? okCallback, | |
// Function()? cancelCallback, | |
// Color? color, | |
// Key? key}) | |
// : super( | |
// () => _SubRouterDialogController( | |
// okCallback: okCallback, | |
// cancelCallback: cancelCallback, | |
// color: color, | |
// ), | |
// ); | |
/// | |
/// | |
/// ``` | |
/// | |
/// Создается и убивается вручную в [onInit] & [dispose] | |
/// контрольного [_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] 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: [ | |
Wrap( | |
children: [ | |
// Враппинг уменьшает геометрию [_StatexWidget] | |
_StatexWidgetInjector<T>( | |
this, | |
builder, | |
tag: tag, | |
permanent: permanent, | |
args: args, | |
), | |
], | |
), | |
buildWidget(context), | |
], | |
); | |
} | |
} | |
/// Контрольный [StatefulWidget] | |
class _StatexWidgetInjector<T extends StatexController> extends StatefulWidget { | |
_StatexWidgetInjector( | |
StatelessWidget parent, | |
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.widget = parent; | |
c.args = args; | |
} | |
final String? tag; | |
final bool permanent; | |
@override | |
_StatexWidgetInjectorState<T> createState() => | |
_StatexWidgetInjectorState<T>(); | |
} | |
/// Состояние для [_StatexWidgetInjector] | |
/// Управляет вызовами [onWidgetDisposed], [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(); | |
} | |
/// Специальный класс контроллера для [StatexWidget]. | |
/// По умолчанию достаточно просто отнаследоваться от него | |
/// и больше ничего не делать. | |
/// В случае необходимости можно реализовать методы | |
/// [onWidgetInitState], [onWidgetDisposed], | |
/// которые вызовутся в нужное время. | |
abstract class StatexController<W extends StatelessWidget> | |
extends GetxController { | |
final subscriptions = rx.CompositeSubscription(); | |
final _args = <String, dynamic>{}; | |
Map<String, dynamic> get args => _args; | |
set args(Map<String, dynamic> value) => _args.assignAll(value); | |
late W widget; | |
/// Вызывается в момент [_StatexWidgetInjectorState.initState]. | |
/// Таким образом можно отлавливать момент перехода на страницы | |
/// в [PageView], например | |
void onWidgetInitState() {} | |
/// Вызывается в момент [_StatexWidgetInjectorState.dispose]. | |
void onWidgetDisposed() {} | |
@mustCallSuper | |
@override | |
void onClose() { | |
subscriptions.clear(); | |
super.onClose(); | |
} | |
} | |
//~ StatexWidget |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment