Como verificar se um AsyncNotifier é Mounted com Riverpod
No artigo anterior, aprendemos como verificar se um widget está mounted após realizar algum trabalho assíncrono.
Um exemplo disso é quando enviamos alguns dados do formulário e tentamos fechar a página antes que a operação seja concluída:
O que acontece se fecharmos a página antes da conclusão da operação?
Como vimos, podemos lidar com o envio do formulário com um método _submitReview
que é chamado a partir do retorno de chamada onPressed
do botão enviar:
class _LeaveReviewFormState extends ConsumerState<LeaveReviewForm> { // chamado a partir de um retorno de chamada de botão Future<void> _submitReview() async { try { // crie um objeto de revisão com a classificação e o comentário final review = Review(rating: _rating, comment: _controller.text); // obtenha o serviço de revisão de um provedor Riverpod final reviewsService = ref.read(reviewsServiceProvider); // use-o para enviar a avaliação (productId é uma propriedade da classe do widget) await reviewsService.submitReview(productId: widget.productId, review: review); // verifique se o widget ainda está montado após a operação assíncrona if (context.mounted) { // abrir a rota atual (usa a extensão GoRouter) context.pop(); } } catch (e, st) { // TODO: mostrar erro de alerta } } }
Também aprendemos que para uma melhor separação de interesses, podemos mover a lógica de negócios para uma subclasse AsyncNotifier
(do pacote Riverpod).
Com base no exemplo “leave review” acima, terminamos com este código:
class LeaveReviewController extends AutoDisposeAsyncNotifier<void> { @override FutureOr<void> build() { // nada para fazer } Future<void> submitReview({ required ProductID productId, required double rating, required String comment, }) async { // crie um objeto de revisão com a classificação e o comentário final review = Review(rating: rating, comment: comment); // obtenha o serviço de revisão de um provedor Riverpod final reviewsService = ref.read(reviewsServiceProvider); // definir o estado do widget para carregamento state = const AsyncLoading(); // enviar a revisão e atualizar o estado quando terminar state = await AsyncValue.guard(() => reviewsService.submitReview(productId: productId, review: review)); // TODO: verifique se há mounted if (state.hasError == false) { // pegue a instância do GoRouter e chame pop nela ref.read(goRouterProvider).pop(); } } }
Isso funciona obtendo a instância GoRouter
de um provedor, para que possamos abrir a rota atual:
ref.read(goRouterProvider).pop();
No entanto, se enviarmos o formulário e fecharmos a página antes que a operação assíncrona seja concluída, terminaremos com um erro “Bad state”:
Para evitar isso, precisamos verificar se o AsyncNotifier
está mounted.
Esta pode parecer uma pergunta simples. Mas, como veremos, não há uma resposta fácil. E neste artigo exploraremos algumas soluções e suas vantagens e desvantagens.
Preparar? Vamos!
Conteudo
Verificando se um AsyncNotifier está mounted
Para evitar o erro “Bad state”, poderíamos tentar escrever algo assim dentro de nosso LeaveReviewController:
final newState = await AsyncValue.guard(someFuture); if (mounted) { // Error -> Undefined name 'mounted' // * defina o estado apenas se o controlador ainda estiver montado state = newState; if (state.hasError == false) { // pegue a instância do GoRouter e chame pop nela ref.read(goRouterProvider).pop(); } }
Infelizmente, atualmente não há planos para adicionar uma propriedade mounted ao Notifier/AsyncNotifier, e o código acima não será compilado.
Então, vamos ver o que podemos fazer sobre isso. 👇
1. Implemente nossa própria propriedade “mounted”
Para focar no problema em questão, deixaremos LeaveReviewController
de lado e usaremos este notifier simples:
// apenas um simples notificador class SomeNotifier extends AutoDisposeAsyncNotifier<void> { @override FutureOr<void> build() { } Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // TODO: Verifique se mounted, então: state = newState; } } // o fornecedor correspondente final someNotifierProvider = AutoDisposeAsyncNotifierProvider<SomeNotifier, void>( SomeNotifier.new);
Como primeira tentativa, podemos tentar adicionar uma propriedade mounted
como booleana:
class SomeNotifier extends AutoDisposeAsyncNotifier<void> { // 1. inicializar como verdadeiro bool mounted = true; @override FutureOr<void> build() { // 2. definido como falso ao descartar ref.onDispose(() => mounted = false); } Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // 3. verifique antes de definir o estado if (mounted) { state = newState; } } }
Aviso: usar um sinalizador booleano mounted não é recomendado e exploraremos uma solução mais confiável em breve.
O código acima evitará o erro “Bad state” adicionando uma propriedade booleana mounted que:
- é definido como
true
na inicialização - é definido como false no descarte (usando o retorno de chamada do ciclo de vida
onDispose
) - pode ser verificado após aguardar a conclusão de algum trabalho assíncrono (observe como
newState
só é atribuído aostate
semounted
for true)
Mas temos uma questão crucial: o sinalizador mounted
dentro do nosso notifier
será definido como falso assim que o widget for desmontado?
E a resposta é sim porque estamos usando um AutoDisposeAsyncNotifier. Isto garante que o provedor seja descartado assim que o último ouvinte for removido.
Embora a solução acima não seja recomendada, ela parece funcionar bem na prática, mas não é muito SECA.
Implementando um mixin NotifierMounted
Para evitar copiar e colar toda a lógica montada em cada notificador em nosso aplicativo, podemos criar um mixin útil:
mixin NotifierMounted { bool _mounted = true; void setUnmounted() => _mounted = false; bool get mounted => _mounted; }
Veja agora como usá-lo:
class SomeNotifier extends AutoDisposeAsyncNotifier<void> // 1. add mixin with NotifierMounted { @override FutureOr<void> build() { // 2. definido como falso ao descartar ref.onDispose(setUnmounted); } Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // 3. verifique antes de definir o estado if (mounted) { state = newState; } } }
Já faz algum tempo que uso essa técnica sem problemas em meus projetos.
No entanto, recentemente me deparei com este problema em que Remi explicou que usar uma propriedade mounted
é enganoso:
Se o próprio autor de Riverpod disser para não fazer algo, é provável que ele saiba do que está falando. 😅
Na verdade, se fosse possível adicionar um sinalizador mounted
com segurança, ele teria sido incorporado ao Notifier/AsyncNotifier desde o início.
Então, vamos voltar à prancheta. 👇
2. Usando Object em vez de bool
Para resolver o problema do “mounted”, Remi recomenda usar um objeto. Então aqui está uma adaptação da solução proposta:
class SomeNotifier extends AutoDisposeAsyncNotifier<void> { Object? key; // 1. criar uma chave @override FutureOr<void> build() { key = Object(); // 2. inicialize-o ref.onDispose(() => key = null); // 3. definido como nulo ao descartar } Future<void> doAsyncWork() async { state = const AsyncLoading(); final key = this.key; // 4. pegue a chave antes de fazer qualquer trabalho assíncrono final newState = await AsyncValue.guard(someFuture); if (key == this.key) { // 5. verifique se a chave ainda é a mesma state = newState; } } }
Esta solução é tecnicamente correta, mas requer um total de cinco etapas apenas para verificar se o notificador está mounted.
Então, como avançamos?
3. Use um StateNotifier
Como adicionar mounted
ao Notifier/AsyncNotifier é proibido, poderíamos usar um bom e velho StateNotifier
.
Então aqui está uma implementação equivalente da mesma classe:
class SomeNotifier extends StateNotifier<AsyncValue<void>> { SomeNotifier(this.ref) : super(const AsyncData(null)); final Ref ref; Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // mounted é uma propriedade e podemos usá-la se necessário if (mounted) { state = newState; } } } // o provider correspondente final someNotifierProvider = StateNotifierProvider.autoDispose<SomeNotifier, AsyncValue<void>>( (ref) => SomeNotifier(ref));
Como podemos ver, a sintaxe é um pouco diferente:
- precisamos fornecer um valor inicial para o super construtor, de forma síncrona
- se precisarmos de uma
ref
, precisamos declará-la explicitamente e passá-la ao construtor - precisamos usar o modificador
autoDispose
na declaração do provedor
Mas pelo lado positivo, mounted
já é uma propriedade de StateNotifier
e podemos usá-lo conforme necessário.
Na verdade, esse é um bom caminho a percorrer, desde que você não precise das APIs modernas do Notifier (inicialização assíncrona, argumentos múltiplos, geração de código).
Conclusão
Começamos com uma pergunta simples: podemos verificar se um AsyncNotifier
está mounted?
Surpreendentemente, não existe uma resposta simples e acabamos com três alternativas:
- adicione nosso próprio sinalizador
mounted
dentro de um mixinNotifierMounted
→ não recomendado - implementar alguma lógica complicada usando
Object
→ correto, mas sujeito a erros e não bom - use o bom e velho
StateNotifier
→ funciona bem, mas não usa a sintaxe moderna doNotifier