Flutter Riverpod: Como registrar um ouvinte durante a inicialização do aplicativo
Você já precisou registrar um ouvinte assim que o aplicativo foi iniciado?
Exemplos disso incluem:
- Ouvindo mensagens recebidas do pacote
FirebaseMessaging
- Ouvindo links dinâmicos recebidos do pacote
FirebaseDynamicLinks
- Ouvindo alterações de estado de autenticação do pacote
FirebaseAuth
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!
Conteudo
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 umDynamicLinksRepository
que usaFirebaseDynamicLinks
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:
- A classe
DynamicLinksService
usa um argumento Ref que podemos usar para acessar qualquer provedor que precisarmos. - Chamamos o método
_init
privado imediatamente dentro do construtor. - Registramos um ouvinte em nosso stream usando
ref.listen
. - Dentro do listener callback, podemos processar os valores anteriores e seguintes conforme necessário.
O tipo dos valores
previous
enext
acima éAsyncValue<PendingDynamicLinkData>
, porque ouvir ou observar umStream<T>
sempre nos fornece valores do tipoAsyncValue<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 deDynamicLinksService
. - 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:
- Crie um
StreamProvider
- Crie uma classe de serviço com um listener que lide com todos os eventos de stream
- Crie um provedor para a classe de serviço
- Leia o provedor de classe de serviço com
ProviderContainer
dentro demain()
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. 🚀