Schnodderbalken
Schnodderbalken

Reputation: 3467

Waiting asynchronously for Navigator.push() - linter warning appears: use_build_context_synchronously

In Flutter, all Navigator functions that push a new element onto the navigation stack return a Future as it's possible for the caller to wait for the execution and handle the result.

I make heavy use of it e. g. when redirecting the user (via push()) to a new page. As the user finishes the interaction with that page I sometimes want the original page to also pop():

onTap: () async {
  await Navigator.of(context).pushNamed(
    RoomAddPage.routeName,
    arguments: room,
  );

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

A common example is the usage of a bottom sheet with a button with a sensitive action (like deleting an entity). When a user clicks the button, another bottom sheet is opened that asks for the confirmation. When the user confirms, the confirm dialog is to be dismissed, as well as the first bottom sheet that opened the confirm bottom sheet.

So basically the onTap property of the DELETE button inside the bottom sheet looks like this:

onTap: () async {
  bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
  if (deleteConfirmed) {
    Navigator.of(context).pop();
  }
},

Everything is fine with this approach. The only problem I have is that the linter raises a warning: use_build_context_synchronously because I use the same BuildContext after the completion of an async function.

Is it safe for me to ignore / suspend this warning? But how would I wait for a push action on the navigation stack with a follow-up code where I use the same BuildContext? Is there a proper alternative? There has to be a possibility to do that, right?

PS: I can not and I do not want to check for the mounted property as I am not using StatefulWidget.

Upvotes: 56

Views: 19869

Answers (4)

Ricardo Emerson
Ricardo Emerson

Reputation: 906

You can do this:

onTap: () async {
  final navigatorContext = Navigator.of(context);

  await Navigator.of(context).pushNamed(
    RoomAddPage.routeName,
    arguments: room,
  );

  navigatorContext.pop();
},

Or

onTap: () async {
  await Navigator.of(context).pushNamed(
    RoomAddPage.routeName,
    arguments: room,
  );

  if (!context.mounted) return;

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

Upvotes: 1

Muees Abdul Rahiman
Muees Abdul Rahiman

Reputation: 11

Another way is using GlobalKey(), by using GlobalKey we can store buildContext of desired class as key and use it in navigator instead of context. Problem gets solved. for eg:-

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
final scaffoldKey = GlobalKey<ScaffoldState>(); // initialize globalkey
class MyApp extends StatelessWidget { 
@override
 Widget build(BuildContext context) {
  return MaterialApp(
   home: HomePage(),
   );
 }
}
class HomePage extends StatelessWidget {
 const HomePage({Key? key}) : super(key: key);
  @override
   Widget build(BuildContext context) {
   return Scaffold(
    key: scaffoldKey,   // use key property of scaffold widget and declare initialized key in it.
    appBar: AppBar(title: Text('Home Page')),
     body: Center(
     child: ElevatedButton(
     child: Text('Open Test Page'),
      onPressed: () {
      Navigator.of(context).push(
      MaterialPageRoute(builder: (_) => TestPage()),
       );
      }
     ),),);
    }
   } 



 class TestPage extends StatefulWidget { 
 @override
 State<TestPage> createState() => _TestPageState(); 
}


class _TestPageState extends State<TestPage> {
late final Timer timer; 
   @override
 void initState() {
  super.initState();
   timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
    setState(() {});
   });
  } 



 @override
 void dispose() {
   timer.cancel();
    super.dispose();
 }

@override
Widget build(BuildContext context) {
final time = DateTime.now().millisecondsSinceEpoch;
return Scaffold(
appBar: AppBar(title: Text('Test Page')),
body: Center(
child: Column(
children: [
Text('Current Time: $time'),
MySafeButton(),
],), ),);
}}
 class MySafeButton extends StatelessWidget { 
 const MySafeButton({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
 return ElevatedButton(
 child: Text('Open Dialog Then Pop Safely'),
 onPressed: () async {
  await showDialog(
   context: context,  
   builder: (_) => AlertDialog( 
title: Text('Dialog Title'),),);
 Navigator.pop(scaffoldKey.currentContext!)     // using of globalkey  context instead of BuildContext 

},); }

Upvotes: 1

ndelanou
ndelanou

Reputation: 921

Flutter ≥ 3.7 answer:

You can now use mounted on a StatelessWidget. This solution will not show linter warning:

onTap: () async {
  bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
  if (mounted && deleteConfirmed) {
    Navigator.of(context).pop();
  }
},

Alternatively, you can use context.mounted if outside of the widget.

Upvotes: 13

WSBT
WSBT

Reputation: 36323

Short answer:

It's NOT SAFE to always ignore this warning, even in a Stateless Widget.

A workaround in this case is to use the context before the async call. For example, find the Navigator and store it as a variable. This way you are passing the Navigator around, not passing the BuildContext around, like so:

onPressed: () async {
  final navigator = Navigator.of(context); // store the Navigator
  await showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: Text('Dialog Title'),
    ),
  );
  navigator.pop(); // use the Navigator, not the BuildContext
},

Long answer:

This warning essentially reminds you that, after an async call, the BuildContext might not be valid anymore. There are several reasons for the BuildContext to become invalid, for example, having the original widget destroyed during the waiting, could be one of the (leading) reasons. This is why it's a good idea to check if your stateful widget is still mounted.

However, we cannot check mounted on stateless widgets, but it absolutely does not mean they cannot become unmounted during the wait. If conditions are met, they can become unmounted too! For example, if their parent widget is stateful, and if their parent triggered a rebuild during the wait, and if somehow a stateless widget's parameter is changed, or if its key is different, it will be destroyed and recreated. This will make the old BuildContext invalid, and will result in a crash if you try to use the old context.

To demonstrate the danger, I created a small project. In the TestPage (Stateful Widget), I'm refreshing it every 500 ms, so the build function is called frequently. Then I made 2 buttons, both open a dialog then try to pop the current page (like you described in the question). One of them stores the Navigator before opening the dialog, the other one dangerously uses the BuildContext after the async call (like you described in the question). After clicking a button, if you sit and wait on the alert dialog for a few seconds, then exit it (by clicking anywhere outside the dialog), the safer button works as expected and pops the current page, while the other button does not.

The error it prints out is:

[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method. #0 Element._debugCheckStateIsActiveForAncestorLookup. (package:flutter/src/widgets/framework.dart:4032:9) #1 Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4046:6) #2 Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4093:12) #3 Navigator.of (package:flutter/src/widgets/navigator.dart:2736:40) #4 MyDangerousButton.build. (package:helloworld/main.dart:114:19)

Full source code demonstrating the problem:

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(
        child: ElevatedButton(
          child: Text('Open Test Page'),
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => TestPage()),
            );
          },
        ),
      ),
    );
  }
}

class TestPage extends StatefulWidget {
  @override
  State<TestPage> createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  late final Timer timer;

  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
      setState(() {});
    });
  }

  @override
  void dispose() {
    timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final time = DateTime.now().millisecondsSinceEpoch;
    return Scaffold(
      appBar: AppBar(title: Text('Test Page')),
      body: Center(
        child: Column(
          children: [
            Text('Current Time: $time'),
            MySafeButton(key: UniqueKey()),
            MyDangerousButton(key: UniqueKey()),
          ],
        ),
      ),
    );
  }
}

class MySafeButton extends StatelessWidget {
  const MySafeButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Text('Open Dialog Then Pop Safely'),
      onPressed: () async {
        final navigator = Navigator.of(context);
        await showDialog(
          context: context,
          builder: (_) => AlertDialog(
            title: Text('Dialog Title'),
          ),
        );
        navigator.pop();
      },
    );
  }
}

class MyDangerousButton extends StatelessWidget {
  const MyDangerousButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Text('Open Dialog Then Pop Dangerously'),
      onPressed: () async {
        await showDialog(
          context: context,
          builder: (_) => AlertDialog(
            title: Text('Dialog Title'),
          ),
        );
        Navigator.of(context).pop();
      },
    );
  }
}

Upvotes: 136

Related Questions