Skip to content

Instantly share code, notes, and snippets.

@ValeriusGC
Last active May 2, 2022 17:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ValeriusGC/e6b967842be8cab42d0131ba51781cc4 to your computer and use it in GitHub Desktop.
Save ValeriusGC/e6b967842be8cab42d0131ba51781cc4 to your computer and use it in GitHub Desktop.
Расширение возможностей инжектирования контроллеров для GetX
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