Como verificar se um AsyncNotifier é Mounted com Riverpod

Tempo de leitura: 5 minutes

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!

 

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:

  1. é definido como true na inicialização
  2. é definido como false no descarte (usando o retorno de chamada do ciclo de vida onDispose)
  3. pode ser verificado após aguardar a conclusão de algum trabalho assíncrono (observe como newState só é atribuído ao state se mounted 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:

  1. adicione nosso próprio sinalizador mounted dentro de um mixin NotifierMountednão recomendado
  2. implementar alguma lógica complicada usando Object → correto, mas sujeito a erros e não bom
  3. use o bom e velho StateNotifier → funciona bem, mas não usa a sintaxe moderna do Notifier