【问题标题】:Navigation problem with FutureBuilder and MaterialAppFutureBuilder 和 MaterialApp 的导航问题
【发布时间】:2021-03-02 06:33:06
【问题描述】:

我的应用有一个被计算为 Future 的状态。 例如,它包含主题颜色,因为我想在导航时更改颜色。 我在等待数据时尝试显示进度指示器。

但我不能让它工作。 要么是 Navigator.push 不工作且应用栏丢失,要么我没有进度指示器和路线错误...

这是一个代码 sn-p。

import 'package:flutter/material.dart';

void main() => runApp(Test());

class Test extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _TestState();
}

class _TestState extends State<Test> {
  Future<Color> color = Model.getColor();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Color>(
      future: color,
      builder: (context, snapshot) {
        if (snapshot.hasError) throw snapshot.error;
        if (snapshot.connectionState != ConnectionState.done) {
          if (false) {
            // Navigation not working. App bar missing.
            return Material(child: Center(child: CircularProgressIndicator()));
          } else {
            // Progress not working. Screen flickering.
            return MaterialApp(home: _buildWait());
          }
        }
        var app = MaterialApp(
          theme: ThemeData(primaryColor: snapshot.data),
          home: _buildPage(),
          // ERROR: The builder for route "/" returned null.
          // routes: {'/': (_) => _buildPage()},
        );
        return app;
      },
    );
  }

  Widget _buildPage() {
    return Builder(
      builder: (context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: RaisedButton(
              child: Text('Push'),
              onPressed: () {
                setState(() {
                  color = Model.getColor();
                });
                Navigator.push(context, MaterialPageRoute(builder: (context) {
                  return Scaffold(appBar: AppBar());
                }));
              },
            ),
          ),
        );
      },
    );
  }
}

Widget _buildWait() {
  return Scaffold(
    appBar: AppBar(title: Text('Wait...')),
    body: Center(child: CircularProgressIndicator()),
  );
}

class Model {
  static final _colors = [Colors.red, Colors.green, Colors.amber];
  static int _index = 0;
  static Future<Color> getColor() {
    return Future.delayed(Duration(seconds: 2), () => _colors[_index++ % _colors.length]);
  }
}

预期结果:当我按下按钮导航到新路线时,它应该显示一个进度指示器,然后是具有不同主题颜色的新屏幕。

【问题讨论】:

  • 您使用了未命名的路线。实际上,每个named routeunnamed route 都应该是MaterialApp 的后代,这意味着您不能像以前那样更改MaterialApp,因为所有路由都是从中拆分出来的。在此处查看named route 的示例:flutter.dev/docs/cookbook/navigation/named-routes
  • @yellowgray 使用命名路线我有错误The builder for route "/" returned null.
  • 嗨@Patrick,我从你的问题中了解到,假设你有3个屏幕A、B和C。都有AppBar,你希望每个屏幕都有不同的应用栏颜色。正确的?假设用户从 A 导航到 B,B 将具有不同的 NavBar 颜色、加载小部件和 AppBar 上的后退按钮。对吗?

标签: flutter dart navigation future flutter-navigation


【解决方案1】:

现在尝试以下操作。尝试单独制作一个根小部件,因为根小部件始终存在。您不希望完整的 UI 路由保留在内存中。还将下一条路线作为单独的小部件。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Test(),
    );
  }
}

class Test extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _TestState();
}

class _TestState extends State<Test> {
  Future<Color> color = Model.getColor();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Color>(
      future: color,
      builder: (context, snapshot) {
        if (snapshot.hasError) return Center(child: Text("An Error Occurred"));
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildWait();
        }
        var app = Theme(
          data: ThemeData(primaryColor: snapshot.data),
          child: _buildPage(),
        );
        return app;
      },
    );
  }

  Widget _buildPage() {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: RaisedButton(
          child: Text('Push'),
          onPressed: () {
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return NextRoute();
            }));
          },
        ),
      ),
    );
  }
}

Widget _buildWait() {
  return Scaffold(
    appBar: AppBar(title: Text('Wait...')),
    body: Center(child: CircularProgressIndicator()),
  );
}

class Model {
  static final _colors = [Colors.red, Colors.green, Colors.amber];
  static int _index = 0;
  static Future<Color> getColor() {
    return Future.delayed(
        Duration(seconds: 2), () => _colors[_index++ % _colors.length]);
  }
}

class NextRoute extends StatefulWidget {
  NextRoute({Key key}) : super(key: key);

  @override
  _NextRouteState createState() => _NextRouteState();
}

class _NextRouteState extends State<NextRoute> {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Color>(
        future: Model.getColor(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(
              child: Text("An Error Occurred"),
            );
          }

          if (snapshot.connectionState == ConnectionState.waiting) {
            return _buildWait();
          }

          return Theme(
            data: ThemeData(primaryColor: snapshot.data),
            child: Scaffold(
              appBar: AppBar(),
            ),
          );
        });
  }
}

【讨论】:

  • 谢谢,但有 2 个问题:1) 断言失败:Theme() 中的“数据!= null”。 2) 有 2 个嵌套的脚手架。
  • @Patrick 我更新了我的代码,现在试试。删除不需要的 Theme 小部件,也是最顶层的脚手架。
  • 它不起作用,对不起。我编辑了问题以添加预期结果。
  • 好的,您想在导航时更改颜色。并在同一路线上使用颜色?还是在下一条路线?
  • 我想要下一条路线使用不同的颜色。
【解决方案2】:

所以我想我已经找到了错误,实际上你必须在runApp() 中有一个MaterialApp 作为root。

所以你不能在FutureBuilder 里面有MaterialApp 您可以做的是将MaterialApp 设为根小部件并拥有一个默认主屏幕,在其构建方法中您可以拥有您的FutureBuilder,但同样不要在其中包含materialApp,直接使用Scaffold。

编辑: 回答有关应用主题的问题

您可以使用切换主题 themedarkTheme 在 materialApp 和控制 themeModeProvider 或任何其他状态管理方法。

MaterialApp(
          title: 'Flutter Tutorials',
          debugShowCheckedModeBanner: false,
          theme: AppTheme.lightTheme,
          darkTheme: AppTheme.darkTheme,
          themeMode: appState.isDarkModeOn ? ThemeMode.dark : ThemeMode.light,
          home: ThemeDemo(),
        );

这里有几种方法可以做到这一点,这是我发现的另一种custom theme app

试试这个,如果不让我知道,它会起作用

【讨论】:

  • 让我知道这是否解决了问题@patrick
  • 我需要更改应用的主题颜色,所以我不能让 MaterialApp 作为 root,我必须有一个父小部件来保持主题状态。
【解决方案3】:

我认为你可以这样做:

  1. 将所有 Future 数据/FutureBuilder 移动到不同的 Stateless/Stateful Widget 并使用 Theme 类覆盖主题颜色

    class SecondPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        var color = Theme.of(context).primaryColor;
    
        return FutureBuilder(
          future: Model.getColor(),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return Theme(
                data: ThemeData(primaryColor: color), 
                child: _buildWait(),
              );
            } else {
              color = snapshot.data;
              return Theme(
                data: ThemeData(primaryColor: color),
                child: Scaffold(
                  appBar: AppBar(),
                ),
              );
            }
          },
        );
      }
    }
    
  2. 首页使用局部变量存储颜色

    ...
    Color _primaryColor;
    
    @override
    void initState() {
      _primaryColor = Theme.of(context).primaryColor;
      super.initState();
    }
    
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        theme: ThemeData(primaryColor: _primaryColor),
        home: _buildPage(),
      );
    }
    ...
    
  3. 如果您希望第一页同时更新主题,您应该使用某种方法在小部件之间共享数据(例如提供程序)。我用简单的方法来捕捉自定义的返回值

    // First Page
    // use "then" can get the return value from the other route
    ...
    onPressed: () {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return SecondPage();
      })).then((color) {
        setState(() {
          _primaryColor = color;
        });
      });
    },
    ...
    
    // Second Page
    // WillPopScope can catch navigator pop event
    ...
    return WillPopScope(
      onWillPop: () async {
        Navigator.of(context).pop(color);
        return Future.value(false);
      },
      child: FutureBuilder(
       ...
    

如果您在尝试更改主题时不需要使用Routing, 我可以提供一个简单的解决方案,通过Theme类更改主题数据

ThemeData currentTheme;

@override
void initState() {
  super.initState();
  currentTheme = Theme.of(context);
}


@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: FutureBuilder<Color>(
      future: color,
      builder: (context, snapshot) {
        Widget child;
        if (snapshot.hasError) throw snapshot.error;
        if (snapshot.connectionState != ConnectionState.done) {
          child =  Scaffold(
            appBar: AppBar(),
            body: const Center(child: CircularProgressIndicator()),
          );
        }else{
          currentTheme = currentTheme.copyWith(primaryColor: snapshot.data);
          child = _buildPage();
        }
        return Theme(
          data: currentTheme,
          child: child,
        );
      },
    ),
  );
}

这里是Themes for part of an application的文档

【讨论】:

  • 是的,我需要为应用程序的不同路线设置不同的主题,对不起。
  • 你能在路线变更过程中描述更多细节what happen/expected result吗?更改路线之前或之后是否准备好未来的数据?
【解决方案4】:

我已经删除了_buildPage 小部件中的return Builder 部分,它似乎可以工作。它还在AppBar 中显示CircularProgressIndıcatorWait... 文本。

这里是修改后的代码:

Widget _buildPage() {
  return Scaffold(
    appBar: AppBar(),
      body: Center(
        child: RaisedButton(
          child: Text('Push'),
          onPressed: () {
            setState(() {
              color = Model.getColor();
            });
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return Scaffold(appBar: AppBar());
            }));
        },
      ),
    ),
  );
}

【讨论】:

  • 很遗憾导航不起作用:没有返回按钮。
  • 没关系。我将尝试另一种解决方案,当我找到它时,我将编辑我的答案。
  • 你是想改变 appBar 颜色还是整个主题颜色?
  • 整个主题。
  • 我认为我们不需要延迟颜色。我们为什么不延迟循环指示器呢?例如,显示指示器两秒钟,然后显示颜色。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-06-20
  • 1970-01-01
  • 2020-02-07
  • 1970-01-01
  • 2020-10-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多