Fala, Dev!

Neste artigo venho trazendo um tema perguntado no fórum onde a resposta é um pouco extensa e como pode ser também de seu interesse venho trazendo em formato de artigo para você! Este tema é o Desacoplamento!

A ideia de desacoplarmos as nossas dependências traz um benefício considerável quando queremos escalar ainda mais a aplicação e trazer uma redução de complexidade quando queremos garantir a manutenibilidade.

Neste pequeno artigo vou te apresentar como podemos fazer isso e entender um pouco mais sobre o tema!

Vamos iniciar das classes mais "baixas" até onde temos os dados de forma mais elaborada, nas mais "altas". Podemos compreender como as "baixas" aquelas a nível de busca de dados, como a HttpManager que realiza as requisições REST, e as mais "altas" como sendo aquelas que obtêm tais dados de forma mais estruturada e que podem realizar algum procedimento com elas, como sendo a nossa HomeController, por exemplo. Então vamos iniciar com a HttpManager.

Para que uma classe perca sua dependência a alguma outra determinada classe vamos trabalhar com a criação da classe abstrata contendo a assinatura do método que precisa ser sobrescrito implementando o código responsável por realizar uma determinada ação.

Refletindo como está estruturado no nosso app de Quitanda Virtual, a classe HttpManager contém apenas o método restRequest que precisa receber os seguintes itens: url, method, headers?, body? (? = Representação para os opcionais). Além disso o método retorma um Future<Map>.

Desta forma precisamos criar a classe abstrata com a assinatura contendo estas mesmas características, conforme abaixo:

abstract class HttpManager {
  Future<Map> restRequest({
    required String url,
    required String method,
    Map? headers,
    Map? body,
  });
}

Veja que temos o nome HttpManager, visto que essa classe é a assinatura. Na classe que faz a implementação das assinaturas é comumente adicionado o sufixo Impl conforme abaixo:

class HttpManagerImpl implements HttpManager { <===

  @override <===
  Future<Map> restRequest({
    required String url,
    required String method,
    Map? headers,
    Map? body,
  }) async {
    // Headers da requisição
    final defaultHeaders = headers?.cast<String, String>() ?? {}
      ..addAll({
        'content-type': 'application/json',
        'accept': 'application/json',
        'X-Parse-Application-Id': 'SUA_APPLICATION_ID',
        'X-Parse-REST-API-Key': 'SUA_REST_API_KEY',
      });

    Dio dio = Dio();

    try {
      Response response = await dio.request(
        url,
        options: Options(
          headers: defaultHeaders,
          method: method,
        ),
        data: body,
      );

      // Retorno do resultado do backend
      return response.data;
    } on DioError catch (error) {
      // Retorno do erro do dio request
      return error.response?.data ?? {};
    } catch (error) {
      // Retorno de map vazio para error generalizado
      return {};
    }
  }
}

Seguindo ainda o padrão para a implementação de uma classe abstrata é preciso adicionar a anotação "@override" sobre o método a ser implementado conforme acima.

Obs: Como disse estamos utilizando o nosso projeto da forma que está como exemplo rápido, mas a abstração pode ser feita também sobre o client http.


--- Segunda parte ---


Indo agora para a classe mais acima, como sendo a repository, podemos estar usando o mesmo padrão de criação de assinatura e implementação, segue abaixo o exemplo utilizando da HomeRepository.

Vamos criar a assinatura:

abstract class HomeRepository {
  Future<HomeResult<CategoryModel>> getAllCategories();
  Future<HomeResult<ItemModel>> getAllProducts(Map<String, dynamic> body);
}

E em seguida a implementação no nosso HomeRepositoryImpl:

class HomeRepositoryImpl implements HomeRepository { <===
  final HttpManager _httpManager; <===

  HomeRepositoryImpl(this._httpManager); <===

  @override
  Future<HomeResult<CategoryModel>> getAllCategories() async {
    ...
  }

  @override
  Future<HomeResult<ItemModel>> getAllProducts(Map<String, dynamic> body) async {
     ...
  }
}

Note que estamos recebendo uma instancia de HttpManager por construtor da nossa classe e não criando o objeto dentro dela. Outro ponto importante é que estamos declarando a classe abstrata que contém somente a assinatura dos métodos, mesmo sendo uma classe abstrata que não pode ser instanciada, mas como a atribuição a seguir é totalmente válida não temos problemas:

HttpManager obj = HttpManagerImpl();

Obs: Já já vemos como vamos receber o objeto HttpManagerImpl por construtor.


--- Terceira parte ---


Agora podemos subir um pouco mais e nos dirigir até a classe controladora que utiliza o HomeRespository.

A nossa classe controladora na maior parte permanecerá a mesma, com a diferença que vamos estar recebendo o objeto do repositório também por construtor. E mais uma vez estaremos declarando a classe abstrata, conforme segue:

class HomeController extends GetxController {
  final HomeRepository homeRepository; <===

  HomeController(this.homeRepository); <===
...

Obs: Não se preocupe com como vamos receber no construtor, provavelmente o projeto esteja apresentando vários erros, mas vamos resolver isso agora.


--- Quarta parte ---


Agora vamos trabalhar bastante com a injeção de dependência no GetX, para que possamos prover todos os objetos necessários para os construtores que precisam.

Lembra que inicialmente fizemos o procedimento na nossa classe HttpManager? Como esta é uma classe que, no decorrer do uso do nosso app vai ser utilizada por todos os repositorios como HomeRepository, CartRepository, AuthRepository e afins, vamos precisar fazer um procedimento para que possamos injetar o objeto HttpManagerImpl para todas as classes que precisarem dele.

Obs: Até então somente a nossa HomeRepositoryImpl precisa receber por parâmetro, mas como você vai querer replicar isso em todas as classes posteriormente essa é a abordagem que vamos realizar.

Para isso vamos estar criando também um binding, como fizemos para injetar os controllers quando iniciássemos algumas telas, como o HomeBinding por exemplo. Vou, então, chamar este binding de AppBinding:

class AppBinding extends Bindings {
  @override
  void dependencies() {
    
  }
}

Dentro do método dependencies vamos adicionar uma injeção utilizando do lazyPut com o auxílio do atributo fenix. Para saber um pouco mais sobre essa forma de injeção tenho um outro artigo na seção de Dúvidas frequentes chamado "Posso adicionar LazyPut em todos os controllers?".

A ideia dessa injeção é enviar um objeto de um determinado tipo sempre que alguma classe solicitar, e o fenix é para que esse funcionamento não pare após o primeiro envio de objeto. Vai ficar desta forma:

class AppBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<HttpManager>( <===
      () => HttpManagerImpl(), <===
      fenix: true,
    );
  }
}

Veja que estamos injetando a nossa implementação HttpManagerImpl. Como na nossa classe HomeRepositoryImpl precisamos receber uma HttpManager precisamos adicionar o cast indicando o tipo de dado que estamos injetando conforme está acima.

Próximo passo é indicarmos para o nosso app quando este binding precisa entrar em ação e injetar este objeto quando solicitado. Não podemos colocar sua ação em uma determinada tela, porque se ela se fechar perderemos a injeção, por isso o GetMaterialApp conta com um atributo chamado initialBinding! :D Vamos deixar desta forma no nosso main.dart:

...
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Greengrocer',
      initialBinding: AppBinding(), <=== AQUI
      theme: ThemeData(
...

Com isso quando o nosso app iniciar teremos a injeção já configurada que irá prover, para quem chamar, o objeto do nosso HttpManagerImpl!

Próximo passo é resolvermos os problemas do nosso HomeBinding. Assim como fizemos para a injeção do HttpManager vamos fazer também para a do HomeRepositoryImpl, com lazyPut e fenix, mas agora temos a diferença de precisar de objeto de HttpManagerImpl visto que precisamos passar no construtor, e como podemos resolver isso? Muito simples, apenas recuperamos da injeção de dependência! Podemos usar apenas o Get.find().

Em seguida precisamos fazer o mesmo para o nosso controlador, que precisa receber uma instancia de HomeRepositoryImpl. Vai ficar assim:

class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<HomeRepository>( <=== 
      () => HomeRepositoryImpl(Get.find()), <=== 
      fenix: true,
    );
    Get.put(HomeController(Get.find())); <=== 
  }
}


E é isso! :D

Com essas modificações sua aplicação irá funcionar de forma desacoplada de criação de instancias dentro da classe e recebimento por construtor de todas as assinaturas implementadas com injeção de dependências.

Espero que tenha gostado! Se este pequeno artigo te ajudou peço que me ajude a crescer na plataforma deixando sua avaliação no meu curso! :D

E como sempre, caso tenha qualquer problema ou dúvida é só mandar uma mensagem! :D

Bons estudos.