1

1. The Problem

I'm using a try-catch-finally1 dynamic to deal with exception handling for Firebase in my simple authentication package for my app. In it, when I receive an error from Firebase's authentication service, I update the labelText on the (custom) TextFormField to show the user that an error occurred.

However, notifyListeners() is apparently being run concurrently to the catch clause, causing the rebuilds to not happen synchronously with exception-handling.

Currently, I'm using ChangeNotifierProvider for basically everything, but should I change to a FutureProvider dynamic. If so, what's the best way of doing it?


1 I did try using then-catchError-whenComplete also.

2. The Code

2.1 The Complete Code (Optional)

The complete code is a bit long at this point in the project, but I think that what's shown below will suffice.

At any rate, if you want to check everything out, the whole project is available: flutter_firebase_auth_benchmark.

The relevant files are:

  • firebase_auth.dart: the provider class with the relevant data.
  • login_screen.dart: the email_field is fed with provider data with the Consumer widget.
  • password_reset_workflow.dart: the button that calls the relevant method is put into the LoginScreen through an AnimatedSwitcher.

2.2 The Provider Class

I'm having to use Future.delayed in order to manually synchronize the notifyListeners() and the build() methods — I know, I know, it's very bad...

class Auth extends ChangeNotifier {
  String _errorMsg;

  String get errorMsg => _errorMsg;

  ...

  Future<void> sendPasswordResetWithEmail({@required String email}) async {
    try {
      await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
    } catch(e) {
      switch (e.code) {
        case 'ERROR_USER_NOT_FOUND':
          _errorMsg = 'user not found';
          break;
        default:
          throw UnknownPasswordResetError(
              'Unknown error for password reset with Firebase.');
      }
    } finally {
      notifyListeners();
      await Future.delayed(Duration(milliseconds: 10));
    }
  }
}

2.3 My Custom TextFormField Widget

Something like this is in the app:

return Consumer<Auth>(
  builder: (context, auth, _) {
    return AuthTextFormField(
      ...,
      errorMsgFromServer: auth.errorMsg,
    );
  }
);

Lastly, a validation button uses formKey.currentState.validate() in an if clause to trigger asynchronously await auth.sendPasswordResetWithEmail.

Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
  • So far, the closest I've been to answering this problem is related to [this SO answer](https://stackoverflow.com/a/56480917/4756173). – Philippe Fanaro Mar 19 '20 at 23:13

1 Answers1

0

notifyListeners() is apparently being run concurrently to the catch clause

notifyListeners() is inside a finally block, finally will ALWAYS be executed, either if you had an error or not, that's why notifyListeners() is always being called.

Also, using Future.delayed to synchronize events is very bad, you should make another architecture for it.

Michel Feinstein
  • 13,416
  • 16
  • 91
  • 173
  • I know `notifyListeners()` will always be executed, but I wish it were executed after error handling and not after the completion of the `try` clause. And I also know that using `Future.delayed` to synchronize events is very bad, that's probably the whole reason for posting the question. – Philippe Fanaro Mar 19 '20 at 22:42
  • What do you mean by "after the error handling and not after `try`"? I am sorry but I am having trouble understanding your English – Michel Feinstein Mar 19 '20 at 22:44
  • Fala em português que depois eu edito a resposta em inglês pra todo mundo entender. – Michel Feinstein Mar 19 '20 at 22:47
  • From the many variations, it seems that the sequence in which the code is being executed is: `try` → `catch` in parallel with `finally`. So `notifyListeners()` would be called while the `errorMsg` was being updated. The `Future.delayed` call would make things synchronize manually. But I'm not sure about all of the sequencing, that's only what I was able to guess while debugging. – Philippe Fanaro Mar 19 '20 at 22:50
  • desculpa já tinha escrito em inglês antes de ver a sua última mensagem. – Philippe Fanaro Mar 19 '20 at 22:51
  • Nao existe "paralelo" em Dart, tudo roda em serie. Quando voce atualiza `_errorMsg ` voce precisa mandar o Flutter dar rebuild na tua tela pra essa nova informacao aparecer, voce tem que ver quem ta gerando esse rebuild pra ver qual o problema. Geralmente o `notifyListeners()` faz o rebuild automaticamente entao voce precisa dele pra que a informacao apareca. – Michel Feinstein Mar 19 '20 at 22:57
  • Voce ta fornecendo o `Auth` com um `ChangeNotifierProvider` ne – Michel Feinstein Mar 19 '20 at 23:01
  • Sim, estou fornecendo o `Auth` com um `ChangeNotifierProvider`. Como eu mencionei, a arquitetura está ficando gradualmente mais complicada, então fica difícil de eu fazer um resumo aqui. Mas acho que a maior dificuldade no memomento é que a criação do campo fica em uma tela e o botão que atualiza o `labelText` aparece através de um `AnimatedSwitcher`. Se você quiser dar uma olhada: [flutter_firebase_auth_benchmark](https://github.com/psygo/flutter_firebase_auth_benchmark). – Philippe Fanaro Mar 19 '20 at 23:11
  • Qual o arquivo com problema? – Michel Feinstein Mar 19 '20 at 23:15
  • O campo `email_field` na `login_screen.dart` é o que deveria sofrer alterações via `provider`. O arquivo `firebase_auth.dart` é o que possui a classe do `provider`. E o arquivo `password_reset_workflow.dart` é o que possui o botão que efetua a atualização (`notifyListeneres()`). – Philippe Fanaro Mar 19 '20 at 23:22
  • Você tem que criar um provider com o Type da classe, tem que ser `ChangeNotifierProvider(create:` – Michel Feinstein Mar 20 '20 at 01:46
  • Tentei isso, mas já achava que não ia ajudar de qualquer maneira, pois o Dart está capturando implicitamente o tipo do `ChangeNotifierProvider` quando ele compila o código. – Philippe Fanaro Mar 20 '20 at 02:23
  • Eu tentei rodar o teu codigo, mas nao posso pq nao tenho o `json` do firebase, entao so posso chutar qual é o problema. Eu acho que voce deve mudar a sua arquitetura e fazer tudo virar um `Future` que so retorna quando ta tudo pronto e perfeito. – Michel Feinstein Mar 22 '20 at 01:49
  • Eu acho que descobri qual é o problema, mas não tenho 100% de certeza. Eu tenho dois providers, um que cuida do `Form` e do gerenciamento do `AnimatedSwitcher` (`LoginWorkflowHandler`) e o outro que cuida das transações com o Firebase (`Auth`). O problema, acho, é que, quando eu aperto o botão pra mandar um pedido de nova senha, por exemplo, o provider `Auth` pede pra atualizar a árvore e, logo em seguida, no mesmo `onPressed`, o `LoginWorkflowProvider` também pede para que uma atualização ocorra, mas através do `save()` do `Form`. Acho que a concorrência entre eles está causando o problema. – Philippe Fanaro Mar 22 '20 at 02:09
  • Eu melhorei um pouco a solução colocando o `Future.delayed` no ato de `save()` do `Form`. Ficou parecido com o que você sugeriu acho. Dá uma olhada [aqui](https://github.com/psygo/flutter_firebase_auth_benchmark/blob/4fec5c3cfb4145bf9db4fb64828ba95147f1aa2a/lib/providers/login_workflow_provider.dart#L36-L39). Esse delay é algo interno ao aparelho, ou seja, não depende da conexão de internet. Eu coloquei 100ms, pois acho que nenhum aparelho hoje dia precisaria de mais pra fazer os cálculos intermediários. – Philippe Fanaro Mar 22 '20 at 02:13
  • 2
    Eu acho que você ta abusando um pouco dessa arquitetura... Você pode simplificar as coisas com FutureBuilder, StreamBuilder etc e nem precisar de um ChangeNotifier – Michel Feinstein Mar 22 '20 at 02:17
  • Sim, concordo. Mas é que eu acabei descobrindo esses problemas só nos últimos dias... E agora estou meio que sem paciência pra mudar a arquitetura. – Philippe Fanaro Mar 22 '20 at 02:18
  • Se você ainda estiver com vontade e disponibilidade de me ajudar — o que eu agradeço bastante —, eu estou disponibilizando o arquivo do `google-services.json` [aqui](https://drive.google.com/file/d/1ePm3ESSgZaeXhYemCVnRAlwhPXrLVdUV/view?usp=sharing) para você. – Philippe Fanaro Mar 22 '20 at 20:09