FutureBuilder 和 MaterialApp 的导航问题

Navigation problem with FutureBuilder and MaterialApp

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

但我做不到。 Navigator.push 无法正常工作并且应用栏丢失,或者我没有进度指示器和路线错误...

这是一个代码片段。

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

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

我删除了 _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());
            }));
        },
      ),
    ),
  );
}

所以我想我发现了错误,实际上你必须在 runApp() 里面有一个 MaterialApp 作为 root。

所以 FutureBuilder 里面不能有 MaterialApp 您可以做的是使 MaterialApp 成为根小部件并具有默认的主屏幕,并且在其构建方法中您可以拥有 FutureBuilder 但同样不要在其中包含 materialApp 只需使用脚手架直接。

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

您可以通过使用切换主题 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

尝试一下它会起作用,如果不告诉我

现在尝试以下操作。尝试单独制作一个根小部件,因为根小部件始终存在。您不希望完整的 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(),
            ),
          );
        });
  }
}

我想你可以这样做:

  1. 将所有 Future data/FutureBuilder 移动到不同的 Stateless/Stateful 小部件中并使用 Theme class

    覆盖主题颜色
    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. 如果你想让第一页同时更新主题,你应该使用一些方法在widget之间共享数据(例如Provider)。我使用简单的方法来捕获自定义 return 值

    // 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 class

更改主题数据
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

的文档