Flutter: Injetor de Dependência

Christian Batista
Flutter Brasil
Published in
6 min readMay 19, 2024

Uma vez que sabemos o que é para que serve Injeção de Dependência, vamos agora então falar sobre uma forma de implementar o acesso aos nossos objetos de forma “prática” dentro nossa aplicação.

Muitas vezes precisamos usar um objeto (isto é: uma instância de uma classe) para realizar determinada tarefa dentro de uma função. Mas infelizmente temos como (mal) hábito criar tal objeto ao invés de solicita-lo como parâmetro!

Isto viola a regra de Injeção de Dependência e dificulta a implementação dos testes unitários.

Então o correto é sempre solicitar a instância do objeto desejado ao invés de criá-lo!

É ai que iremos usar o nosso Injetor de Dependência para nos ajudar!

Pense no injetor como um armário, onde cada gaveta tem uma etiqueta na frente dizendo o “tipo” de objeto existente dentro da gaveta!

O que faremos agora é apresentar uma implementação (muito) simples deste “armário”, para que você possa entender como ele funciona e eventualmente criar e usar no futuro suas próprias implementações mais complexas:

class Injector {
static Injector? _instance;
static Injector get instance => _instance ??= Injector._();

Injector._();

final Map<Type, dynamic> _objects = {};

bool contains<T>() => _objects.containsKey(T);

void add<T>(T instance) => contains<T>() ? throw Exception('Class ${T.runtimeType} already registered!') : _objects[T] = instance;

T get<T>() => contains<T>() ? _objects[T] : throw Exception('Class ${T.runtimeType} not registered!');

void remove<T>() => _objects.remove(T);

void clear() => _objects.clear();
}

Nesta implementação temo o nosso Injector como um singleton. Isso nos permitirá acessá-lo de qualquer ponto do nosso projeto em nenhuma burocracia! (É claro que você pode criar sua própria implementação usando o InheritedWidget ou qualquer outro padrão que desejar!)

Como você pode ter notado, ele nada mais é do que um Map<Type, dynamic> que representa nossas “gavetas” com suas “etiquetas” (Type) dizendo quais são os nossos “objetos” (dynamic) contidos nelas. Os métodos que ele possui são simplesmente para manipular/acessar este Map.

Simples, não acha?! Mas como usamos isso na vida real?

Primeiramente vamos escrever algumas classes do nosso projeto, com suas abstrações e respectivas implementações para efeito de ilustração:

(obs: estas classes estão bem simplificadas para efeito de exemplo)

Gateway:

abstract class Gateway {
Future<Response> post(String path, {Object? data});
Future<Response> get(String path, {Object? data});
}
class GatewayDio implements Gateway {
final Dio dio;

GatewayDio(this.dio);

@override
Future<Response> post(String path, {Object? data}) => dio.post(path, data: data);

@override
Future<Response> get(String path, {Object? data}) => dio.get(path, data: data);
}

OrderRepository:

abstract class OrderRepository {
Future<Order?> createOrder(Order order);
Future<List<Order>?> getOrders();
}
class OrderRepositoryImpl extends OrderRepository {
Gateway gateway;

OrderRepositoryImpl(this.gateway);

@override
Future<Order?> createOrder(Order order) async {
try {
var response = await gateway.post("orders", data: order.toJson());
if (response.statusCode != 200) return null;
return Order.fromJson(response.data);
} catch (e) {
return null;
}
}

@override
Future<List<Order>?> getOrders() async {
try {
var response = await gateway.get("orders", data: {});
if (response.statusCode != 200) return null;
return (response.data as List).map((e) => Order.fromJson(e)).toList();
} catch (e) {
return null;
}
}
}

ProductRepository:

abstract class ProductRepository {
Future<Product?> getProduct(String id);
Future<List<Product>?> getProducts();
}
class ProductRepositoryImpl extends ProductRepository {
Gateway gateway;

ProductRepositoryImpl(this.gateway);

@override
Future<Product?> getProduct(String id) async {
try {
var response = await gateway.get("products", data: {"id": id});
if (response.statusCode != 200) return null;
return Product.fromJson(response.data);
} catch (e) {
return null;
}
}

@override
Future<List<Product>?> getProducts() async {
try {
var response = await gateway.get("products", data: {});
if (response.statusCode != 200) return null;
return (response.data as List).map((e) => Product.fromJson(e)).toList();
} catch (e) {
return null;
}
}
}

OrderUseCases:

abstract class CreateOrderUseCase {
Future<Order?> call(Order order);
}
class CreateOrderUseCaseImpl implements CreateOrderUseCase {
OrderRepository orderRepository;

CreateOrderUseCaseImpl(this.orderRepository);

@override
Future<Order?> call(Order order) async {
return orderRepository.createOrder(order);
}
}
abstract class GetOrdersUseCase {
Future<List<Order>?> call();
}
class GetOrdersUseCaseImpl implements GetOrdersUseCase {
OrderRepository orderRepository;

GetOrdersUseCaseImpl(this.orderRepository);

@override
Future<List<Order>?> call() async {
return orderRepository.getOrders();
}
}

ProductUseCases:

abstract class GetProductUseCase {
Future<Product?> call(String id);
}
class GetProductUseCaseImpl implements GetProductUseCase {
ProductRepository productRepository;

GetProductUseCaseImpl(this.productRepository);

@override
Future<Product?> call(String id) async {
return productRepository.getProduct(id);
}
}
abstract class GetProductsUseCase {
Future<List<Product>?> call();
}
class GetProductsUseCaseImpl implements GetProductsUseCase {
ProductRepository productRepository;

GetProductsUseCaseImpl(this.productRepository);

@override
Future<List<Product>?> call() async {
return productRepository.getProducts();
}
}

Como sei que você inteligente e astuto, duas coisas importantes que você deve ter notado:

  • Todos as abstrações NÃO POSSUEM DEPENDÊNCIAS!
  • Todas as implementações POSSUEM PELO MENOS UMA DEPENDÊNCIA de uma classe “anterior” do nosso projeto.

Se fossemos “desenhar” quem depende de quem (do ponto de vista das implementações), teremos:

UseCase => Repository => Gateway => Dio => Flutter

UseCase “depende” de um Repository… que “depende” de um Gateway… que “depende” do Dio… que não depende de nenhuma classe da nossa aplicação!

Então agora vamos por em prática o Injector!

Primeiramente vamos criar uma função “top level” para fazer o “setup” das nossas dependências:

void setUpDependencies() {
Injector injector = Injector.instance;
}

Em seguida vamos adicionar a primeira dependência (sempre de trás para frente).

Logo, a primeira dependência que temos é uma instancia da classe Dio:

void setUpDependencies() {
Injector injector = Injector.instance;

injector.add<Dio>(Dio());
}

Vamos entender o que acabamos de fazer:

injector.add<Dio>(Dio());

Estamos dizendo ao nosso injector para criar (add) uma “gaveta”, colocar a etiqueta <Dio> e guardar dentro dela uma instância do objeto Dio().

Ótimo! Então vamos agora adicionar a próxima dependência: Gateway.

void setUpDependencies() {
Injector injector = Injector.instance;

injector.add<Dio>(Dio());
injector.add<Gateway>(GatewayDio(injector.get<Dio>()));
}

Vamos novamente entender o que acabamos de fazer:

injector.add<Gateway>(GatewayDio(injector.get<Dio>()));

Estamos dizendo ao nosso injector para criar (add) uma nova gaveta, colocar a etiqueta <Gateway> e guarde dentro dela uma instância do objeto GatewayDio().

Porém, para criarmos uma nova instância do objeto GatewayDio, precisamos passar uma instancia de um objeto Dio!

E onde podemos achar um Dio? No próprio injector!!

Veja que ja adicionamos uma instancia do Dio instantes atrás!

Basta pedir então ao injector para procurar uma gaveta com a etiqueta <Dio> e pegar o objeto que está nela!!

É isto que o injector.get<Dio>() faz!

Esta função obtém do injector um objeto a partir do “tipo” que voce especificar na tipagem!

Resumindo: iremos criar novas instâncias dos nosso objetos usando como dependência as instâncias de objetos que já criamos e guardamos no nosso injector!

Logo, basta irmos usando desta mesma forma para os demais objetos:

void setUpDependencies() {
Injector injector = Injector.instance;

// dio
injector.add<Dio>(Dio());

// gateway
injector.add<Gateway>(GatewayDio(injector.get<Dio>()));

// repositories
injector.add<OrderRepository>(OrderRepositoryImpl(injector.get<Gateway>()));
injector.add<ProductRepository>(ProductRepositoryImpl(injector.get<Gateway>()));

// use cases
injector.add<CreateOrderUseCase>(CreateOrderUseCaseImpl(injector.get<OrderRepository>()));
injector.add<GetOrdersUseCase>(GetOrdersUseCaseImpl(injector.get<OrderRepository>()));
injector.add<GetProductUseCase>(GetProductUseCaseImpl(injector.get<ProductRepository>()));
injector.add<GetProductsUseCase>(GetProductsUseCaseImpl(injector.get<ProductRepository>()));
}

Agora basta chamar a função setUpDependencies() no inicio do seu app:

void main() async {
setUpDependencies();
runApp(MyApp());
}

… que ele está “cheio de gavetas” para serem usadas/chamadas nas funções/classes que desejar, sempre usando o método get<Type>():

GetProductsUse useCase = injector.get<GetProductsUse>();
List<Product>? products = await useCase();

E agora vamos ver uma vantagem bem interessante em se usar um injetor de dependências.

Vamos dizer que a partir de agora vamos deixar de usar uma web api e passaremos por exemplo a usar o firebase!

UseCase => Repository => Gateway => Firebase => Flutter

Se a nossa web api foi bem “projetada”, com os caminhos respeitando as regras RESTFull, basta para isto criarmos uma nova implementação do Gateway usando o Firebase:

class GatewayFirebase implements Gateway {
final DatabaseReference dbRef;

GatewayFirebase(this.dbRef);

@override
Future<Response> post(String path, {Object? data}) => // do firebase stuff!

@override
Future<Response> get(String path, {Object? data}) => // do firebase stuff!
}

E alterarmos as nossas dependências, retirando o Dio, inserindo o Firebase, e alterando a criação do Gateway que trocou o Dio pelo Firebase:

void setUpDependencies() {
Injector injector = Injector.instance;

// dio
// injector.add<Dio>(Dio());

// firebase
injector.addInstance<DatabaseReference>(FirebaseDatabase.instance.ref());

// gateway
// injector.add<Gateway>(GatewayDio(injector.get<Dio>()));
injector.add<Gateway>(GatewayFirebase(injector.get<DatabaseReference>()));

// repositories
injector.add<OrderRepository>(OrderRepositoryImpl(injector.get<Gateway>()));
injector.add<ProductRepository>(ProductRepositoryImpl(injector.get<Gateway>()));

// use cases
injector.add<CreateOrderUseCase>(CreateOrderUseCaseImpl(injector.get<OrderRepository>()));
injector.add<GetOrdersUseCase>(GetOrdersUseCaseImpl(injector.get<OrderRepository>()));
injector.add<GetProductUseCase>(GetProductUseCaseImpl(injector.get<ProductRepository>()));
injector.add<GetProductsUseCase>(GetProductsUseCaseImpl(injector.get<ProductRepository>()));
}

Pronto! Simples assim! Agora toda a nossa aplicação está acessando os dados no firebase ao invés de em uma web api, sempre precisarmos alterar 1 linha sequer dos repositórios, use cases, controllers, widgets, etc!

Só isso já justificaria usar a injeção de dependência com um injector, não acham?

E no próximo artigo iremos falar mais sobre a forma ideal de usarmos o nosso injector dentro do projeto, criando o mínimo de dependência e acoplamento possível!

See ya!

--

--