skip to Main Content

I am currently doing a mobile app development internship and working on a microfinance app. In my app, I am using Providers for state management.

I created a login function, but Flutter gave me the warning:
"Don’t use BuildContext in async gaps."

How can I resolve this issue and handle BuildContext correctly in asynchronous operations?

Thanks in advance for your help!

This is the method for loggin user in my provider

class UserProvider extends ChangeNotifier {
  //Text Editing Controller

  final TextEditingController _userNameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  //Getters For Text Eiting Controllers
  TextEditingController get userNameController => _userNameController;
  TextEditingController get passwordController => _passwordController;

  final DBController _dbController = DBController();
  final AuthController _authController = AuthController();
  final ApiController _apiController = ApiController();

  User? _loggedInUser;
  bool _isLoading = false;
  String? _errorMessage;

  // Cached
  List<String> _routesList = [];
  List<BranchModel> _branchesList = [];

//Getter
  List<String> get routesList => _routesList;
  List<BranchModel> get branchesList => _branchesList;
  bool get isLoading => _isLoading;
  String? get errorMessage => _errorMessage;

  User? get loggedInUser => _loggedInUser;

//Method to Login
  Future<bool> login({
    required String? branch,
    required BuildContext context,
  }) async {
    Logger().i("Login Method Called");

    Logger().i(
        "Username: ${userNameController.text}, Password: ${passwordController.text} Branch: $branch");
    _isLoading = true;
    _errorMessage = null;
    notifyListeners();

    // Show SpinKit loading dialog
    CustomDialog.showLoadingDialog(context);


    try {
      final response = await _apiController.loginUser(
          username: userNameController.text,
          password: passwordController.text,
          branch: branch);

      if (response['USER_ST'] == "1") {
        final List<dynamic> userDetails = response['USER_DET'];
        final List<dynamic> companyDetails = response['company'];

        if (userDetails.isNotEmpty && companyDetails.isNotEmpty) {
          //Save User Data
          for (var userJson in userDetails) {
            User user = User.fromJson(
                userJson, response['USER_ST'], companyDetails.first['code']);
            await _dbController.insertUser(user);
          }

          //Save Company Data
          for (var companyJson in companyDetails) {
            await _dbController.insertCompany(companyJson);
          }

          // Save session data in SharedPreferences
          final prefs = await SharedPreferences.getInstance();
          prefs.setString('USER_CODE', userDetails.first['code']);
          prefs.setString('USER_NAME', userDetails.first['user_name']);
          prefs.setString('BRANCH', userDetails.first['branch']);
          prefs.setString('IS_LOGIN', "1");

          _isLoading = false;
          notifyListeners();
          return true;
        } else {
          _errorMessage = "No user or company data found.";
          Logger().e(_errorMessage);
        }
      } else {
        _errorMessage = "Invalid username or password.";
        Logger().e(_errorMessage);
        CustomDialog.toast(context, "Invalid username or password.",textColor: AppColors.background,backgroundColor: AppColors.warningsRed);
      }
    } catch (e) {
      _errorMessage = "An error occurred: $e";
      Logger().e(_errorMessage);
    }

    // Hide SpinKit loading dialog if login fails
    CustomDialog.hideLoadingDialog(context);

    _isLoading = false;
    notifyListeners();
    return false;
  }
}

And this is the Method calling in my Login UI

CustomButton(
   size: size,
   buttonName: "Sign In",
   icon: Icons.arrow_circle_right_outlined,
   colors: const [
   Color(0xFF007BFF),
   Color(0xFF007BFF)
   ],
   ontap: () {
     if (_formKey.currentState!.validate()) {
     // If all data are correct then save data to out variables
     _formKey.currentState!.save();
     // You can now call your provider or any API to submit the data
     if (mounted) {
        provider.login(
           branch: branchCode,
           context: context).then((success) {
              if (success) {
              // Navigate to the homepage if login is successful
              CustomNavigator.goWithDisableBack(context, Homepage());
              } else {
              // Show an error message if login failed
              Logger().i("Login failed");
              }
           },
        );
      }
     }
   },
),

Can anyone help me to solve this?

2

Answers


  1. For your case here, I would not pass BuildContext in the login function. BuildContext is related to UI and its not a good pattern to pass it to your business logic part of your app, like you do in the login function.

    My opinion here is that you need to show the loading dialog on the UI just before calling login, then login is called and should be awaited. After the login is done, navigate to home according to success value.

    In general, just keep in mind that BuildContext should stay in the UI only.

    Login or Signup to reply.
  2. As stated in the official document Don’t use BuildContext in async gaps rule is to inform developers that:

    1. Storing BuildContext for later usage can easily lead to difficult to diagnose crashes
    2. Possibility of context used when the context is not mounted (the page is already exited but still using it’s state)

    So to use BuildContext in your async function there’s some option:

    Option 1 – Check if its mounted or not

    This option is what you use in your code. Check if the context is mounted or not.

    Future<void> asyncFunction(BuildContext context) async {
      // Implement await for asynchronous
      await Future.delayed(const Duration(seconds: 2));
    
      // -- Using conditional mounted example 1
      // 
      // If the `context` is mounted then continue to use `context`
      if (context.mounted) {
        // Use `context` after await with check if it `mounted` or not
        Navigator.pop(context);
      }
    
      // -- Using conditional mounted example 2
      // 
      // If the `context` is not mounted then exit the function.
      if (!context.mounted) return;
    }
    

    Option 2 – Use GlobalKey

    This option using globalkey to get where the current context is.

    1. Create a global variable (outside your class). Mine creating globals.dart file in lib folder to store all global variable.
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    
    1. Use your navigatorKey in your main MaterialApp widget

    Example of main.dart

    import 'package:clean_project/globals.dart';
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MainApp());
    }
    
    class MainApp extends StatelessWidget {
      const MainApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          navigatorKey: navigatorKey,
          home: Scaffold(
            body: Center(
              child: Text('Hello World!'),
            ),
          ),
        );
      }
    }
    
    1. Use this navigatorKey inside your Async Function with
    Future<void> asyncFunction(BuildContext context) async {
      // Implement await for asynchronous
      await Future.delayed(const Duration(seconds: 2));
    
      // Get the current context using `navigatorKey`.
      // 
      // Remember `currentContext` is nullable, so you need to make sure that
      // your `currentContext` is not null.
      Navigator.pop(navigatorKey.currentContext!);
    }
    

    Option 3 – Ignore rules (!!!Not Recommended)

    This option is very not recommended because it’s ignore the rules which it can caused the problem that stated in the official documentation.

    Future<void> asyncFunction(BuildContext context) async {
      // Implement await for asynchronous
      await Future.delayed(const Duration(seconds: 2));
    
      // (!NOT RECOMMENDED, FOR INFORMATION ONLY) 
      // Use context in async function by ignoring rules. Just add
      // `ignore: use_build_context_synchronously` as a comment above
      // used context code.
      // 
      // ignore: use_build_context_synchronously
      Navigator.pop(context);
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search