[Solved] Flutter async await not working as expected

Question

Asked by WebRoose D on December 20, 2021 (source).

Since I have started Flutter am facing an issue related to Flutter async-await. Most of the time I try to use Future and await for the result it skips the await and gets the shortest way to return. if i try to print after await the null value prints first and then await is called

here is my onPressed

 onPressed: () async {
                  if (_textEditingController.text.isNotEmpty) {
                    Map a = await Authentication.sendOtp(
                        phoneNum: _textEditingController.text);
                    print(a);
                  }
                },

and my Authentication class:

class Authentication {
 static Future<Map> sendOtp({required String phoneNum}) async {
    String? vid;
    try {
      if (!kIsWeb) {
        await FirebaseAuth.instance.verifyPhoneNumber(
          phoneNumber: phoneNum,
          verificationCompleted: (PhoneAuthCredential credential) {},
          verificationFailed: (FirebaseAuthException e) {},
          timeout: const Duration(seconds: 5),
          codeSent: (String verificationId, int? resendToken) {
            print('Code Sent $verificationId');
            vid = verificationId;
          },
          codeAutoRetrievalTimeout: (String verificationId) {},
        );
      } else {
        final recaptchaVerifier = RecaptchaVerifier(
            container: null,
            size: RecaptchaVerifierSize.compact,
            theme: ThemeMode.system as RecaptchaVerifierTheme);
        await FirebaseAuth.instance
            .signInWithPhoneNumber(phoneNum, recaptchaVerifier)
            .then((confirmationResult) {
          vid = confirmationResult.verificationId;
        });
      }
      return {'msg': vid, 'val': false};
    } on FirebaseAuthException catch (e) {
      print('------${e.code}');
      return {'msg': e.code, 'val': true};
    } catch (e) {
      print(e);
      return {'msg': null, 'val': true};
    }
  }
}

output i get:

I/flutter (14230): {msg: null, val: false}
E/zzf     (14230): Problem retrieving SafetyNet Token: 7: 
W/System  (14230): Ignoring header X-Firebase-Locale because its value was null.
W/System  (14230): A resource failed to call end. 
W/System  (14230): A resource failed to call end. 
D/EGL_emulation(14230): eglCreateContext: 0xef618f80: maj 2 min 0 rcv 2
E/zzf     (14230): Failed to get reCAPTCHA token with error [The web operation was canceled by the user.]- calling backend without app verification
I/FirebaseAuth(14230): [FirebaseAuth:] Preparing to create service connection to fallback implementation
W/System  (14230): Ignoring header X-Firebase-Locale because its value was null.
I/flutter (14230): Code Sent AJOnW4ROl1S4AeDErwZgls2LAxaQuwURrzDMJ1WNjQH8hWce-BTUeUE21JyCvHpMvfxT4TA8Hcp-mSWFqlzzX-IEd7X6z8ry1mkeCHC7u_ir-lnBL89OP0M6-4kU7BlOKcMPBY5OT4pmpdjETCoyAhrdc8TBR8yJqw
W/FirebaseAuth(14230): [SmsRetrieverHelper] Timed out waiting for SMS.

Please help make a better understanding of flutter async-await, or show me where am doing wrong so that I can improve my code

Answer

Question answered by osaxma (source).

You're not using await wrong per se but you're having the wrong expectation.

FirebaseAuth.instance.verifyPhoneNumber will complete its future once the function is executed but it'll not wait until the SMS is sent. The Future here indicate that the process of phone verification has started. In other words, the codeSent callback will be called at a later time after the Future was completed (i.e. until the SMS is sent to the user):

 /// [codeSent] Triggered when an SMS has been sent to the users phone, and
 ///   will include a [verificationId] and [forceResendingToken].

You'll need to account for that behavior in your app/widgets.

Here's an approach:

Change your function definition to:

static Future<void> sendOtp({required String phoneNum, required PhoneCodeSent codeSent}) {
    String? vid;
    try {
      if (!kIsWeb) {
        await FirebaseAuth.instance.verifyPhoneNumber(
          phoneNumber: phoneNum,
          verificationCompleted: (PhoneAuthCredential credential) {},
          verificationFailed: (FirebaseAuthException e) {},
          timeout: const Duration(seconds: 5),
          codeSent: codeSent, // <~~ passed from your app
          codeAutoRetrievalTimeout: (String verificationId) {},
        );
      }
    // the rest is the same without return values tho
}

Since you edited the code above to let the app receive the data once codeSent is called, you don't need to return anything from sendOtp.

Now in your widget:

onPressed:  () async {
    if (_textEditingController.text.isNotEmpty) {
      await Authentication.sendOtp(
        phoneNum: _textEditingController.text,
        codeSent: (String verificationId, int? resendToken) {
          // #2 Once this is called (which will be after the `print(a)` below),
          // update your app state based on the result (failed or succeeded)
        }
      );
      // #1 update your app state to indicate that the 'Message is on the way'
      // maybe show a progress indicator or a count down timer for resending
     // print(a);  <~~ there's no `a` anymore
    }
  };

As you can see above, the code #1 will be executed before the code #2 since codeSent is called at a later time. I'm not sure if there's a timeout or if you've to keep your own timer though.


If you don't want to handle the data at the UI, you can change the callback to something else and make it return the Map as before:

static Future<void> sendOtp({required String phoneNum, required ValueChanged<Map<String, dynamic>> onCodeSent}) {
    String? vid;
    try {
      if (!kIsWeb) {
        await FirebaseAuth.instance.verifyPhoneNumber(
          phoneNumber: phoneNum,
          verificationCompleted: (PhoneAuthCredential credential) {},
          verificationFailed: (FirebaseAuthException e) {},
          timeout: const Duration(seconds: 5),
          codeSent: (String verificationId, int? resendToken) {
            onCodeSent.call({'msg': verificationId, 'val': true});
          },
          codeAutoRetrievalTimeout: (String verificationId) {},
        );
      }
// the rest is the same without return values tho
}

and on your widget, you can do something like this:

onPressed:  () async {
    if (_textEditingController.text.isNotEmpty) {
      await Authentication.sendOtp(
        phoneNum: _textEditingController.text,
        codeSent: (Map<String, dynamic> map) {
          setState((){ 
             a = map
          });
        }
      );
 
    }
  };

The same applies for the web portion, just invoke the callback with the map:

final recaptchaVerifier = RecaptchaVerifier(
            container: null,
            size: RecaptchaVerifierSize.compact,
            theme: ThemeMode.system as RecaptchaVerifierTheme);
        await FirebaseAuth.instance
            .signInWithPhoneNumber(phoneNum, recaptchaVerifier)
            .then((confirmationResult) {
          onCodeSent.call({'vid': confirmationResult.verificationId, 'val': true);
        });
      }
ASYNC-AWAIT ASYNCHRONOUS DART FLUTTER FUTURE
SHARE: