Injeção de Dependências no Flutter

João Pedro Quintino
Popcodemobile
Published in
6 min readApr 8, 2021

A injeção de dependências é um padrão de projeto que tem como objetivo manter um baixo acoplamento entre módulos de um sistema. Mas, como identifico esse acoplamento no código e como sei que preciso de injeção de dependências? Além disso, como posso implementar a injeção de dependências no Flutter? É isso que tratarei neste artigo.

Acoplamento entre módulos

Diz-se que duas classes estão com alto acoplamento quando uma das classes “conhece” muito da outra. Ex.:

No exemplo acima, a classe BuscaNomeUseCase conhece o construtor concreto BuscaNomesRepository , por isso, as duas classes estão com alto acoplamento, além de uma baixa coesão, pois o método call do BuscaNomeUseCase sabe obter uma lista de nomes e sabe criar um repositório, o que fere o princípio da responsabilidade única.

Por que um alto acoplamento é ruim?

Do jeito que está o código acima, não é possível implementar testes unitários com Mocks, pois o objeto BuscaNomeUseCase cria o repositório, tornando inviável que seja criado um repositório em Mock que retorne valores de teste para o objeto.

Em linhas gerais, um baixo acoplamento traz mais facilidade de manutenção e implementação de novas funcionalidades, além de permitir a utilização de mocks para realizar testes unitários.

Como reduzir o acoplamento?

Uma maneira bastante eficiente de reduzir o acoplamento é implementando a injeção de dependências. Para isso, é imprescindível que seja utilizadas interfaces, a fim de registrá-las como um contrato entre os módulos que precisarão interagir entre si.

No exemplo acima, foi definida uma interface para o repositório de busca, de modo que todo repositório que implementar esta interface, disponibilize uma função de busca que recebe uma String e retorne uma lista.

Dessa forma, basta que o BuscaNomeUseCase receba um repositório de busca, através da interface. Assim, ele não precisará saber qual é o repositório concreto que está sendo passado. Ex.:

Assim, a classe BuscaNomeUseCase não precisa conhecer o repositório concreto e, muito menos, construí-lo.

Essa abordagem faz com que seja possível implementar testes unitários com mocks, bastando passar o mock do repositório no construtor do use case.

Mas ainda há um problema: Imagine que a classe que vai instanciar o BuscaNomeUseCase precisará agora passar também o repositório no construtor. Bem, não é interessante que uma classe que utilize um use case (comumente uma View) saiba, também, criar um use case e um repositório. Injetar as dependências manualmente pelo construtor somente transfere a responsabilidade para a camada acima, fazendo com que a última camada precise instanciar uma classe com uma grande quantidade de parâmetros.

Para resolver esse problema, utiliza-se um Injection Container, que é responsável por instanciar as classes e todas as suas dependências.

Para implementar um Injection Container no Flutter, utilizam-se service locators (localizadores de serviço), como get_it e kiwi. Porém, é necessário registrar as dependências de forma manual. Para registrar as dependências de forma automática, utilizam-se geradores de código para cada package. Para o get_it temos o injectable e para o kiwi temos o kiwi_generator. Ambos permitem usar anotações nas classes para indicar que ela deve ser registrada como uma dependência. Neste artigo, tratarei apenas do get_it, mas a implementação com o kiwi é muito semelhante.

Utilizando o get_it para realizar a injeção de dependências

Após adicionar o get_it às dependências do seu projeto, é hora de criar o seu injection container e registrar as dependências.

O get_it fornece um Singleton através de GetIt.instance (ou, de uma forma mais minimalista, GetIt.I). Para registrar os serviços, basta chamar as funções de acordo com a maneira desejada de registro, sendo possível registrar de três maneiras diferentes:

  • Factory: Nessa maneira, você deve passar uma função factory que retornará sempre uma nova instância da dependência. Ex:
  • Singleton: Nessa maneira, você deve passar uma instância ou classe derivada de uma dependência. Nessa modalidade, sempre será retornada a mesma instância. Como as instâncias do Singleton precisam ser feitas no momento em que são registradas, isso pode acabar consumindo tempo. Ex:
  • Lazy Singleton: Nessa maneira, você deve passar uma função factory que retorna uma instância de uma implementação da dependência, de modo semelhante ao Factory. A diferença é que apenas na primeira vez que essa dependência for chamada, ela criará uma instância, nas próximas vezes será retornada a mesma instância já criada, de forma semelhante ao Singleton. Esta opção é preferida sobre o Singleton, pois a nova instância só será criada na primeira vez que for chamada, evitando consumir muito tempo durante o início do app. Ex:

É possível ainda, passar parâmetros para as funções de registro de factories utilizando a função registerFactoryParam, onde podem ser passados dois parâmetros. Com exceção dos parâmetros, esta função funciona igual à registerFactory.

Para todas as funções de registro de dependências citadas acima, existe uma equivalente assíncrona, que pode ser usada quando a inicialização da dependência é assíncrona (no caso dos Singleton e Lazy Singleton) ou, se, no caso da Factory, ela precisar chamar uma função assíncrona. São elas registerFactoryAsync, registerFactoryParamAsync, registerLazySingletonAsync, registerSingletonAsync. A diferença destas é que elas retornam Future<T> ao invés de T.

Abaixo está um exemplo utilizando as classes BuscaNomeUseCase, BuscaNomesRepository e IBuscaRepository do exemplo anterior, adicionando um componente de lógica de apresentação BuscaNomeCubit que depende de BuscaNomeUseCase.

Depois de criada a função registrarDependencias, basta chamá-la no início do app:

Para cancelar o registro de um serviço, basta chamar a função unregister, conforme o exemplo abaixo:

Com o GetIt, só é possível registrar mais de uma instancia sob o mesmo tipo, se as instancias forem nomeadas, isso é feito passando o parâmetro instanceName e, dessa forma, esta instancia só pode ser acessada passando tipo e nome.

Ao chamar um serviço, serão instanciadas automaticamente todas as suas dependências. Para realizar a chamada, basta utilizar o Singleton fornecido pelo GetIt, utilizando a função get() ou chamando diretamente a instância como uma função.

Para cancelar o registro de instâncias nomeadas, basta passar o nome no unregister:

Automatizando o registro das dependências

À medida que o código cresce, vão sendo criadas cada vez mais dependências e, consequentemente, o trabalho manual de registrar as dependências, aumenta. Para solucionar este problema, foi criado o injectable, que é um gerador de código para o get_it. Deixando de lado os prós e contras desta abordagem, este é um package bastante conveniente para os adeptos da geração de código. Com ele, basta anotar as classes que deverão ser registradas e tudo acontece como num passe de mágica, bastando apenas uma pequena configuração.

Para configurar o injectable, basta anotar a função que registrará as dependências como @injectableInit e chamar a função gerada $initGetIt(), passando a instância do GetIt, conforme exemplo a seguir.

Depois disso, basta chamar a função no início do app normalmente e anotar as classes/objetos que deverão ser registrados, conforme exemplos abaixo:

Depois de anotadas as classes, basta rodar o comando

flutter packages pub run build_runner build

para gerar a função de registro das dependências automaticamente.

Você pode ir ainda além e eliminar a necessidade de anotações nas classes, se o seu projeto seguir rigorosamente alguma convenção de nomes (naming convention) na criação de arquivos ou classes. Nesse caso, basta criar um arquivo build.yaml na raiz do projeto (no mesmo nível do pubspec.yaml) e especificar os arquivos a serem registrados. Por exemplo, você pode avisar ao gerador para registrar automaticamente classes que terminem com DataSource, Repository, Cubit ou UseCase usando um simples padrão de expressão regular class_name_pattern: "DataSource$|Repository$|Cubit$|UseCase" ou, da mesma forma, registrar automaticamente pelo nome do arquivo.

Referências

Inversion of Control Containers and the Dependency Injection pattern — https://martinfowler.com/articles/injection.html

Flutter TDD Clean Architecture Course [13] — Dependency Injection — https://resocoder.com/2019/10/21/flutter-tdd-clean-architecture-course-13-dependency-injection-user-interface/

Injectable — Flutter & Dart Equivalent to Dagger & Angular Dependency Injection — https://resocoder.com/2020/02/04/injectable-flutter-dart-equivalent-to-dagger-angular-dependency-injection/

Princípio da Responsabilidade Única — https://medium.com/@angelomribeiro/princípio-da-responsabilidade-única-6d633087fa4e

Inversão de Controle: Service Locator e Injeção de Dependência — https://imasters.com.br/software/inversao-de-controle-service-locator-e-injecao-de-dependencia

Injeção de Dependência — https://medium.com/@eduardolanfredi/injeção-de-dependência-ff0372a1672

--

--