【问题标题】:Force Flutter navigator to reload state when popping弹出时强制 Flutter 导航器重新加载状态
【发布时间】:2018-09-23 02:22:32
【问题描述】:

我在 Flutter 中有一个带有按钮的 StatefulWidget,它使用 Navigator.push() 将我导航到另一个 StatefulWidget。在第二个小部件上,我正在更改全局状态(一些用户偏好)。当我从第二个小部件返回到第一个小部件时,使用Navigator.pop() 第一个小部件处于旧状态,但我想强制它重新加载。知道怎么做吗?我有一个想法,但看起来很丑:

  1. pop 删除第二个小部件(当前一个)
  2. 再次弹出以删除第一个小部件(上一个)
  3. 推送第一个小部件(它应该强制重绘)

【问题讨论】:

  • 没有答案,只是一般性评论:在我的情况下,让我来这里寻找这个的原因将通过一个用于 shared_preferences 的同步方法来解决,我保证可以取回更新的首选项我刚才写在另一页。 :\ 即使使用 .then(...) 也不总是让我得到等待写入的更新数据。
  • 刚刚从弹出的新页面返回了一个值,解决了我的问题。参考flutter.dev/docs/cookbook/navigation/returning-data

标签: flutter dart navigation


【解决方案1】:

您可以在这里做几件事。 @Mahi 的答案虽然正确,但可能更简洁一些,并且实际上使用 push 而不是 OP 所询问的 showDialog 。这是一个使用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(),
      ),
    );

但是,还有另一种方法可以很好地适合您的用例。如果您使用global 作为影响第一页构建的东西,您可以使用InheritedWidget 来定义您的全局用户偏好,并且每次更改它们时您的FirstPage 都会重新构建。这甚至可以在无状态小部件中使用,如下所示(但也应该在有状态小部件中使用)。

flutter 中的inheritedWidget 的一个例子是应用程序的主题,尽管他们在小部件中定义它,而不是像我在这里那样直接构建它。

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

如果您使用继承的小部件,则不必担心查看您推送的页面的弹出,这适用于基本用例,但最终可能会在更复杂的场景中出现问题。

【讨论】:

  • 非常好,第一个案例对我来说非常有效(第二个非常适合更复杂的情况)。我不清楚从第二页使用 OnWillPopUp ;甚至根本没有工作。
  • 嘿,如果我想更新我的列表项怎么办。假设我的第一页包含列表项,第二页包含项目详细信息。我正在更新项目的价值,我希望它在列表项目中更新。如何做到这一点?
  • @AanalMehta 我可以给你一个快速的建议 - 要么有一个后端存储(即 sqflite 表或内存列表),用于两个页面,然后在 .then 你可以强制你的小部件以某种方式刷新(我个人使用了一个递增计数器,我之前在 setState 中更改了它 - 这不是最干净的解决方案,但它有效)......或者,你可以将更改传递回 .then 函数中的原始页面,对列表进行修改,然后重新构建(但请注意,对列表进行更改不会触发刷新,因此再次使用递增计数器)。
  • @AanalMehta 但如果这没有帮助,我建议您寻找一些可能更相关的其他答案(我很确定我已经看过一些关于列表等的答案),或提出一个新问题。
  • @rmtmckenzie 实际上我尝试了上述一种解决方案。我在 .then 中应用了更改,但它不会得到反映。我想现在我只有一个选择来使用提供程序。无论如何,非常感谢您的帮助。
【解决方案2】:

简答:

在第一页使用这个:

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

这在第二页:

Navigator.pop(context);

有两件事,从

传递数据
  • 第一页到第二页

    在第一页使用这个

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

    在第二页使用这个。

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

  • 第二页到第一页

    在第二页使用这个

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

    在第一页使用这个,它与之前使用的相同,但几乎没有修改。

      // receiving "Bar" in 1st
      String received = await Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    

【讨论】:

  • 使用 Navigator.popUntil 方法从路由 C 弹出时如何重新加载路由 A。
  • @VinothVino 目前还没有直接的方法,你需要做一些解决方法。
  • @CopsOnRoad 在活动 TabBar 中调用对话框中的第二个选项卡提交按钮单击从 Navigator.pushreplacement 重定向到第二个选项卡。
  • 不明白什么时候调用pop?
  • @alex 您从第二个屏幕调用Navigator.pop(...) - 您想要返回并返回结果的屏幕。
【解决方案3】:

对我来说这似乎有效:

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

然后简单地在孩子中调用Navigator.pop()

【讨论】:

    【解决方案4】:

    Easy Trick 是使用 Navigator.pushReplacement 方法

    第 1 页

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

    第 2 页

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

    【讨论】:

    • 这样你会丢失导航器堆栈。用返回按钮弹出屏幕怎么样?
    【解决方案5】:

    您可以使用 pushReplacement 并指定新的 Route

    【讨论】:

    • 但是您正在丢失导航器堆栈。如果你使用 android 的后退按钮弹出屏幕怎么办?
    【解决方案6】:

    只需在 page1() 上的 Navigator.push 之后添加 .then((value) { setState(() {});,如下所示:

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

    现在,当您从 page2 使用 Navigator.pop(context) 时,您的 page1 会自行重建

    【讨论】:

      【解决方案7】:

      我的解决方案是在 SecondPage 上添加一个函数参数,然后接收正在从 FirstPage 完成的重新加载函数,然后在 Navigator.pop(context) 行之前执行该函数。

      首页

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

      然后在推送到下一页...

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

      第二页

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

      然后在导航弹出行之前,

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

      所有需要从上一页重新加载的内容都应该在弹出后更新。

      【讨论】:

        【解决方案8】:

        这项工作真的很好,我从颤动页面的这个文档中得到:flutter doc

        我定义了从第一页控制导航的方法。

        _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(() {});
            }
          }
        

        所以,我用墨水池的动作方法调用它,但也可以从按钮调用:

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

        最后在第二页,返回一些东西(我返回了一个布尔值,你可以返回任何你想要的东西):

        onTap: () {
          Navigator.pop(context, true);
        }
        

        【讨论】:

        • 你甚至可以只 await Navigator.... 并在 pop 中省略结果值。
        【解决方案9】:
        onTapFunction(BuildContext context) async {
            final reLoadPage = await Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => IdDetailsScreen()),
            );
        
            if (reLoadPage) {
                setState(() {});
            }
        }
        

        现在,在从第二页执行 Navigator.pop 以返回第一页时,只需返回一些值,在我的情况下,如果 bool 键入

        onTap: () {
            Navigator.pop(context, true);
        }
        

        【讨论】:

        • 正是我需要的,因为如果我使用.then,返回类型将是bool,所以我将无法调用setState
        【解决方案10】:

        把它放在你要推送到第二个屏幕的地方(在异步函数内)

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

        把这个放在你要弹出的地方

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

        setStatepop 闭包内被调用以更新数据。

        【讨论】:

        • 您究竟在哪里传递setState 作为参数?您基本上是在闭包内调用setState。不将其作为参数传递。
        • 这是我正在寻找的确切答案。我基本上想要Navigator.pop 的完成处理程序。
        【解决方案11】:

        您可以在弹出上下文时传回dynamic result,然后在值为true 时调用setState((){}),否则保持原样。

        我已经粘贴了一些代码 sn-ps 供您参考。

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

        问候, 鲯鳅

        【讨论】:

        • 我不确定我是否理解如何做第二部分。首先只是简单的 Navigator.pop(context, true) ,对吧?但是我怎样才能获得这个true 值呢?使用BuildContext?
        • 不适合我。我使用了 push 的结果,称为 setState 并得到了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. ....
        • hmm,有趣的是,您在每次调用 setState((){}); 之前是否使用了 if(!mounted) return;,这样您可以避免更新已处理的小部件或不再活动的小部件。
        • 没有错误,但它也没有像我预测的那样工作;)
        • 我和@bartektartanus 有同样的问题。如果我在 setState 之前添加 if !mounted,我的状态将永远不会被设置。看起来很简单,但几个小时后我还没弄明白。
        【解决方案12】:

        需要强制重建我的一个无状态小部件。不想使用有状态的。想出了这个解决方案:

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

        注意 context 和 enclosingWidgetContext 可以是相同或不同的上下文。例如,如果您从 StreamBuilder 内部推送,它们会有所不同。

        我们在这里不对 ModalRoute 做任何事情。仅订阅的行为就足以强制重建。

        【讨论】:

          【解决方案13】:

          如果您使用的是警报对话框,那么您可以使用在对话框关闭时完成的 Future。未来完成后你可以强制小部件重新加载状态。

          首页

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

          在警报对话框中

          Navigator.of(context).pop();
          

          【讨论】:

            【解决方案14】:

            这个简单的代码让我可以转到根目录并重新加载状态:

                ...
                onPressed: () {
                     Navigator.of(context).pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
                            },
                ...
            

            【讨论】:

              【解决方案15】:

              为我工作:

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

              在这种情况下,无论您如何弹出(在 UI 中使用按钮或在 android 中使用“返回”),更新都会完成。

              【讨论】:

                【解决方案16】:

                我也有类似的问题。

                请试试这个:

                在第一页:

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

                在您从 SecondPage() 弹回 FirstPage() 后,“then”语句将运行并刷新页面。

                【讨论】:

                • 简约优雅
                【解决方案17】:

                简而言之,您应该让小部件监视状态。您需要为此进行状态管理。

                我的方法基于Flutter Architecture SamplesFlutter Docs 中解释的Provider。请参考它们以获得更简洁的解释,但或多或​​少的步骤是:

                • 使用小部件需要观察的状态定义您的状态模型。

                您可以有多个状态,例如 dataisLoading,以等待某个 API 进程。模型本身扩展了ChangeNotifier

                • 使用 watcher 类包装依赖于这些状态的小部件。

                这可能是ConsumerSelector

                • 当您需要“重新加载”时,您基本上会更新这些状态并广播更改。

                对于状态模型,该类或多或少如下所示。注意广播变化的notifyListeners

                class DataState extends ChangeNotifier{
                
                  bool isLoading;
                  
                  Data data;
                
                  Future loadData(){
                    isLoading = true;
                    notifyListeners();
                
                    service.get().then((newData){
                      isLoading = false;
                      data = newData;
                      notifyListeners();
                    });
                  }
                  
                }
                

                现在是小部件。这将是一个非常骨架的代码。

                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(...);
                
                              }
                          ));
                        },
                      ),
                  }
                );
                

                状态模型的实例由 ChangeNotifierProvider 提供。 Selector 和 Consumer 观察状态,分别对应 isLoadingdata。有not much difference between them,但您个人如何使用它们取决于他们的构建者提供的内容。消费者提供对状态模型的访问,因此对于直接位于其下方的任何小部件,调用 loadData 更简单。

                如果没有,那么您可以使用Provider.of。如果我们想在从第二个屏幕返回时刷新页面,那么我们可以这样做:

                await Navigator.push(context, 
                  MaterialPageRoute(
                    builder: (_) {
                     return Screen2();
                ));
                
                Provider.of<DataState>(context, listen: false).loadData();
                
                

                【讨论】:

                  【解决方案18】:

                  在你推后很简单地使用“then”,当导航器弹出时它会触发 setState 并且视图会刷新。

                  Navigator.push(blabla...).then((value) =&gt; setState(() {}))

                  【讨论】:

                    【解决方案19】:

                    在颤振 2.5.2 中,这对我有用,也适用于更新列表

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

                    然后在第二页我只写代码

                    Navigator.pop(context);
                    

                    我在第一页有一个ListView,它显示了一个list[] 数据,第二页正在更新我的list[] 的数据,所以上面的代码对我有用。

                    【讨论】:

                      【解决方案20】:
                      // Push to second screen
                       await Navigator.push(
                         context,
                         CupertinoPageRoute(
                          builder: (context) => SecondScreen(),
                         ),
                       );
                      
                      // Call build method to update any changes
                      setState(() {});
                      

                      【讨论】:

                        【解决方案21】:
                        navigtor.pop(context,(){
                        setState((){}
                        }))
                        

                        这将弹出屏幕并刷新上一个屏幕

                        【讨论】:

                          猜你喜欢
                          • 2019-09-05
                          • 1970-01-01
                          • 2023-03-28
                          • 1970-01-01
                          • 2021-06-03
                          • 1970-01-01
                          • 1970-01-01
                          • 2021-04-10
                          • 2016-01-31
                          相关资源
                          最近更新 更多