179

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

Bermjly Team
  • 2,315
  • 3
  • 7
  • 16
  • doesn't seem wise to pass context to objects like that for the purpose of navigation. If your navigation stack changes after you have passed the context to MyCustomClass and you try to navigate again using the old context, you will get errors. – Mozes Ong Aug 21 '21 at 11:25
  • I agree, then how this scenario should be approached? – Bermjly Team Aug 21 '21 at 12:54
  • Use some state management like BloC, where you can trigger navigation when a state changes. So long as you do not store your context, but instead, use the context for navigation purposes without storing the instance. – Mozes Ong Aug 21 '21 at 12:59
  • 3
    Does this answer your question? https://stackoverflow.com/a/69512692/1032613 – WSBT May 27 '22 at 04:28
  • There is no context.mounted property. Please update answer. – Scorb Dec 31 '22 at 02:53
  • @Scorb It will be there in next flutter major update. – Bermjly Team Dec 31 '22 at 11:11
  • Try to switch the flutter channel to beta using 'flutter channel beta'. It looks like 'mounted' is an experimental param. – XDen23333 Jan 04 '23 at 05:44

12 Answers12

204

Update Flutter 3.7+ :

mounted property is now officially added to BuildContext, so you can check it from everywhere, whether it comes from a StatefulWidget State, or from a Stateless widget.

While storing context into external classes stays a bad practice, you can now check it safely after an async call like this :

class MyCustomClass {
  const MyCustomClass();

  Future<void> myAsyncMethod(BuildContext context) async {
    Navigator.of(context).push(/*waiting dialog */);
    await Future.delayed(const Duration(seconds: 2));
    if (context.mounted) Navigator.of(context).pop();
  }
}

// Into widget
  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: () => const MyCustomClass().myAsyncMethod(context),
      icon: const Icon(Icons.bug_report),
    );
  }
// Into widget

Original answer

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),
    );
  }
}
Guildem
  • 2,183
  • 1
  • 10
  • 13
  • 20
    How can you do it in a StatelessWidget? – stk Mar 04 '22 at 08:33
  • 2
    @stk I don't think you can, a StatelessWidget don't really have a lifecycle (no state). But if you want an async method, then you have a long-ish thing to do, and you want the app user to see something is happening, so you need a state. If you look at your app, you should see where you need to handle this state and create a Stateful widget. – Guildem Mar 05 '22 at 09:12
  • 3
    @stk In stateless widget, you can use this: https://stackoverflow.com/a/69512692/11675817 – M Karimi Jun 12 '22 at 06:27
  • that genius xD , what i have been doing is making all the methods inside the widget class just so i can get access to the mounted lol , but this actually will help a lot , thank you sir – Zack Heisenberg Oct 13 '22 at 02:55
  • I think the flow of the code dosen't changes anything using this method, even if we use the async method outside it just avoids that lints. but the issue remains the same. – TarunNagaSai Nov 18 '22 at 13:11
  • @TarunNagaSai what issue do you see with this method ? The main issue (besides the justified lint warning) of the question was to call the navigator of a potentially unmounted context, after an async call. In this version, the navigator is called after the async method, but only if the component is mounted, removing the issue (and the lint warning). – Guildem Nov 19 '22 at 10:52
  • 2
    @stk answer updated for stateless widgets on Flutter 3.7+ – Guildem Jan 26 '23 at 06:58
  • 1
    I get error `the getter 'mounted' isn't defined for the type 'BuildContext'` . I am using stateless widget – Aseem Feb 23 '23 at 22:55
  • 1
    @Aseem are you using Flutter 3.7 ? – Guildem Feb 25 '23 at 07:45
  • "While storing context into external classes stays a bad practice". So you can't pass the build context to extracted functions? What would be an alternative for keeping the code clean? – Marcel May 17 '23 at 23:03
  • @Marcel you can pass it, but not keep it as an internal property of an external class. context is only a part of the current displayed UI, and can evolve (and be unmounted) on each new frame, that's why it's all good when used synchronously, but you must check its mounted state after asynchronous process (because process is running while UI is evolving). – Guildem May 18 '23 at 12:20
  • @Guildem I'm having an asynchronous function inside my provider class. So I must wait it completes before navigating to another route. It looks like: if( context.mounted ) { await context.read().myFunc(); Navigator.of(context)..movingRoute; // issue still warning at this line } To fix this I have to put two these lines into 2 if statements. Which looking so dumb. Is there another solution for this, sir? – Huỳnh Đức Bùi Jul 21 '23 at 04:22
  • 1
    @HuỳnhĐứcBùi You need to test if your context is mounted AFTER each async call, so after your myFunc() call. The purpose is to check if something happened in your widgets tree while async call was running, before trying anything else with your context. – Guildem Jul 22 '23 at 07:24
64

Use context.mounted*

In StatefulWidget/StatelessWidget or in any class that has BuildContext:

void foo(BuildContext context) async {
  await someFuture();
  if (!context.mounted) return;
  Navigator.pop(context); // No warnings now
}

* If you're in a StatefulWidget, you can also use just mounted instead of context.mounted

iDecode
  • 22,623
  • 19
  • 99
  • 186
  • Can you please explain the `StatelessWidget` example? Where do we pass in `mounted` of `false`? – BeniaminoBaggins Sep 17 '22 at 22:24
  • 3
    @BeniaminoBaggins `StatelessWidget` is always mounted, so you never have to pass `mounted: false` in them, but to get away with the warning, it's better to have a field named `mounted` set to `true`. – iDecode Sep 18 '22 at 09:02
  • Thanks, that is very interesting. Is that in any docs? I don't feel like it is well known. – BeniaminoBaggins Sep 18 '22 at 19:54
  • 2
    @BeniaminoBaggins This entire solution/concept is nowhere in the docs (as far as I know). Docs may suggest you to convert your `StatelessWidget` to `StatefulWidget` and then use `mounted` flag to check if the widget is in the tree. – iDecode Sep 19 '22 at 05:40
  • 1
    Ah, I see `mounted` is on `BuildContext` now. And it is mentioned in the question. – BeniaminoBaggins Sep 27 '22 at 05:35
  • Isn't the `StatelessWidget` example effectively just masking the warning? The root issue--using the context across an async gap--is still there (due to `await`). – Chuck Batson Dec 17 '22 at 02:18
  • 1
    `the getter 'mounted' isn't defined for the type 'BuildContext'` I get this error on my stateless widget – Aseem Feb 23 '23 at 22:59
  • 2
    @Aseem Update your Flutter version using `flutter upgrade`. – iDecode Feb 24 '23 at 20:58
29

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 ;)

Ruchit
  • 2,137
  • 1
  • 9
  • 24
mcgtrt
  • 657
  • 6
  • 22
  • 5
    This solution does not work. It has exactly the same problem as if you had called ```Navigator.of(context).pop()``` directly from the async method. If this hides the related analyzer warning, it is only a bug in the analyzer. – kine Jun 28 '22 at 13:47
  • what about if I don't have a stateful widget and I have that navigator code inside a normal class? – Dani Jul 24 '22 at 14:34
  • Hey guys, please see my answer after edit. I have found the solution for this problem after many tries myself as this issue was sometimes visiting me back and I said enough :) – mcgtrt Jul 27 '22 at 17:41
  • As it is, your answer is wrong. Context can't be used without checking mounted before use on async method. On the custom class, without StatefulWidget implementation, you must let the caller of myAsyncMethod check a context it may not have itself here (because you gave context at the wrong step and it may have changed since constructor). There's no spaghetti code in the accepted answer. Only adaptation for the "Flutter isn't happy when I use context in my custom async method of my custom class". And there's no reason a service class do UI stuff itself. Not its role. – Guildem Jul 28 '22 at 10:26
  • As a confirmation of my solution, you can even see the comments under your own answer, which states similarly, here you have also richer explanation why this solution is correct and is not used in any case only to silence the warning: stackoverflow.com/a/69512692/11675817. Your answer is not being denied, your answer is correct. Sometimes there are more complex functions which requires handling additional parameters on return and that makes it problematic - more code, and then resulting in spaghetti code. Using context before async gap is solution, also with connection with things like BLOC. – mcgtrt Jul 28 '22 at 19:26
  • This is not every inituitive, but adding that check fixed my problem. Gotta look further into this to figure out what exactly is the problem here... – BastiBen Sep 17 '22 at 07:23
7

If you want to use mounted check in a stateless widget its possible by making an extension on BuildContext

extension ContextExtensions on BuildContext {
  bool get mounted {
    try {
      widget;
      return true;
    } catch (e) {
      return false;
    }
  }
}

and then you can use it like this

if (context.mounted)

Inspiration taken from GitHub PR for this feature and it passes the same tests in the merged PR

Markusrut
  • 71
  • 1
  • 4
6

you can use this approach

myAsyncMethod() async {
    await someFuture().then((_){
    if (!mounted) return;          
    Navigator.of(context).pop(); 
  }
});
Munsif Ali
  • 1,839
  • 1
  • 8
  • 22
4

In Flutter 3.7.0 BuildContext has the property mounted. It can be used both in StatelessWidget and StatefulWidgets like this:

void bar(BuildContext context) async {
  await yourFuture();
  if (!context.mounted) return;
  Navigator.pop(context);
}
Yayo Arellano
  • 3,575
  • 23
  • 28
2

This help for me.

/// transition to the main page after a pause of 3 seconds
Future<void> _navigateToMainScreen(BuildContext context) async {
  await Future.delayed(const Duration(seconds: 3));
  if (context.mounted) {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => const MainScreen()));
  }
}
-1

Just simplify create a function to call the navigation

void onButtonTapped(BuildContext context) {
  Navigator.of(context).pop();
}
Tyler2P
  • 2,324
  • 26
  • 22
  • 31
-1

To avoid this in StatelessWidget you can refer to this example

class ButtonWidget extends StatelessWidget {
  final String title;
  final Future<String>? onPressed;

  final bool mounted;

  const ButtonWidget({
    super.key,
    required this.title,
    required this.mounted,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const SizedBox(height: 20),
        Expanded(
            child: ElevatedButton(
          onPressed: () async {
            final errorMessage = await onPressed;
            if (errorMessage != null) {
              // This to fix: 'Do not use BuildContexts across async gaps'
              if (!mounted) return;
              snackBar(context, errorMessage);
            }
          },
          child: Text(title),
        ))
      ],
    );
  }
}

ahmnouira
  • 1,607
  • 13
  • 8
-1

using buildcontext across async method or class is now not work in current version of flutter rather than introduce context in async await use method callback for navigation and other purposes

-4

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
      }
Adam Smaka
  • 5,977
  • 3
  • 50
  • 55
  • 2
    No be careful of doing that! context might not be mounted so absolutely this does not guarantee to fix the issue and might cause the app to crash instead! – Bermjly Team Oct 15 '21 at 20:41
  • 1
    @BermjlyTeam I do not understand how this might not fix the issue. The BuildContext was used to fetch the Navigator instance. The resulting instance is now fetched. After the await, we must not use the BuildContext. We don't. We only use the Navigator instance. How does this not fix the issue in at least a StatelessWidget ? – Sxndrome Feb 10 '22 at 23:41
  • @Sxndrome imagine that your someFuture() waits 5 seconds (biiiiig task). During this time, you go back with Android back button or another implemented way. What //2 will do when someFuture() is finished ? Context before and after the future won't always be the same. – Guildem Mar 05 '22 at 10:17
  • 1
    This is very close to being the correct answer. It just needs a check before calling navigator.pop(). Change to `if (navigator.mounted) navigator.pop();` I would also store the *NavigatorState* instead of *BuildContext*: `MyCustomClass { final NavigatorState navigator; const MyCustomClass(this.navigator); }` Example instantiation: `final obj = MyCustomClass(Navigator.of(context));` – Jim Gomes May 13 '22 at 18:48
-11

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();
}
Izzamed
  • 83
  • 4
  • 21
    You can't just copy and paste directly from documentation and not at least cite your source: https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html – jaredbaszler Mar 28 '22 at 18:35