232

I have one StatefulWidget in Flutter with button, which navigates me to another StatefulWidget using Navigator.push(). On second widget I'm changing global state (some user preferences). When I get back from second widget to first, using Navigator.pop() the first widget is in old state, but I want to force it's reload. Any idea how to do this? I have one idea but it looks ugly:

  1. pop to remove second widget (current one)
  2. pop again to remove first widget (previous one)
  3. push first widget (it should force redraw)
bartektartanus
  • 15,284
  • 6
  • 74
  • 102
  • 1
    No answer, just a general comment: in my case, what brought me here looking for this would be solved with just having a sync method for shared_preferences where I'm guaranteed to get back an updated pref I wrote just a moment ago in another page. :\ Even using .then(...) isn't always getting me the pending-write updated data. – ChrisH Feb 15 '20 at 12:48
  • Just returned a value from the new page on pop, solved my issue. Refer https://flutter.dev/docs/cookbook/navigation/returning-data – Joe M May 24 '20 at 22:57

24 Answers24

151

There's a couple of things you could do here. @Mahi's answer while correct could be a little more succinct and actually use push rather than showDialog as the OP was asking about. This is an example that uses Navigator.push:

import 'package:flutter/material.dart';

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: Column(
        children: <Widget>[
          RaisedButton(
            onPressed: () => Navigator.pop(context),
            child: Text('back'),
          ),
        ],
      ),
    );
  }
}

class FirstPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new FirstPageState();
}

class FirstPageState extends State<FirstPage> {

  Color color = Colors.white;

  @override
  Widget build(BuildContext context) {
    return new Container(
      color: color,
      child: Column(
        children: <Widget>[
          RaisedButton(
            child: Text("next"),
            onPressed: () async {
              final value = await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => SecondPage()),
                ),
              );
              setState(() {
                color = color == Colors.white ? Colors.grey : Colors.white;
              });
            },
          ),
        ],
      ),
    );
  }
}

void main() => runApp(
      MaterialApp(
        builder: (context, child) => SafeArea(child: child),
        home: FirstPage(),
      ),
    );

However, there's another way to do this that might fit your use-case well. If you're using the global as something that affects the build of your first page, you could use an InheritedWidget to define your global user preferences, and each time they are changed your FirstPage will rebuild. This even works within a stateless widget as shown below (but should work in a stateful widget as well).

An example of inheritedWidget in flutter is the app's Theme, although they define it within a widget instead of having it directly building as I have here.

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: Column(
        children: <Widget>[
          RaisedButton(
            onPressed: () {
              ColorDefinition.of(context).toggleColor();
              Navigator.pop(context);
            },
            child: new Text("back"),
          ),
        ],
      ),
    );
  }
}

class ColorDefinition extends InheritedWidget {
  ColorDefinition({
    Key key,
    @required Widget child,
  }): super(key: key, child: child);

  Color color = Colors.white;

  static ColorDefinition of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(ColorDefinition);
  }

  void toggleColor() {
    color = color == Colors.white ? Colors.grey : Colors.white;
    print("color set to $color");
  }

  @override
  bool updateShouldNotify(ColorDefinition oldWidget) =>
      color != oldWidget.color;
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var color = ColorDefinition.of(context).color;

    return new Container(
      color: color,
      child: new Column(
        children: <Widget>[
          new RaisedButton(
              child: new Text("next"),
              onPressed: () {
                Navigator.push(
                  context,
                  new MaterialPageRoute(builder: (context) => new SecondPage()),
                );
              }),
        ],
      ),
    );
  }
}

void main() => runApp(
      new MaterialApp(
        builder: (context, child) => new SafeArea(
              child: new ColorDefinition(child: child),
            ),
        home: new FirstPage(),
      ),
    );

If you use inherited widget you don't have to worry about watching for the pop of the page you pushed, which will work for basic use-cases but may end up having problems in a more complex scenario.

Dave
  • 11,499
  • 5
  • 34
  • 46
rmtmckenzie
  • 37,718
  • 9
  • 112
  • 99
  • Excellent, the first case worked for me perfectly (the second one is perfect for a more complex situation). Using OnWillPopUp from the second page wasn't clear to me; and didn't even work at all. – cdsaenz Jul 20 '19 at 17:49
  • Hey, what if I want to update my list item. Let's say my 1st page contains list items and second page contains item detail. I am updating value of item and I want it updated back at list items. How to achieve that? – Aanal Shah Sep 05 '19 at 18:23
  • @AanalMehta I can give you a quick recommendation - either have a backend store (i.e. sqflite table or in-memory list) that is used from both the pages, and in the .then you could then force your widget to refresh somehow (I personally have used an incrementing counter that I change in setState before - it's not the cleanest solution but it works)... Or, you could pass the changes back to the original page in the .then function, make the modifications to your list, and then rebuild (but note that making changes to a list doesn't trigger a refresh, so once again use incrementing counter). – rmtmckenzie Sep 08 '19 at 13:31
  • @AanalMehta But if that doesn't help, I'd recommend you look for some other answers that may be more relevant (I'm fairly sure I've seen a few about lists etc), or ask a new question. – rmtmckenzie Sep 08 '19 at 13:34
  • @rmtmckenzie Actually I tried the above one solution. I applied the chan ges in .then but it won't get reflected. I guess now I have only one option to use providers. Anyway, thank you so much for your help. – Aanal Shah Sep 09 '19 at 04:45
  • @AanalMehta did you use a counter like I recommended? Modifying a list won't trigger a rebuild as flutter does comparisons like (list == list), and if you've modified the list that will report as true. You can either copy the list into a new list each time you make a change (which isn't as bad as it sounds really) or use a secondary piece of information that changes like the incrementing counter. But if you still do end up running into trouble open a question and someone will try to help =) – rmtmckenzie Sep 10 '19 at 07:41
  • Sir can you help https://stackoverflow.com/questions/59159175/connectionstate-always-waiting – Tony Dec 04 '19 at 05:28
  • Great answer! Second option is a really good solution if a user can navigate to a view in different ways (actions, back button, tabs or menus). BWT, inheritFromWidgetOfExactType is deprecated. – Andrei Volgin Feb 06 '20 at 04:20
  • Thanks for the great answer. The second case suits my work and I'm trying to use it BUT I'm again facing the same problem which is The widget tree in the `FirstPage` is not rebuilding when coming back from `SecondPage`, in case that `ColorDefinition` Class is doing fine. So do you think that there is a way to rebuild the widget tree after data changed in `SecondPage`? Something like adding conditions or something like that? any ways hasn't Flutter yet add a feature to rebuild the widget tree after popping since this question was asked? Any link would be appreciated too. Thanks in advance <3 – Taba Jul 27 '20 at 18:31
  • @Taba without context it's a little difficult to tell exactly what the issue is that you're having. It might be worth asking a new question. That being said, have you used setState(() => ....) in the callback? That's normally how you'd rebuild the widget tree. Note that changing an object (i.e. adding to a list) won't actually trigger rebuild, in that case the easiest thing to do is add a change counting integer. – rmtmckenzie Jul 28 '20 at 22:57
  • FIrst one works for me like a champ. Thanks for this answer – Raju Gupta Dec 11 '20 at 04:20
  • how to do the same if my second page is a stateless widget? – alex Dec 04 '21 at 02:41
  • In the ```ColorDefinition``` shouldn't the variables be final? I thought that ```InheritedWidget``` is immutable – Panagiss May 27 '22 at 19:22
  • 1
    @Panagiss that's probably recommended nowadays, but this question & answer are from 2018, well before immutable classes were part of dart or the recommended usage. – rmtmckenzie May 31 '22 at 06:44
88

Short answer:

Use this in 1st page:

Navigator.pushNamed(context, '/page2').then((_) => setState(() {}));

and this in 2nd page:

Navigator.pop(context);

There are 2 things, passing data from

  • 1st Page to 2nd

    Use this in 1st page

      // sending "Foo" from 1st
      Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    

    Use this in 2nd page.

      class Page2 extends StatelessWidget {
        final String string;
    
        Page2(this.string); // receiving "Foo" in 2nd
    
        ...
      }
    

  • 2nd Page to 1st

    Use this in 2nd page

      // sending "Bar" from 2nd
      Navigator.pop(context, "Bar");
    

    Use this in 1st page, it is the same which was used earlier but with little modification.

      // receiving "Bar" in 1st
      String received = await Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 1
    How can I reload route A when popping from route C using Navigator.popUntil method. – Vinoth Vino Jun 23 '20 at 10:03
  • 1
    @VinothVino There is no direct way of doing that just yet, you need to do some sort of workaround. – CopsOnRoad Jun 24 '20 at 06:41
  • @CopsOnRoad in activity TabBar to call the second tab from the dialog submit button click from Navigator.pushreplacement to redirect to the second tab. – s.j Dec 11 '20 at 11:28
  • don't understand when to call pop? – alex Dec 04 '21 at 02:26
  • @alex You call `Navigator.pop(...)` from second screen - the screen from where you want to come back and return the result. – CopsOnRoad Dec 04 '21 at 20:36
44

For me this seems to work:

Navigator.of(context).pushNamed("/myRoute").then((value) => setState(() {}));

Then simply call Navigator.pop() in the child.

marre
  • 699
  • 7
  • 7
  • this only works once, second time you go back to same route make change and pop doesn't refresh – Boss Nass Sep 25 '22 at 14:05
  • I have not noticed this behavior, but also have not worked with flutter since 1 year. Happy to hear if others also observed this! – marre Oct 04 '22 at 14:06
26

The Easy Trick is to use the Navigator.pushReplacement method

Page 1

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => Page2(),
  ),
);

Page 2

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => Page1(),
  ),
);
Matt Ke
  • 3,599
  • 12
  • 30
  • 49
allentiology
  • 904
  • 8
  • 7
  • 15
    This way you are losing the navigator stack. What about poping the screen with the back button? – encubos Oct 21 '20 at 16:28
17

Simply add .then((value) { setState(() {}); after Navigator.push on page1() just like below:

Navigator.push(context,MaterialPageRoute(builder: (context) => Page2())).then((value) { setState(() {});

Now when you use Navigator.pop(context) from page2 your page1 rebuild itself

Arslan Kaleem
  • 1,410
  • 11
  • 25
14
onTapFunction(BuildContext context) async {
    final reLoadPage = await Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => IdDetailsScreen()),
    );

    if (reLoadPage) {
        setState(() {});
    }
}

Now while doing Navigator.pop from second page to come back to first page just return some value which in my case is of bool type

onTap: () {
    Navigator.pop(context, true);
}
Tarun Jain
  • 411
  • 5
  • 17
13

You can use pushReplacement and specify the new Route

Sobhan Jachuck
  • 195
  • 2
  • 5
  • 13
    But you are losing the navigator stack. What if you pop the screen using the android back button? – encubos Oct 21 '20 at 16:25
  • Even pushreplacement wont rebuild context and state if it has been loaded before I guess – MohitC May 19 '22 at 14:09
11

my solution went by adding a function parameter on SecondPage, then received the reloading function which is being done from FirstPage, then executed the function before the Navigator.pop(context) line.

FirstPage

refresh() {
setState(() {
//all the reload processes
});
}

then on pushing to the next page...

Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage(refresh)),);

SecondPage

final Function refresh;
SecondPage(this.refresh); //constructor

then on before the navigator pop line,

widget.refresh(); // just refresh() if its statelesswidget
Navigator.pop(context);

Everything that needs to be reloaded from the previous page should be updated after the pop.

rodalyn camba
  • 141
  • 1
  • 2
11

This work really good, i got from this doc from flutter page: flutter doc

I defined the method to control navigation from first page.

_navigateAndDisplaySelection(BuildContext context) async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => AddDirectionPage()),
    );

    //below you can get your result and update the view with setState
    //changing the value if you want, i just wanted know if i have to  
    //update, and if is true, reload state

    if (result) {
      setState(() {});
    }
  }

So, i call it in a action method from a inkwell, but can be called also from a button:

onTap: () {
   _navigateAndDisplaySelection(context);
},

And finally in the second page, to return something (i returned a bool, you can return whatever you want):

onTap: () {
  Navigator.pop(context, true);
}
Pedro Molina
  • 1,101
  • 12
  • 12
7

I had a similar issue.

Please try this out:

In the First Page:

Navigator.push( context, MaterialPageRoute( builder: (context) => SecondPage()), ).then((value) => setState(() {}));

After you pop back from SecondPage() to FirstPage() the "then" statement will run and refresh the page.

Ayrix
  • 433
  • 5
  • 16
6

Put this where you're pushing to second screen (inside an async function)

Function f;
f= await Navigator.pushNamed(context, 'ScreenName');
f();

Put this where you are popping

Navigator.pop(context, () {
 setState(() {});
});

The setState is called inside the pop closure to update the data.

knoxgon
  • 1,070
  • 2
  • 15
  • 31
  • 2
    Where exactly do you pass `setState` as an argument? You are basically calling `setState`inside a closure. Not passing it as an argument. – knoxgon Apr 13 '20 at 16:29
  • 1
    This is the exact answer I was looking for. I basically wanted a completion handler for `Navigator.pop`. – David Chopin Oct 22 '20 at 20:21
4

You can pass back a dynamic result when you are popping the context and then call the setState((){}) when the value is true otherwise just leave the state as it is.

I have pasted some code snippets for your reference.

handleClear() async {
    try {
      var delete = await deleteLoanWarning(
        context,
        'Clear Notifications?',
        'Are you sure you want to clear notifications. This action cannot be undone',
      );
      if (delete.toString() == 'true') {
        //call setState here to rebuild your state.

      }
    } catch (error) {
      print('error clearing notifications' + error.toString());
             }
  }



Future<bool> deleteLoanWarning(BuildContext context, String title, String msg) async {

  return await showDialog<bool>(
        context: context,
        child: new AlertDialog(
          title: new Text(
            title,
            style: new TextStyle(fontWeight: fontWeight, color: CustomColors.continueButton),
            textAlign: TextAlign.center,
          ),
          content: new Text(
            msg,
            textAlign: TextAlign.justify,
          ),
          actions: <Widget>[
            new Container(
              decoration: boxDecoration(),
              child: new MaterialButton(
                child: new Text('NO',),
                onPressed: () {
                  Navigator.of(context).pop(false);
                },
              ),
            ),
            new Container(
              decoration: boxDecoration(),
              child: new MaterialButton(
                child: new Text('YES', ),
                onPressed: () {
                  Navigator.of(context).pop(true);
                },
              ),
            ),
          ],
        ),
      ) ??
      false;
}

Regards, Mahi

Mahi
  • 5,726
  • 13
  • 31
  • 41
  • I'm not sure I understand how to do the second part. First is just simply `Navigator.pop(context, true)` , right? But how I can obtain this `true` value? Using `BuildContext`? – bartektartanus Apr 12 '18 at 20:07
  • Not working for me. I've used the result from push, called setState and got `setState() called after dispose(): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. ....` – bartektartanus Apr 12 '18 at 20:24
  • hmm, this is interesting did you use `if(!mounted) return;` before every call to `setState((){});` this way you can avoid updating the disposed widgets or widgets that are no longer active. – Mahi Apr 12 '18 at 20:27
  • No error but also it is not working as I predicted ;) – bartektartanus Apr 12 '18 at 20:29
  • I have the same issue as @bartektartanus. If I add if !mounted before setState, my state will never be set. It seems simple but I have not figured it out after a few hours. – Swift Apr 06 '20 at 23:59
2

Needed to force rebuild of one of my stateless widgets. Did't want to use stateful. Came up with this solution:

await Navigator.of(context).pushNamed(...);
ModalRoute.of(enclosingWidgetContext);

Note that context and enclosingWidgetContext could be the same or different contexts. If, for example, you push from inside StreamBuilder, they would be different.

We don't do anything here with ModalRoute. The act of subscribing alone is enough to force rebuild.

CKK
  • 190
  • 13
2

If you are using an alert dialog then you can use a Future that completes when the dialog is dismissed. After the completion of the future you can force widget to reload the state.

First page

onPressed: () async {
    await showDialog(
       context: context,
       builder: (BuildContext context) {
            return AlertDialog(
                 ....
            );
       }
    );
    setState(() {});
}

In Alert dialog

Navigator.of(context).pop();
Nimna Perera
  • 1,015
  • 10
  • 16
2

In flutter 2.5.2 this is worked for me also it works for updating a list

Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => SecondPage()))
    .then((value) => setState(() {}));

then in the second page I just code this

Navigator.pop(context);

I have a ListView in fist page which is display a list[] data, the second page was updating the data for my list[] so the above code works for me.

Abdullah Bahattab
  • 612
  • 1
  • 16
  • 32
1

This simple code worked for me to go to the root and reload the state:

    ...
    onPressed: () {
         Navigator.of(context).pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
                },
    ...
Juanma Menendez
  • 17,253
  • 7
  • 59
  • 56
1

In short, you should make the widget watch the state. You need state management for this.

My method is based on Provider explained in Flutter Architecture Samples as well as Flutter Docs. Please refer to them for more concise explanation but more or less the steps are :

  • Define your state model with states that the widget needs to observe.

You could have multiple states say data and isLoading, to wait for some API process. The model itself extends ChangeNotifier.

  • Wrap the widgets that depend on those states with watcher class.

This could be Consumer or Selector.

  • When you need to "reload", you basically update those states and broadcast the changes.

For state model the class would look more or less as follows. Pay attention to notifyListeners which broadcasts the changes.

class DataState extends ChangeNotifier{

  bool isLoading;
  
  Data data;

  Future loadData(){
    isLoading = true;
    notifyListeners();

    service.get().then((newData){
      isLoading = false;
      data = newData;
      notifyListeners();
    });
  }
  
}

Now for the widget. This is going to be very much a skeleton code.

return ChangeNotifierProvider(

  create: (_) => DataState()..loadData(),
      
  child: ...{
    Selector<DataState, bool>(

        selector: (context, model) => model.isLoading,

        builder: (context, isLoading, _) {
          if (isLoading) {
            return ProgressBar;
          }

          return Container(

              child: Consumer<DataState>(builder: (context, dataState, child) {

                 return WidgetData(...);

              }
          ));
        },
      ),
  }
);

Instance of the state model is provided by ChangeNotifierProvider. Selector and Consumer watch the states, each for isLoading and data respectively. There is not much difference between them but personally how you use them would depend on what their builders provide. Consumer provides access to the state model so calling loadData is simpler for any widgets directly underneath it.

If not then you can use Provider.of. If we'd like to refresh the page upon return from the second screen then we can do something like this:

await Navigator.push(context, 
  MaterialPageRoute(
    builder: (_) {
     return Screen2();
));

Provider.of<DataState>(context, listen: false).loadData();

inmyth
  • 8,880
  • 4
  • 47
  • 52
1

For me worked:

...
onPressed: (){pushUpdate('/somePageName');}
...

pushUpdate (string pageName) async {      //in the same class
  await pushPage(context, pageName);
  setState(() {});
}


//---------------------------------------------
//general sub
pushPage (context, namePage) async {
  await Navigator.pushNamed(context, namePage);
}

In this case doesn't matter how you pop (with button in UI or "back" in android) the update will be done.

1

Very simply use "then" after you push, when navigator pops back it will fire setState and the view will refresh.

Navigator.push(blabla...).then((value) => setState(() {}))

1
// Push to second screen
 await Navigator.push(
   context,
   CupertinoPageRoute(
    builder: (context) => SecondScreen(),
   ),
 );

// Call build method to update any changes
setState(() {});
Davies
  • 135
  • 1
  • 1
  • 7
  • Not at all a good solution, since ``` Navigator.push``` would add the current page to the history and in the case of popping the context again, (user goes back home for example) it would take you to the same page again. – Santiago Mar 29 '22 at 19:24
0

Use setstate in your navigation push code.

Navigator.push(context, MaterialPageRoute(builder: (context) => YourPage())).then((value) {
  setState(() {
    // refresh state
  });
});
Laxman Kumar
  • 134
  • 6
0

This simple code goes to the root and reloads the state even without setState:

Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (context) => MainPage()),  (Route<dynamic> route) => false,);   //// this MainPage is your page to refresh
0

Another stupid but effective solution would be to create inside the first widget of a widget tree a function that all it does is call the setState() method:

class _FirstWidgetState extends State<FirstWidget> { 

    reBuild() {
      setState(() {});
    }
    
    @override
    Widget build(BuildContext context) {
      ...
    }
}

Pass it down to as many widgets as you want:

@override
Widget build(BuildContext context) {
  return SecondWidget(
   reBuild,
  );
}

Store it as a final variable of type Function:

class SecondWidget extends StatelessWidget {
  final Function buildFunction;

  const SecondWidget(this.buildFunction, {super.key});

  @override
  Widget build(BuildContext context) {
    ...
  }
}

And then, once you're done, you call the function so that it will rebuild the initial widget:

await Navigator.of(context).pushNamed(
        ThirdWidget.routeName,
      ).then((value) => buildFunction());

This helped me in a more complex situation compared to then ones I saw in other answers, so I hope this will be helpful to someone :)

0

To avoid too much flickering and prevent messing up with the routes, it can be chosen to just rebuild the current context. However this does neither do popping nor pushing, it is a refresh of the current context only - and it might not be sufficient in regards to the topic of this thread.

  void refresh() {
    if (mounted) {
      Navigator.of(context).build(context);
    }
  }

The guard for mounted is included in case any async calls are made to make sure that the context is mounted and ready prior to the refresh.

Jess N
  • 11
  • 2