Flutter: Injeção de Dependência

Christian Batista
Flutter Brasil
Published in
4 min readMay 18, 2024

Injeção de Dependência é um dos padrões atualmente usados em grandes projetos e muito se tem falado sobre isto, mas talvez nem todos saibam exatamente o que é!

Para aqueles que ainda não sabem o que significa:

Injeção de Dependência é uma técnica de programação onde um objeto ou função recebe como “parâmetro” outros objetos ou funções que necessita, ao invés de criá-los (instanciá-los) internamente.

Agora que voce já sabe pelo menos a “definição” de Injeção de Dependência, vem a pergunta:

“Mas por que cargas d’água precisamos usa-la? Qual a utilidade dela na vida real?”

Existem várias razões para aplicarmos este padrão, mas para mim a principal utilidade dela é: permitir testes unitários automatizados!

Vamos a um exemplo prático para entendermos melhor o processo!

Suponhamos que tenhamos uma aplicação que precisa acessar dezenas de “endpoints” dentro da sua api. Precisaremos criar dezenas de “repositórios”, mas vamos nos concentrar em apenas 1 deles por enquanto (e bem simplificado):

  • AuthRepository: responsável em comunicar com o “auth” da sua api (login, logout, password-reset, etc…)
class AuthRepository {
Dio dio = Dio();

Future<User?> login(String email, String password) async {
try {
var response = await dio.post("http://localhost:3000/login", data: {"email": email, "password": password});
if (response.statusCode != 200) return null;
return User.fromJson(response.data);
} catch (e) {
return null;
}
}
}

Note nesta classe nós a criamos com a “dependência” da classe Dio, onde o estamos usando para fazer a comunicação com o nosso backend.

Logo na segunda linha da função login, disparamos um método desta classe que faz uma consulta a uma api externa (via post).

O teste desta classe seria então:

(obs: o teste está bem simplificado para efeito de ilustração do problema!)

void main() {
test('AuthRepository.login with correct credentials', () async {
AuthRepository authRepository = AuthRepository();
User? user = await authRepository.login("myemail@email.com", "123456");
expect(user!.email, "myemail@email.com");
});

test('AuthRepository.login with wrong credentials', () async {
AuthRepository authRepository = AuthRepository();
User? user = await authRepository.login("myemail@email.com", "wrong password");
expect(user, null);
});
}

E é bem aqui que as coisas começam a se complicar!!

Quando rodamos o teste unitário da nossa classe AuthRepository, ele irá disparar o método login e irá “bater” em uma api! E não importa se esta api for de “testes” ou de “produção”.

A idéia dos testes unitários é permitir que possamos validar o nosso código sem a necessidade de “bater” nos nosso endpoints externos! Isto se deve por várias questões, desde performance (uma vez que os testes unitários PRECISAM ser rápidos), até mesmo o controle extato que cada endpoint irá retornar.

Ou seja: mesmo que o endpoint seja “local”, não é aconselhável acionar a nossa api todas as vezes que formos rodar nossos testes unitários! (Os testes que vão acionar a api são os de INTEGRAÇÃO, e serão feitos em outro momento.)

Mas da forma como nós implementamos a “dependência” da classe Dio, estamos condicionamos a SEMPRE acionar a api e sem a possibilidade de substituí-la por um outro objeto “simulado”, onde poderíamos controlar o comportamento dele.

OK, certo… mas como resolver isso?

É aqui que vamos então aplicar o tal do bendito padrão de Injeção de Dependência!

A idéia principal por debaixo do capô da Injeção de Dependência é que a classe que “usa” a dependência (no caso a classe AuthRepository) não “saiba” necessariamente qual implementação da “comunicação” ela está utilizando!

Para isso primeiramente então devemos criar uma classe abstrata que irá servir de contrato para a nossa comunicação externa:

abstract class HttpGateway {
Future<Response> post(String path, {Object? data});
}

E então reescrevemos a nossa classe AuthRepository de forma a “solicitar” como dependência uma instância já criada da abstração:

class AuthRepository {
HttpGateway httpGateway;

AuthRepository(this.httpGateway);

Future<User?> login(String email, String password) async {
try {
var response = await httpGateway.post("http://localhost:3000/login", data: {"email": email, "password": password});
if (response.statusCode != 200) return null;
return User.fromJson(response.data);
} catch (e) {
return null;
}
}
}

Veja que agora a nossa classe AuthRepository recebe no construtor o objeto que implementa o HttpGateway como dependência (o objeto é INJETADO ao invés de ser CRIADO).

Basta agora criamos 2 implementações do nosso contrato HttpGateway:

  • Uma implementação para ser usada “na aplicação”, onde nesta sim teremos a instancia interna do Dio:
class HttpGatewayDio implements HttpGateway {
Dio dio = Dio();

@override
Future<Response> post(String path, {Object? data}) => dio.post(path, data: data);
}
  • e outra para ser usada “nos testes”, onde nesta podemos “simular” o que os métodos respondem:
class HttpGatewayFake implements HttpGateway {
@override
Future<Response> post(String path, {Object? data}) async => onPost(path, data:data);

Future<Response> Function(String path, {Object? data}) onPost = (String path, {Object? data}) => throw UnimplementedError();
}

E agora alteramos o nosso teste para “responder” o comportamento desejado da api!

Ou seja: não vamos precisar “consultar” em uma api! Basta simularmos a resposta que o HttpGateway deveria dar ao “consultar” a api.

Isto nos permite responder ao método “post” da forma que desejamos! E de forma extremamente rápida!

void main() {
test('AuthRepository.login with correct credentials', () async {
HttpGatewayFake httpGateway = HttpGatewayFake();
httpGateway.onPost = (String path, {Object? data}) async => Response(statusCode: 200, data: {"email": (data as Map<String, dynamic>)["email"]}, requestOptions: RequestOptions(path: path));
AuthRepository authRepository = AuthRepository(httpGateway);
User? user = await authRepository.login("myemail@email.com", "123456");
expect(user!.email, "myemail@email.com");
});

test('AuthRepository.login with wrong credentials', () async {
HttpGatewayFake httpGateway = HttpGatewayFake();
httpGateway.onPost = (String path, {Object? data}) async => Response(statusCode: 404, data: null, requestOptions: RequestOptions(path: path));
AuthRepository authRepository = AuthRepository(httpGateway);
User? user = await authRepository.login("myemail@email.com", "wrong password");
expect(user, null);
});
}

Pronto! Agora você já sabe usar Injeção de Dependência!

E principalmente… já sabe o porquê de usá-la!

A próxima etapa é criarmos um Injetor de Dependências!

Mas este é assunto para o próximo artigo!

See ya!

--

--