Flutter Riverpod: Como registrar um ouvinte durante a inicialização do aplicativo

Tempo de leitura: 5 minutes

Você já precisou registrar um ouvinte assim que o aplicativo foi iniciado?

Exemplos disso incluem:

Em todos esses casos, nosso objetivo é:

  • registre um ouvinte de stream para lidar com todos os eventos recebidos
  • execute algum código para modificar o estado do aplicativo ou navegue para uma página específica

Por exemplo, você pode ter escrito um código como este para processar todos os links recebidos do FirebaseDynamicLinks:

void main() async {
  //Inicialização normal
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // registra um ouvinte para processar links recebidos
  FirebaseDynamicLinks.instance.onLink.listen((link) {
    // TODO: Lidar com link
  });
  // execute o aplicativo
  runApp(const MyApp());
}

Embora isso funcione, pode ficar fora de controle se o código de manipulação de eventos for complexo.

E se você precisar de mais de um ouvinte, seu método main() rapidamente se tornará uma lixeira para todo o código de inicialização do seu aplicativo.

Felizmente, o pacote Riverpod pode nos ajudar aqui e podemos usá-lo para:

  • inicializar objetos complexos que possuem uma ou mais dependências (lendo os provedores correspondentes)
  • mantenha nossa lógica de inicialização de aplicativos limpa e organizada

Ao longo do caminho, aprenderemos sobre classes úteis, como ProviderContainer e UncontrolProviderScope, e adicionaremos uma nova técnica valiosa ao nosso kit de ferramentas para desenvolvedores. 🛠

Preparar? Vamos!

 

Revisitando a lógica de inicialização do aplicativo

Como ponto de partida, vamos considerar este código do exemplo acima:

void main() async {
  ...
  // registra um ouvinte para processar links recebidos
  FirebaseDynamicLinks.instance.onLink.listen((link) {
    // TODO: Lidar com link
  });
  runApp(const MyApp());
}

Colocar isso dentro do método principal não é o ideal, especialmente se a lógica de manipulação de eventos for complexa e precisarmos acessar dependências adicionais. E também devemos evitar usar o singleton FirebaseDynamicLinks.instance aqui.

Uma abordagem melhor é:

  • mova toda a lógica de manipulação de listener e event para uma classe separada
  • inicialize essa classe quando o aplicativo for iniciado

Então vamos ver como fazer isso com o pacote Riverpod, seguindo um processo de 4 etapas.

 

1. Crie um StreamProvider

Primeiro de tudo, vamos criar um StreamProvider que nos dê acesso ao stream que precisamos:

final onDynamicLinkProvider = StreamProvider<PendingDynamicLinkData>((ref) {
  // Para simplificar, aqui usamos FirebaseDynamicLinks diretamente.
  // Nas bases de código de produção obteríamos o stream de um DynamicLinksRepository.
  return FirebaseDynamicLinks.instance.onLink;
});

Para simplificar, acessamos FirebaseDynamicLinks.instance diretamente dentro do provedor. Mas em uma base de código de produção, podemos criar um DynamicLinksRepository que usa FirebaseDynamicLinks como argumento do construtor.

 

2. Crie uma classe de serviço com o código de tratamento de eventos

Agora que temos nosso onDynamicLinkProvider, podemos criar uma classe de serviço que o utiliza:

class DynamicLinksService {
  // 1. Passe um argumento Ref para o construtor
  DynamicLinksService(this.ref) {
    // 2. Chame _init assim que o objeto for criado
    _init();
  }
  final Ref ref;

  void _init() {
    // 3. ouça o StreamProvider
    ref.listen<AsyncValue<PendingDynamicLinkData>>(onDynamicLinkProvider,
        (previous, next) {
      // 4. Implementar o código de manipulação de eventos
      final linkData = next.value;
      if (linkData != null) {
        debugPrint(linkData.toString());
        // TODO: Lidar com linkData
      }
    });
  }
}

Algumas notas:

  1. A classe DynamicLinksService usa um argumento Ref que podemos usar para acessar qualquer provedor que precisarmos.
  2. Chamamos o método _init privado imediatamente dentro do construtor.
  3. Registramos um ouvinte em nosso stream usando ref.listen.
  4. Dentro do listener callback, podemos processar os valores anteriores e seguintes conforme necessário.

O tipo dos valores previous e next acima é AsyncValue<PendingDynamicLinkData>, porque ouvir ou observar um Stream<T> sempre nos fornece valores do tipo AsyncValue<T>.

Devo também salientar que:

  • O método _init é privado e quaisquer outros métodos adicionados a esta classe também devem ser privados. Dessa forma, a única maneira de iniciar o listener é criando uma instância de DynamicLinksService.
  • Não incluí o código de manipulação de eventos, pois é específico do aplicativo. Se precisar acessar qualquer outra dependência nesta classe, você pode chamar ref.read(someProvider).someMethod().

 

3. Crie um provedor para a classe de serviço

Assim que tivermos nossa classe DynamicLinksService, podemos criar um provedor que usaremos para acessá-la:

final dynamicLinksServiceProvider = Provider<DynamicLinksService>((ref) {
  return DynamicLinksService(ref);
});

Isso é muito simples, pois só precisamos passar o argumento ref para o construtor.

Mas se iniciarmos o aplicativo agora, o código dentro do DynamicLinksService não será executado porque o dynamicLinksServiceProvider só o criará quando o lermos pela primeira vez (os provedores Riverpod são lazy-loaded) e nenhum widget ou outra classe o usará.

Em outras palavras: se quisermos utilizar o DynamicLinksService, precisamos inicializá-lo dentro do método main().

 

4. Leia o provedor de classe de serviço com ProviderContainer

Aqui está nosso método main() mais uma vez:

void main() async {
  // Inicialização normal
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // TODO: Como inicializar seu serviço DynamicLinks?
  // execute o aplicativo
  runApp(ProviderScope(
    child: const MyApp(),
  ));
}

Observe que só podemos criar um DynamicLinksService com um objeto Ref, e o método main() não possui um. 🧐

Para resolver esse problema do ovo e da galinha 🐣, precisamos usar um ProviderContainer. Veja como:

void main() async {
  // Inicialização normal
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  // 1. Crie um ProviderContainer
  final container = ProviderContainer();
  // 2. Use-o para ler o provedor
  container.read(dynamicLinksServiceProvider);
  // 3. Passe o contêiner para um UncontrolProviderScope e execute o aplicativo
  runApp(UncontrolledProviderScope(
    container: container,
    child: const MyApp(),
  ));
}

E pronto! Agora podemos ler o dynamicLinksServiceProvider com um ProviderContainer que é então passado como argumento para um UncontrolProviderScope.

E como o provedor criará o DynamicLinksService, nosso ouvinte agora será registrado e tratará todos os eventos recebidos quando o aplicativo for iniciado! 🏁

A propósito, observe que a chamada para container.read() retorna o próprio DynamicLinksService:

final dynamicLinksService = container.read(dynamicLinksServiceProvider);

Mas neste caso, podemos ignorar o valor de retorno porque não precisamos dele. Além disso, não podemos chamar nenhum método, pois o único método público é o construtor (que inicia o listener).

 

Como funcionam ProviderContainer e UncontrolProviderScope?

Mas o que é um ProviderContainer? A documentação do Riverpod o define como:

Um objeto que armazena o estado dos provedores e permite substituir o comportamento de um provedor específico.

Também diz isto:

Se você estiver usando o Flutter, não precisará se preocupar com esse objeto (fora dos testes), pois ele é criado implicitamente para você pelo ProviderScope.

A exceção a esta regra é se precisarmos criar um objeto que receba um argumento Ref dentro do método main(). Nesse caso, a criação de um ProviderContainer nos fornece explicitamente uma “saída de emergência” e nos permite acessar/inicializar o DynamicLinksService.

E se você tiver substituições de provedor no ProviderScope, agora poderá movê-las para o ProviderContainer:

final container = ProviderContainer(
  overrides: [], // liste suas substituições aqui
);

 

Conclusão

Agora descobrimos como registrar um listener durante a inicialização do aplicativo, mantendo nosso método main() organizado e organizado.

Mais uma vez, estas são as quatro etapas:

  1. Crie um StreamProvider
  2. Crie uma classe de serviço com um listener que lide com todos os eventos de stream
  3. Crie um provedor para a classe de serviço
  4. Leia o provedor de classe de serviço com ProviderContainer dentro de main()

Essa abordagem é bem dimensionada se você tiver diversas classes de serviço, pois é possível inicializar cada serviço com uma única linha de código:

final container = ProviderContainer();
container.read(authServiceProvider);
container.read(dynamicLinksServiceProvider);
container.read(messagingServiceProvider);
runApp(UncontrolledProviderScope(
  container: container,
  child: const MyApp(),
));

Isso é mais útil para ouvintes que são:

  • sempre ativo enquanto o aplicativo está em execução
  • independente da IU e não específico de nenhum widget

Porém, se desejar, você pode adicionar uma saída Stream ou ValueListenable à sua classe de serviço para que os widgets na camada da UI possam observá-la. Isso é útil se você quiser mostrar um alerta ou SnackBar, ou navegar para uma página específica quando um determinado evento ocorrer.

É isso! Agora temos um processo repetível para registrar ouvintes de forma escalável sem sobrecarregar o método main com toda a lógica de inicialização. 🚀