Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New Modular 6 #829

Merged
merged 22 commits into from
Aug 1, 2023
Merged

feat: New Modular 6 #829

merged 22 commits into from
Aug 1, 2023

Conversation

jacobaraujo7
Copy link
Contributor

@jacobaraujo7 jacobaraujo7 commented Jan 26, 2023

Descrição

Esse PR se trata das alterações feitas no modular_core, flutter_modular e shelf_modular.

Inicialização no flutter_modular

Melhoramos a forma de iniciar o Modular no widget principal:

 return MaterialApp.router(
      routerConfig: Modular.routerConfig,
    );

Mudança no sistema de Injeção

A principal mudança será a substituição do modular_core pelo AutoInjector como sistema de injeção de dependência, melhorando a sintaxe de registro:

Modular 5<

List<Bind> get binds [
     Bind.factory((i) => Datasource1()),
     Bind.factory((i) => Datasource2()),
     Bind.singleton((i) => Repository(i(), i())),
];

Nova sintaxe de registro Modular 6>

List<Bind> get binds [
     AutoBind.factory(Datasource1.new),
     AutoBind.factory(Datasource2.new),
     AutoBind.singleton(Repository.new),
];

Nessa nova versão será introduzido o AutoBind que permite a nova sintaxe de registro dos binds.
A ideia inicial era substituir a sintaxe atual do Bind, porém estamos decidindo se iremos manter
os dois conceitos (Bind e AutoBind).

O novo sistema de injeção parece está trabalhando bem em demandas mais altas, porém alguns testes adicionais
ainda precisam ser realizados comprovar isso. Esses testes estão sendo feito no repositório do AutoInjector.

Adicionar Módulo em tempo de execução (Injeção)

Também estamos adicionando dois métodos para adição e remoção de módulo fora da árvore do Flutter:
Modular.bindModule e Modular.unbindModule.
Essa forma coloca módulos fora da árvore do Flutter, não participando dos eventos de dispose solicitado pelo ciclo de vida do ModularApp. Por tanto, essa prática deve ser usada com cuidado e o desenvolvedor deve ter a consciência de remover os módulos quando não precisar mais. Essa feature será útil para aplicações que precisão iniciar o sistema de injeção antes mesmo do próprio Flutter.
Colocaremos warnings na documentação para evitar incongruências no código.

Remoção do AsyncBinds

Como teremos mais liberdade com os Binds, os AsyncBinds passam a ser desnecessários, visto que um módulo pode ser resolvido e adicionado ao sistema de injeção fora da inicialização comum no ModularApp.
Percebemos que o sistema do modular funciona melhor de forma síncrona, e vamos colocar na documentação uma forma melhor de trabalhar com dados que precisam ser resolvidos assincronamente.

Adições de classes e métodos:

  • AutoBind -> Nova forma de registrar Binds sem a necessidade de pré-declaração de dependências (i);
  • Modular.tryGet -> retorna null se não encontrar alguma referência/instância.
  • Modular.bindModule -> adiciona um módulo em tempo de execução. (Esse módulo não participa da sessão de "dispose automático").
  • Modular.unbindModule -> remove um módulo;
  • Modular.routerConfig -> iniciar Navigator 2.0;

As correções de issues serão listadas a baixo.

Checklist

  • The title of my PR starts with a Conventional Commit prefix (fix:, feat:, docs: etc).
  • I have read the Contributor Guide and followed the process outlined for submitting PRs.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples.

Breaking Change

  • Yes, this is a breaking change.

  • No, this is not a breaking change.

  • Remove AsyncBind class.

  • Remove constructors Bind() e Bind.scoped.

  • Remove method Modular.isModuleReady, Modular.getAsync, Modular.reassemble, Modular.releaseScopedBinds;

Related Issues

#649

@codecov
Copy link

codecov bot commented Jan 26, 2023

Codecov Report

Merging #829 (fc15bc8) into master (41907d2) will not change coverage.
Report is 7 commits behind head on master.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##            master      #829    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            8        38    +30     
  Lines          114       920   +806     
==========================================
+ Hits           114       920   +806     
Files Changed Coverage Δ
deprecated/modular_test/lib/modular_test.dart 100.00% <100.00%> (ø)

... and 40 files with indirect coverage changes

@AlvaroVasconcelos
Copy link
Contributor

@jacobaraujo7

Acho interessante fazer um breaking change mesmo no Bind para não precisar criar um AutoBind, como alternativa, ter apenas o Bind padrão.

@toshiossada
Copy link
Contributor

Eu manteria os dois, pois da nova forma eu nao conseguiria registrar um método estático no Bind (tecnica utilizada para desacoplar os Packages com métodos estático), obviamente nao precisaria de todos os ciclos de vida (lazySingleton, Singleton, etc) apenas o factory entao poderia colocar os demais como Deprecated, para nao causar breaking changes inesperados, e dpois de 2 versões retirar eles, mas mantendo o factory (ou chamar de outoro nome)
Ex.:

image

@fredfv
Copy link

fredfv commented Jan 26, 2023

Bind.factory((i) => const HiveServiceFactory(path: 'database'))

No modelo novo eu conseguiria ter um construtor passando uma constante como parâmetro?

@toshiossada
Copy link
Contributor

outra coisa, ja que tem o Modular.bindModule podia ter o Modular.bind para a adiçao fora da árvore do Flutter

@jacobaraujo7
Copy link
Contributor Author

Bind.factory((i) => const HiveServiceFactory(path: 'database'))

No modelo novo eu conseguiria ter um construtor passando uma constante como parâmetro?

Na nova versão vc pode usar função anonima sem o (i)

AutoBind.factory<HiveServiceFactory>(() => const HiveServiceFactory(path: 'database'));

@jacobaraujo7
Copy link
Contributor Author

Eu manteria os dois, pois da nova forma eu nao conseguiria registrar um método estático no Bind (tecnica utilizada para desacoplar os Packages com métodos estático), obviamente nao precisaria de todos os ciclos de vida (lazySingleton, Singleton, etc) apenas o factory entao poderia colocar os demais como Deprecated, para nao causar breaking changes inesperados, e dpois de 2 versões retirar eles, mas mantendo o factory (ou chamar de outoro nome) Ex.:

image

Dá pra fazer a mesma coisa:

AutoBind.factory<FShowDialog>(() ⇒ (Widget child) async {
        return await Asuka.showDialog(builder: (context) ⇒ child);
}),
AutoBind.factory<FAlert>(() ⇒ (String text) {
        AsukaSnackbar.alert("success").show();
}),
AutoBind.factory<IDialogAdapter>(() ⇒ AsukaDialog(
          fShowDialog: i<FShowDialog>(),
          fAlert: i<FAlert>(),
)),

O Class.new cria uma função, () => Class();

@jacobaraujo7
Copy link
Contributor Author

outra coisa, ja que tem o Modular.bindModule podia ter o Modular.bind para a adiçao fora da árvore do Flutter

O escopo de registro é feito por módulo. Sem esse limite pode haver incongruência de dados, visto que o Modular lida com contexto de módulo e não registro 1 a 1.

@jacobaraujo7
Copy link
Contributor Author

Acredito que substituir totalmente o Bind pelo AutoBind seja a forma mais correta, pois é uma melhoria de como sempre deveria ser. Manter a forma antiga fará com que as pessoas não se atualizem

@raphaelbarbosaqwerty
Copy link
Contributor

Acredito que substituir totalmente o Bind pelo AutoBind seja a forma mais correta, pois é uma melhoria de como sempre deveria ser. Manter a forma antiga fará com que as pessoas não se atualizem

Mas aí vai quebrar de cara os projetos ou vai por como deprecated?

@jacobaraujo7
Copy link
Contributor Author

Acredito que substituir totalmente o Bind pelo AutoBind seja a forma mais correta, pois é uma melhoria de como sempre deveria ser. Manter a forma antiga fará com que as pessoas não se atualizem

Mas aí vai quebrar de cara os projetos ou vai por como deprecated?

Só dá pra colocar @deprecated quando o método vai ter um substituto. Ali é refatoração, assim como foi no BLoC 7 pro 8

@toshiossada
Copy link
Contributor

Eu manteria os dois, pois da nova forma eu nao conseguiria registrar um método estático no Bind (tecnica utilizada para desacoplar os Packages com métodos estático), obviamente nao precisaria de todos os ciclos de vida (lazySingleton, Singleton, etc) apenas o factory entao poderia colocar os demais como Deprecated, para nao causar breaking changes inesperados, e dpois de 2 versões retirar eles, mas mantendo o factory (ou chamar de outoro nome) Ex.:
image

Dá pra fazer a mesma coisa:

AutoBind.factory<FShowDialog>(() ⇒ (Widget child) async {
        return await Asuka.showDialog(builder: (context) ⇒ child);
}),
AutoBind.factory<FAlert>(() ⇒ (String text) {
        AsukaSnackbar.alert("success").show();
}),
AutoBind.factory<IDialogAdapter>(() ⇒ AsukaDialog(
          fShowDialog: i<FShowDialog>(),
          fAlert: i<FAlert>(),
)),

O Class.new cria uma função, () => Class();

dai troca o fAlert: i<FAlert>(), por fAlert: Modular.get<FAlert>(),

@jacobaraujo7
Copy link
Contributor Author

Eu manteria os dois, pois da nova forma eu nao conseguiria registrar um método estático no Bind (tecnica utilizada para desacoplar os Packages com métodos estático), obviamente nao precisaria de todos os ciclos de vida (lazySingleton, Singleton, etc) apenas o factory entao poderia colocar os demais como Deprecated, para nao causar breaking changes inesperados, e dpois de 2 versões retirar eles, mas mantendo o factory (ou chamar de outoro nome) Ex.:
image

Dá pra fazer a mesma coisa:

AutoBind.factory<FShowDialog>(() ⇒ (Widget child) async {
        return await Asuka.showDialog(builder: (context) ⇒ child);
}),
AutoBind.factory<FAlert>(() ⇒ (String text) {
        AsukaSnackbar.alert("success").show();
}),
AutoBind.factory<IDialogAdapter>(() ⇒ AsukaDialog(
          fShowDialog: i<FShowDialog>(),
          fAlert: i<FAlert>(),
)),

O Class.new cria uma função, () => Class();

dai troca o fAlert: i<FAlert>(), por fAlert: Modular.get<FAlert>(),

Na verdade se vc colocar FAlert.new já vai funcionar.
Vou corrigir aquele código

AutoBind.factory<FShowDialog>(() ⇒ (Widget child) async {
        return await Asuka.showDialog(builder: (context) ⇒ child);
}),
AutoBind.factory<FAlert>(() ⇒ (String text) {
        AsukaSnackbar.alert("success").show();
}),
AutoBind.factory<IDialogAdapter>(AsukaDialog.new),

@toshiossada
Copy link
Contributor

toshiossada commented Jan 31, 2023

Uma feature muito util que poderia ter no modular que já tem no GetIt, Getx e Provider é a possibilidade de recuperar um bind por InstanceName
Imagine a Situaçao onde tenho 2 packages onde tenho um componente que depende de uma abstração e as implementações precisam ficar no BaseApp (cenário comum no server driven UI)
image

Com o Getit conseguimos criar varias implementações e injetar no componente olhando pelo instanceName e nao pelo tipo

getIt.registerFactory<BaseAction>(
  () => Action1(),
  instanceName: 'Action1',
);

getIt.registerFactory<BaseAction>(
  () => Action2(),
  instanceName: 'Action2',
);

getIt.registerFactory<BaseAction>(
  () => Action3(),
  instanceName: 'Action3',
);

getIt.registerFactory(
  () => Component(
	getIt.get(action: 'Action2'),
  ),
);

class Component{
	final BaseAction action;
	Component({required this.action});
	
}

Minha sugestão é habilitar essa possibilidade no modular e caso o dev necessite fazer isso na hora de recuperar a instancia utilize da forma antiga passando o lambda

List<Bind> get binds [
	Bind.instance<BaseAction>(Action1.new, instanceName = 'Action1'),
	Bind.instance<BaseAction>(Action2.new, instanceName = 'Action2'),
	Bind.instance<BaseAction>(Action3.new, instanceName = 'Action3'),	
    Bind.factory(() => Component(action: Modular.get<BaseAction>(instanceName = 'Action2'))),
];

class Component{
	final BaseAction action;
	Component({required this.action});
	
}

Outra possibilidade e caso exista uma das instancias sem o intanceName e o dev registra o componente com o Component.new ele pegue o que esta sem o instanceName
Nesse caso ira pegar o Action3

List<Bind> get binds [
	Bind.instance<BaseAction>(Action1.new, instanceName = 'Action1'),
	Bind.instance<BaseAction>(Action2.new, instanceName = 'Action2'),
	Bind.instance<BaseAction>(Action3.new),	
    Bind.factory(Component.new),
];

class Component{
	final BaseAction action;
	Component({required this.action});
	
}

E caso o Dev nao defina nenhum Bind sem o instanceName e utilize do Component.new pegue o primeiro bind aleatoriamente.
Nesse caso vai pegar o primeiro Action1

List<Bind> get binds [
	Bind.instance<BaseAction>(Action1.new, instanceName = 'Action1'),
	Bind.instance<BaseAction>(Action2.new, instanceName = 'Action2'),
	Bind.instance<BaseAction>(Action3.new, instanceName = 'Action3'),	
    Bind.factory(Component.new),
];

class Component{
	final BaseAction action;
	Component({required this.action});
	
}

@jacobaraujo7
Copy link
Contributor Author

@jacobaraujo7
Copy link
Contributor Author

Temos duas decisões a tomar:
Sobre o break change nos Binds ou usar o AutoBind.
Sobre o que o @toshiossada sugeriu

@eduardoflorence
Copy link

eduardoflorence commented Feb 2, 2023

Olá @jacobaraujo7,

Aproveitando que teremos uma alteração na "major version" do Modular e, consequentemente, possíveis breaking changes, gostaria de sugerir uma alteração no WidgetModule, pois atualmente ele possui um problema que temos que contornar.

class LocalModule extends WidgetModule{
  @override
  List<Bind> get binds => [
    Bind.singleton((i) => MySpecialController())
  ];

  @override
  Widget get view => MyWidget(Modular.get<MySpecialController>());
}

O exemplo acima (parecido com o da documentação) lança um erro, pois MyWidget é instanciado de forma imediata, antes mesmo do Bind acontecer e não conseguimos passar o controller pelo constructor.
Isso seria resolvido se o getter view esperasse uma função que retorna Widget e não diretamente o Widget (breaking change). Exemplo:

Widget Function() get view => () => MyWidget(Modular.get<MySpecialController>());

Atualmente para contornar, eu crio um widget "wrap" dessa forma:

class WidgetModuleView extends StatelessWidget {
  const WidgetModuleView({super.key, required this.child});

  final Widget Function() child;

  @override
  Widget build(BuildContext context) => child();
}

E uso assim:

Widget get view => WidgetModuleView(child: () => MyWidget(Modular.get<MySpecialController>()));

O que acha?

@jacobaraujo7
Copy link
Contributor Author

Bind.instance(Action1.new,

Perfeito.
Vou adicionar isso

@AlvaroVasconcelos
Copy link
Contributor

Temos duas decisões a tomar:

Sobre o break change nos Binds ou usar o AutoBind.

Sobre o que o @toshiossada sugeriu

break change nos Binds mesmo.

@jacobaraujo7
Copy link
Contributor Author

Um sugestão que me ocorreu aqui.
E se a gente criasse um OldBind pra representar a maneira antiga e facilitar a integração?

@jacobaraujo7
Copy link
Contributor Author

jacobaraujo7 commented Feb 3, 2023

Olá @jacobaraujo7,

Aproveitando que teremos uma alteração na "major version" do Modular e, consequentemente, possíveis breaking changes, gostaria de sugerir uma alteração no WidgetModule, pois atualmente ele possui um problema que temos que contornar.

class LocalModule extends WidgetModule{
  @override
  List<Bind> get binds => [
    Bind.singleton((i) => MySpecialController())
  ];

  @override
  Widget get view => MyWidget(Modular.get<MySpecialController>());
}

O exemplo acima (parecido com o da documentação) lança um erro, pois MyWidget é instanciado de forma imediata, antes mesmo do Bind acontecer e não conseguimos passar o controller pelo constructor. Isso seria resolvido se o getter view esperasse uma função que retorna Widget e não diretamente o Widget (breaking change). Exemplo:

Widget Function() get view => () => MyWidget(Modular.get<MySpecialController>());

Atualmente para contornar, eu crio um widget "wrap" dessa forma:

class WidgetModuleView extends StatelessWidget {
  const WidgetModuleView({super.key, required this.child});

  final Widget Function() child;

  @override
  Widget build(BuildContext context) => child();
}

E uso assim:

Widget get view => WidgetModuleView(child: () => MyWidget(Modular.get<MySpecialController>()));

O que acha?

Estou trabalhando em tornar o WidgetModule um Widget puro e assim poderemos chegar em uma sintaxe melhor.
image

Com esse código podemos Chegar a uma sintaxe assim:

class MyWidgetModule extends WidgetModule {

    @override
    List<Bind> get binds => []
    
    @override
    Widget build(BuildContext context){
         return Container();
    }

}

Assim acredito que fica com mais cada Widget.
Me diz o que acha disso @eduardoflorence

@eduardoflorence
Copy link

Com esse código podemos Chegar a uma sintaxe assim:

class MyWidgetModule extends WidgetModule {

    @override
    List<Bind> get binds => []
    
    @override
    Widget build(BuildContext context){
         return Container();
    }

}

Assim acredito que fica com mais cada Widget. Me diz o que acha disso @eduardoflorence

Assim vai ficar bem melhor @jacobaraujo7. Dentro do método build poderemos passar o Bind via constructor para o widget que é responsável pela view, certo? Exemplo:

class MyWidgetModule extends WidgetModule {

    @override
    List<Bind> get binds => [
        Bind.singleton(MyController.new)
    ];
    
    @override
    Widget build(BuildContext context){
        return HomePage(Modular.get<MyController>());
    }

}

@tspoke
Copy link

tspoke commented May 16, 2023

Hi, any ETA on this PR ? I'm still stuck with the v4.5.* and flutter 2 (because modular v5 has too many DI problems). I'm still hoping this version will allow me to migrate to flutter 3 soon...

Thanks for your work

@arthurbcd
Copy link

Estou testando o release do Modular 6.0.0-beta.1.

O novo WidgetModule ainda está com o problema do Bind ser chamado antes de ser injetado.

Fiz um ajuste para que isso não aconteça, permitindo o uso de suas dependências imediatamente.

class _ModuleElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  _ModuleElement(PageWidget super.widget);

  @override
  Widget build() {
    final widgetModule = widget as PageWidget;
    final child = () => widgetModule.build(this);

    return _ModularProvider(
      module: _ModuleImpl(
        binds: widgetModule.binds,
        exportedBinds: widgetModule.exportedBinds,
        imports: widgetModule.imports,
      ),
      tag: widgetModule.runtimeType.toString(),
      child: child,
    );
  }

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget, 'widget == newWidget');
    rebuild(force: true);
  }
}

class _ModularProvider extends StatefulWidget {
  final Widget Function() child;
  final Module module;
  final String tag;

  const _ModularProvider({
    super.key,
    required this.child,
    required this.module,
    required this.tag,
  });

  @override
  _ModularProviderState createState() => _ModularProviderState();
}

class _ModularProviderState extends State<_ModularProvider> {
  @override
  void initState() {
    super.initState();
    injector.get<BindModule>().call(widget.module, widget.tag);
  }

  @override
  Widget build(BuildContext context) {
    return widget.child();
  }

  @override
  void dispose() {
    super.dispose();
    injector.get<UnbindModule>().call(type: widget.tag);
  }
}

Com esse ajuste, consigo consumir sem problemas. :).

Essa nova sintaxe está muito boa, abraços à equipe.

@raphaelbarbosaqwerty
Copy link
Contributor

Hey, @tspoke which problems are you getting with past Modular? I'm using the Flutter 3 + Modular 5, without any problems, you are free to reach me, just send me a message.

Hi Team! I think this one #848 can be released together, just to keep it clean on prod.

@arthurbcd
Copy link

arthurbcd commented Jun 14, 2023

Version: ˆ6.0.0-beta.3
RouterOutlet throws the following exception when using the new Modular.routerConfig

      routerConfig: Modular.routerConfig,
      // routerDelegate: Modular.routerDelegate,
      // routeInformationParser: Modular.routeInformationParser,

The exception:
image

Works normally on the old setup:

      // routerConfig: Modular.routerConfig,
      routerDelegate: Modular.routerDelegate,
      routeInformationParser: Modular.routeInformationParser,

@arthurbcd
Copy link

arthurbcd commented Jul 14, 2023

Exception on Flutter Web. App will work normally on debug but on release Modular will not work:

AutoInjectorException: minified:I Class is already added.

Problem:

When running flutter run -d chrome --release, app loading results in a gray screen.
This error occurs on all version of Modular ˆ6.0.0-beta (from beta.1 to beta.5).
This error does not occur on Modular 5.

Solution:

I resolved this error by manually typing all Bind/AutoBind constructors, even when redundant.

import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

void main() {
  runApp(ModularApp(module: AppModule(), child: const AppWidget()));
}

class AppWidget extends StatelessWidget {
  const AppWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: Modular.routerConfig,
      routeInformationParser: Modular.routeInformationParser,
    );
  }
}

class AController {}

class BController {}

class AppModule extends Module {
  @override
  final List<Bind> binds = [
    // AutoBind.singleton(AController.new), <-- this will pass
    // AutoBind.singleton(BController.new), <-- this will throw "AutoInjectorException: Class is already added"
    AutoBind.singleton<AController>(AController.new), // <-- this works
    AutoBind.singleton<BController>(BController.new), // <-- this works
  ];

  @override
  final List<ModularRoute> routes = [
    ChildRoute('/', child: (_, args) => const Page()),
  ];
}

class Page extends StatelessWidget {
  const Page({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text('Hello World!'),
      ),
    );
  }
}

The environment:

name: modular_web
description: A new Flutter project.
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.0.5 <4.0.0'

dependencies:
  flutter_modular: ^6.0.0-beta.5
  flutter:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

@jacobaraujo7 jacobaraujo7 marked this pull request as ready for review July 30, 2023 20:39
@jacobaraujo7 jacobaraujo7 merged commit 2251b70 into master Aug 1, 2023
6 checks passed
@jacobaraujo7 jacobaraujo7 deleted the modular_6 branch August 1, 2023 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.