Do not use BuildContexts across async gaps

Viewed 26224

I have noticed a new lint issue in my project.

Long story short:

I need to use BuildContext in my custom classes

flutter lint tool is not happy when this being used with aysnc method.

Example:

   MyCustomClass{

      final buildContext context;
      const MyCustomClass({required this.context});

      myAsyncMethod() async {
        await someFuture();
        # if (!mounted) return;          << has no effect even if i pass state to constructor
        Navigator.of(context).pop(); #   << example
      }
   }

UPDATE: 17/September/2022

It appears that BuildContext will soon have a "mounted" property

So you can do:

if (context.mounted)

It basically allows StatelessWidgets to check "mounted" too.

Reference: Remi Rousselet Tweet

7 Answers

Don't stock context directly into custom classes, and don't use context after async if you're not sure your widget is mounted.

Do something like this:

class MyCustomClass {
  const MyCustomClass();

  Future<void> myAsyncMethod(BuildContext context, VoidCallback onSuccess) async {
    await Future.delayed(const Duration(seconds: 2));
    onSuccess.call();
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: () => const MyCustomClass().myAsyncMethod(context, () {
        if (!mounted) return;
        Navigator.of(context).pop();
      }),
      icon: const Icon(Icons.bug_report),
    );
  }
}

In StatefulWidget, use:

void bar(BuildContext context) async {
  await yourFuture();
  if (!mounted) return;
  Navigator.pop(context);
}

In StatelessWidget or any other class, try this approach:

class Foo {
  void bar(BuildContext context, [bool mounted = true]) async {
    await yourFuture();
    if (!mounted) return;
    Navigator.pop(context);
  }
}

If your class can extend from StatefulWidget then adding

if (!mounted) return;

would work!

EDIT

I had this issue again and again and here's the trick - use or declare variables using context before using async methods like so:

MyCustomClass{
  const MyCustomClass({ required this.context });

  final buildContext context;
  
  myAsyncMethod() async {
    // Declare navigator instance (or other context using methods/classes)
    // before async method is called to use it later in code
    final navigator = Navigator.of(context);
    await someFuture();
    
    // Now use the navigator without the warning
    navigator.pop();
  }
}

EDIT END

As per Guildem's answer, he still uses

if (!mounted) return;

so what's the point of adding more spaghetti code with callbacks? What if this async method will have to pass some data to the methods you're also passing context? Then my friend, you will have even more spaghetti on the table and another extra issue.

The core concept is to not use context after async bloc is triggered ;)

Just simpliy creat a function to call the navigation

void onButtonTapped(BuildContext context) {

Navigator.of(context).pop(); }

User Like this ::

final nav = Navigator.of(context);

await SomeFuture();

nav.pop()

just save your navigator or whatever needs a context to a variable at the beginning of the function

      myAsyncMethod() async {
        final navigator = Navigator.of(context); // 1
        await someFuture();
        navigator.pop();  // 2
      }

DO NOT use BuildContext across asynchronous gaps.

Storing BuildContext for later usage can easily lead to difficult to diagnose crashes. Asynchronous gaps are implicitly storing BuildContext and are some of the easiest to overlook when writing code.

When a BuildContext is used from a StatefulWidget, the mounted property must be checked after an asynchronous gap.

So, I think, you can use like this:

GOOD:

class _MyWidgetState extends State<MyWidget> {
  ...

  void onButtonTapped() async {
    await Future.delayed(const Duration(seconds: 1));

    if (!mounted) return;
    Navigator.of(context).pop();
  }
}

BAD:

void onButtonTapped(BuildContext context) async {
  await Future.delayed(const Duration(seconds: 1));
  Navigator.of(context).pop();
}
Related