screen_manager 1.1.0 copy "screen_manager: ^1.1.0" to clipboard
screen_manager: ^1.1.0 copied to clipboard

Padronizando as dependências e controladores de UI utilizando os recursos do Flutter.

ScreenManager #

Buy Me A Coffee

Por que? #

Sempre que falamos em injeções de dependências, separação de regra da UI, vamos diretamente em plugins que nos facilitam isso, porém sempre fiquei com pé atrás em ficar utilizando plugins terceiros que nos ajudam a realizar tais tarefas, onde com o próprio Flutter tem recursos para realizarmos tais tarefas. Então pensei em criar um plugin utilizando os recursos do Flutter para ajudar a realizar as injeções e separações de forma simples.

Observações #

Antes de utilizar esse plugin, saiba que ele está em fase inicial, isso significa que não possuí muitos recursos, basicamente o que desenvolvi nele nada mais é que um padrão para seu projeto, e o que ele vem para ajudar basicamente foi feito conforme eu necessitava em meus projetos iniciais, então dependendo do que você esteja procurando, pode ser que ainda o ScreenManager não tenha essa funcionalidade, mas nada impede desse plugin futuramente crescer ainda mais para ser utilizados para várias situações do dia a dia do desenvolvedor.

Pontos a entender #

O ScreenManager possuí uma estrutura criado por mim mesmo no momento de criar suas UI do aplicativo, vamos entender essa estrutura, lembrando que você não precisa seguir essa estrutura de certa forma, fica a seu critério o melhor jeito, vamos lá.

 - lib
   - home
     - controller
     - injection
     - view
     - widgets || components
  • controller: Aqui ficará o controlador da sua view, suas regras de negócio, atualização de estado entre outros.
  • injection: Aqui ficará suas dependências que você deseja que sua tela tenha acesso como, data sources, repository, use case entre outros.
  • view: Aqui ficará sua view, onde você cria a ponte entre as injeções e sua tela, também ficará a junção de seus widgets/components.
  • widgets || components: Caso você tenha o mesmo gosto que eu, quando tem uma AppBar, FloatAction em minha View, costumo separar esses widgets ou components em arquivos separados, deixando as coisas mais desacopladas, aqui ficará todos essas partes da sua UI.

Controller #

Agora vamos entender a Controller, antes que você pergunte, sim algumas coisas eu fiz baseado no plugin GetX, para criar uma controller é muito simples, basta seguir o código abaixo:

 class HomeController extends ScreenController {}

Apenas isso que você precisa para criar sua Controller, ela também possuí alguns métodos que são possíveis realizar sobrecarga, segue exemplo:

 class HomeController extends ScreenController {
   @override
   void onInit() {
    super.onInit();
   }
   
   @override
   void onReady() {
    super.onReady();
   }
   
   @override
   void onClose() {
    super.onClose();
   }
   
   @override
   void onDependencies() {
    super.onDependencies();
   }
 }
  • onInit: Esse método é executado no initState do StatefulWidget.
  • onReady: Esse método é executado após o build do StatefulWidget.
  • onClose: Esse método é executado no dispose do StatefulWidget.
  • onDependencies: Esse método é executado no didChangeDependencies do StatefulWidget.
  • refresh: Esse método chama o setState do seu StatefulWidget.

Injection #

O Injection é a parte que você cria as instâncias de objetos que você deseja acessar posteriormente em sua UI, segue exemplo de criação:

 class HomeInjection extends ScreenInjection<HomeController> {
  HomeInjection({
   Key? key,
   required ScreenBridge child
  }) : super(
    key: key,
    child: child,
    controller: HomeController()
  );
 }

Como pode notar, na Injection também colocamos qual é o controller responsável, para que a classe ScreenBridge, que no caso é a nossa ponte entre as injeções e a UI possa fazer seu trabalho de realizar as injeções da controller na UI. Na injection também é possível no super do construtor definir mais um argumento que se chama receiveArgs, segue um exemplo, esse será explicado posteriormente.

 receiveArgs: const ScreenReceiveArgs(receive: true, identity: "homeview")

Dependências globais #

Após repensar em alguns pontos, pensei em um ponto onde seria interessante o desenvolvedor injetar dependências globais na aplicação. Nesse caso imagine que você tenha uma instância de um objeto que contém a conexão com um sqlite, nisso você necessita em vários locais da aplicação criar novas instâncias de datasources que dependam dessa conexão, nesse caso não seria interessante ficar criando várias instâncias, então nisso agora nas Injections você têm um método chamado dependencies, onde você pode sobescrever esse método e instanciar suas variáveis com essa dependência. Bom, vamos para o exemplo. Priemeiramente crie um InheritedWidget que irá conter suas dependências globais, no meu caso vou colocar uma classe fictícia, segue código:

class GlobalDependencies extends InheritedWidget {
  final ConnectionSqlite connection = ConnectionSqlite();

  const GlobalDependencies({
    super.key,
    required super.child
  });

  static GlobalDependencies of(BuildContext context) {
    final result = context.dependOnInheritedWidgetOfExactType<GlobalDependencies>();
    assert(result != null, "No injection found on context");
    return result!;
  }

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}

Agora no seu main.dart coloque esse InheritedWidget como pai de todos:

runApp(
  GlobalDependencies(
    child: MaterialApp(
      initialRoute: "/",
      routes: {
        FirstPage.firstPageRoute: (context) => const FirstPage(),
        SecondPage.secondPageRoute:(context) => const SecondPage()
      },
    )
  )
);

Agora vamos pegar como base o HomeInjection, nessa classe sobrescreva o método dependencies, você irá ver que esse método depende de um contexto, esse método será chamado no momento em que o Injection for instanciado. Com esse context você pode chamar o GlobalDependencies e buscar as dependências que precisa para instanciar seus novos objetos. Vamos supor que nesse meu Injection eu vou instanciar um novo DataSource que depende da conexão.

@override
void dependencies(BuildContext? context) {
    homeDataSource = HomeDataSource(
      connection: GlobalDependencies.of(context!).connection
    );
}

Pronto, assim você irá conseguir criar suas novas instâncias em qualquer Injection que dependa por exemplo da conexão.

Por fim você deve-se lembrar que caso você vai utilizar essa abordagem, deverá colocar um late em suas variáveis e na hora de criar sua instância do Injection deverá passar o contexto, segue o exemplo:

@override
FirstPageInjection build(BuildContext context) {
  return FirstPageInjection(
    context: context,
    child: const ScreenBridge<FirstPageController, FirstPageInjection>(
      child: FirstPageView(),
    )
  );
}

Como buscar as dependências #

Para buscar uma dependência do Injection é muito simples, vamos pegar o exemplo anterior, e vamos buscar a dependência na Controller, lembrando que essa dependência também pode ser buscada no Widget ou na View.

class HomeInjection extends ScreenInjection<HomeController> {
  final user = Usuario();

  HomeInjection({
   Key? key,
   required ScreenBridge child
  }) : super(
    key: key,
    child: child,
    controller: HomeController()
  );
 }

Como você pode notar criei uma variável user que é um novo Usuario, vamos supor que você deseja buscar essa instância em sua Controller, você pode fazer dessa forma utilizando a classe ScreenInjection. Lembrando que no caso da Controller, essas dependências devem ser buscadas no método onInit, onReady ou onDependencies, onde de fato o contexto já foi criado.

class HomeController extends ScreenController {
 late final Usuario user;
 
 @override
 void onInit() {
  super.onInit();
  
  user = ScreenInjection.of<HomeInjection>(context).user;
 }
}

Basicamente você irá passar no método of qual Injection ele deverá buscar e qual informação você deseja pegar, pronto nisso você terá em sua Controller a instância do seu Usuario.

Você também pode ter notado que na Injection contém o seguinte método:

@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;

Esse método vem do InheritedWidget, caso você queira que sua View seja atualizada por alguma mudança nos atributos de sua injection, você pode implementar esse método para isso, para entender mais procure sobre InheritedWidget e veja como fazer.

View #

Nesse momento será a hora de criar sua View, aqui conterá a classe ScreenBridge, sua Injection e sua View, segue exemplo:

  class HomeBridge extends Screen {
    const HomeBridge({Key? key}) : super(key: key);
    
    @override
    HomeInjection build(BuildContext context) {
      return HomeInjection(
        child: const ScreenBridge<HomeController, HomeInjection>(
          child: HomeView(),
        )
      );
    }
  }
  
  class HomeView extends ScreenView<HomeController> {
    const HomeView({Key? key}) : super(key: key);
    
    @override
    Scaffold build(BuildContext context) {
      return const Scaffold();
    }
  }

Como você pode ver, na parte da Bridge você primeiramente retorna sua Injection, nisso você retorna o ScreenBridge onde é a ponte, indicando para a mesma qual é a controller e a injection que ele deverá utilizar para realizar as injeções e criações necessárias, nisso o filho da ponte será de fato sua view, essa ponte é extremamente necessária estar configurada corretamente, pois se não a criação da view não será possível ser feito, e erros podem acontecer.

Na parte da View apenas é necessário fazer a sobrecarga do método build e retornar um Scaffold, toda View será necessário ter um Scaffold para ser possível realizar as operações como BottomSheet entre outros widgets que necessita de um Scaffold.

Um ponto importante, toda View nesse caso é um StatefulWidget.

Widgets #

Caso você queira criar Widgets para sua tela, onde eles também teram acesso ao controller da UI, lembrando que esses widgets são feitos exclusivamente para a UI, por motivos que ele compartilha o mesmo controller.

 class FabWidget extends ScreenWidget<HomeController> {
   @override
   void onInit() {
    super.onInit();
    
    print("INIT");
   }
   
   @override
   void onReady() {
    print("READY");
   }
 
   @override
   Widget build(BuildContext context) {
     super.build(context);
     
     return FloatActionButton(
       onPressed: controller.add,
       child: const Icon(Icons.add)
     );
   }
 }

Também possuí alguns métodos de sobrecarga:

  • onInit: Esse método é executado antes de retornar o Widget em si.
  • onReady: Após o build do Widget o mesmo é chamado.

Um ponto importante, todo Widget que extends ScreenWidget é um StatelesWidget.

Disparando mensagens entre Views (ScreenReceiveArgs) #

Lembra daquele argumento que eu falei que você poderia adicionar a mais no super do Injection? Aí está o ponto, aquele argumento serve para identificar se sua View estará visível para receber chamadas, segue novamente um exemplo mostrando. Então primeiramente em sua Injection:

class HomeInjection extends ScreenInjection<HomeController> {
  HomeInjection({
    Key? key,
    required ScreenBridge child
  }) : super(
    key: key,
    child: child,
    controller: HomeController(),
    receiveArgs: const ScreenReceiveArgs(receive: true, identity: "homeview")
  );

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}

Como você pode ver, na Injection da Home, definimos que ela receberá chamadas, passando como true no argumento receive e a identificação que será utilizada para chamar essa View. Após isso em nossa controller implementaremos um método chamado receive, ou um nome que você achar melhor, segue exemplo:

void receive(String message, dynamic value, {ScreenReceive? screen}) {
  switch (message) {
    case "new_people":
      peoples.add(value);
      break;
    case "update_people":
      int position = peoples.indexWhere((people) => people.id == value.id);
      peoples[position] = value;
  }

  refresh();
}

e na sua View, irá fazer uma sobrecarga no método receive:

@override
void receive(String message, value, {ScreenReceive? screen}) {
  controller.receive(message, value);
}

O método receive tem 3 parâmetros:

  • message: Qual a mensagem da chamada, no nosso exemplo tem a mensagem 'new_people', 'update_people', nesse caso baseado na mensagem você irá disparar funções diferentes.
  • value: No caso se você deseja enviar um valor para View, você irá passar nessa informação, no nosso exemplo estou passando uma entidade do tipo People.
  • screen: No caso, se você desejar passar a instância da View que está realizando a chamada, e criar uma lógica do tipo, caso essa tela esteja em atualização, nenhuma chamada da View x será aceito, você também pode utilizar dessa forma.

Para realizar a chamada é bem simples, você usará a classe ScreenMediator para isso, segue exemplo:

ScreenMediator.callScreen("homeview", "new_people", people);

Lembrando que, para realizar essa chamada, na Injection novamente, deve-se estar configurado para a View receber essas chamadas, cuidado também com a identificação, se a mesma estiver errada, um erro será disparado.

Caso queira ver alguns exemplos, nesse repositório contém o exemplo que criei utilizando os padrões do plugin, também em meu repositório existe o projeto EasyNote, onde eu fiz esse projeto utilizando os padrões desse plugin.

👨 Dev #


Daniel Melonari
🚀

Done with ❤️ by Daniel Melonari 👋🏽 Contact!

Linkedin Badge Gmail Badge

0
likes
120
pub points
10%
popularity

Publisher

unverified uploader

Padronizando as dependências e controladores de UI utilizando os recursos do Flutter.

Repository (GitHub)
View/report issues

Documentation

API reference

License

Apache-2.0 (LICENSE)

Dependencies

build_runner, flutter, mockito

More

Packages that depend on screen_manager