【问题标题】:Implementing Multiple Pages into a Single Page using Navigation and a Stack使用导航和堆栈将多个页面实现为单个页面
【发布时间】:2020-07-09 03:05:19
【问题描述】:

在 Flutter 中,我想在 android 中使用 Fragment 制作屏幕,在我的代码中,我尝试将每个屏幕替换为当前屏幕,就像在 android 中使用 Fragment.replecae 一样,我使用了 HookProvider 和我的代码在单击按钮以在它们之间切换时工作正常,但我无法实现回栈,这意味着当我单击手机上的 Back 按钮时,我的代码应该显示我存储到 _backStack 变量中的最新屏幕,此屏幕之间的每个开关我都将当前屏幕索引存储到此变量中。

如何在我的示例代码中从这个堆栈中解决?

// Switch Between screens:
DashboardPage(), UserProfilePage(), SearchPage()
------------->   ------------->     ------------->
// When back from stack:
                      DashboardPage(), UserProfilePage(), SearchPage()
Exit from application <--------------  <----------------  <-----------

我使用了Hook,我想用这个库功能实现这个动作

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MultiProvider(providers: [
    Provider.value(value: StreamBackStackSupport()),
    StreamProvider<homePages>(
      create: (context) =>
          Provider.of<StreamBackStackSupport>(context, listen: false)
              .selectedPage,
    )
  ], child: StartupApplication()));
}

class StartupApplication extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BackStack Support App',
      home: MainBodyApp(),
    );
  }
}

class MainBodyApp extends HookWidget {
  final List<Widget> _fragments = [
    DashboardPage(),
    UserProfilePage(),
    SearchPage()
  ];
  List<int> _backStack = [0];
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BackStack Screen'),
      ),
      body: WillPopScope(
        // ignore: missing_return
        onWillPop: () {
          customPop(context);
        },
        child: Container(
          child: Column(
            children: <Widget>[
              Consumer<homePages>(
                builder: (context, selectedPage, child) {
                  _currentIndex = selectedPage != null ? selectedPage.index : 0;
                  _backStack.add(_currentIndex);
                  return Expanded(child: _fragments[_currentIndex]);
                },
              ),
              Container(
                width: double.infinity,
                height: 50.0,
                padding: const EdgeInsets.symmetric(horizontal: 15.0),
                color: Colors.indigo[400],
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    RaisedButton(
                      onPressed: () => Provider.of<StreamBackStackSupport>(
                              context,
                              listen: false)
                          .switchBetweenPages(homePages.screenDashboard),
                      child: Text('Dashboard'),
                    ),
                    RaisedButton(
                      onPressed: () => Provider.of<StreamBackStackSupport>(
                              context,
                              listen: false)
                          .switchBetweenPages(homePages.screenProfile),
                      child: Text('Profile'),
                    ),
                    RaisedButton(
                      onPressed: () => Provider.of<StreamBackStackSupport>(
                              context,
                              listen: false)
                          .switchBetweenPages(homePages.screenSearch),
                      child: Text('Search'),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void navigateBack(int index) {
    useState(() => _currentIndex = index);
  }

  void customPop(BuildContext context) {
    if (_backStack.length - 1 > 0) {
      navigateBack(_backStack[_backStack.length - 1]);
    } else {
      _backStack.removeAt(_backStack.length - 1);
      Provider.of<StreamBackStackSupport>(context, listen: false)
          .switchBetweenPages(homePages.values[_backStack.length - 1]);
      Navigator.pop(context);
    }
  }
}

class UserProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenProfile ...'),
    );
  }
}

class DashboardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenDashboard ...'),
    );
  }
}

class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenSearch ...'),
    );
  }
}

enum homePages { screenDashboard, screenProfile, screenSearch }

class StreamBackStackSupport {
  final StreamController<homePages> _homePages = StreamController<homePages>();

  Stream<homePages> get selectedPage => _homePages.stream;

  void switchBetweenPages(homePages selectedPage) {
    _homePages.add(homePages.values[selectedPage.index]);
  }

  void close() {
    _homePages.close();
  }
}

【问题讨论】:

    标签: flutter dart flutter-hooks


    【解决方案1】:

    TL;DR

    完整的代码在最后。

    改用Navigator

    您应该以不同的方式处理这个问题。我可以为您提供一个适用于您的方法的解决方案,但是,我认为您应该通过实现 custom Navigator 来解决这个问题,因为这是 Flutter 中的内置解决方案。


    当您使用Navigator 时,您不需要任何基于流的管理,即您可以完全删除StreamBackStackSupport

    现在,您在之前的 Consumer 位置插入一个 Navigator 小部件:

    children: <Widget>[
      Expanded(
        child: Navigator(
          ...
        ),
      ),
      Container(...), // Your bottom bar..
    ]
    

    导航器使用字符串管理其路线,这意味着我们需要有一种方法将您的enum(我将其重命名为Page)转换为Strings。我们可以为此使用describeEnum 并将其放入extension

    enum Page { screenDashboard, screenProfile, screenSearch }
    
    extension on Page {
      String get route => describeEnum(this);
    }
    

    现在,您可以使用例如获取页面的字符串表示形式。 Page.screenDashboard.route.

    此外,您希望将实际页面映射到片段小部件,您可以这样做:

    class MainBodyApp extends HookWidget {
      final Map<Page, Widget> _fragments = {
        Page.screenDashboard: DashboardPage(),
        Page.screenProfile: UserProfilePage(),
        Page.screenSearch: SearchPage(),
      };
      ...
    

    要访问Navigator,我们需要有一个GlobalKey。通常我们会有一个StatefulWidget 并像这样管理GlobalKey。由于您想使用flutter_hooks,我选择使用GlobalObjectKey

      @override
      Widget build(BuildContext context) {
        final navigatorKey = GlobalObjectKey<NavigatorState>(context);
      ...
    

    现在,您可以在小部件中的任何位置使用navigatorKey.currentState 来访问此自定义导航器。完整的 Navigator 设置如下所示:

    Navigator(
      key: navigatorKey,
      initialRoute: Page.screenDashboard.route,
      onGenerateRoute: (settings) {
        final pageName = settings.name;
    
        final page = _fragments.keys.firstWhere((element) => describeEnum(element) == pageName);
    
        return MaterialPageRoute(settings: settings, builder: (context) => _fragments[page]);
      },
    )
    

    如您所见,我们传递了之前创建的navigatorKey 并定义了一个initialRoute,利用了我们创建的route 扩展。在onGenerateRoute 中,我们找到与路由名称对应的Page 枚举条目(String),然后返回带有适当_fragments 条目的MaterialPageRoute

    要推送新路由,只需使用navigatorKeypushNamed

    onPressed: () => navigatorKey.currentState.pushNamed(Page.screenDashboard.route),
    

    返回按钮

    我们还需要在自定义导航器上自定义调用pop。为此,需要WillPopScope

    WillPopScope(
      onWillPop: () async {
        if (navigatorKey.currentState.canPop()) {
          navigatorKey.currentState.pop();
          return false;
        }
    
        return true;
      },
      child: ..,
    )
    

    访问嵌套页面内的自定义导航器

    在传递给onGenerateRoute 的任何页面中,即在您的任何“片段”中,您可以只调用Navigator.of(context) 而不是使用全局键。这是可能的,因为这些路由是自定义导航器的子级,因此BuildContext 包含该自定义导航器。

    例如:

    // In SearchPage
    Navigator.of(context).pushNamed(Page.screenProfile.route);
    

    默认导航器

    您可能想知道现在如何访问MaterialApp 根导航器,例如推送新的全面屏路线。您可以为此使用findRootAncestorStateOfType

    context.findRootAncestorStateOfType<NavigatorState>().push(..);
    

    或者干脆

    Navigator.of(context, rootNavigator: true).push(..);
    

    这里是完整的代码:

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    
    void main() {
      runApp(StartupApplication());
    }
    
    enum Page { screenDashboard, screenProfile, screenSearch }
    
    extension on Page {
      String get route => describeEnum(this);
    }
    
    class StartupApplication extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'BackStack Support App',
          home: MainBodyApp(),
        );
      }
    }
    
    class MainBodyApp extends HookWidget {
      final Map<Page, Widget> _fragments = {
        Page.screenDashboard: DashboardPage(),
        Page.screenProfile: UserProfilePage(),
        Page.screenSearch: SearchPage(),
      };
    
      @override
      Widget build(BuildContext context) {
        final navigatorKey = GlobalObjectKey<NavigatorState>(context);
    
        return WillPopScope(
          onWillPop: () async {
            if (navigatorKey.currentState.canPop()) {
              navigatorKey.currentState.pop();
              return false;
            }
    
            return true;
          },
          child: Scaffold(
            appBar: AppBar(
              title: Text('BackStack Screen'),
            ),
            body: Container(
              child: Column(
                children: <Widget>[
                  Expanded(
                    child: Navigator(
                      key: navigatorKey,
                      initialRoute: Page.screenDashboard.route,
                      onGenerateRoute: (settings) {
                        final pageName = settings.name;
    
                        final page = _fragments.keys.firstWhere(
                            (element) => describeEnum(element) == pageName);
    
                        return MaterialPageRoute(settings: settings,
                            builder: (context) => _fragments[page]);
                      },
                    ),
                  ),
                  Container(
                    width: double.infinity,
                    height: 50.0,
                    padding: const EdgeInsets.symmetric(horizontal: 15.0),
                    color: Colors.indigo[400],
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: <Widget>[
                        RaisedButton(
                          onPressed: () => navigatorKey.currentState
                              .pushNamed(Page.screenDashboard.route),
                          child: Text('Dashboard'),
                        ),
                        RaisedButton(
                          onPressed: () => navigatorKey.currentState
                              .pushNamed(Page.screenProfile.route),
                          child: Text('Profile'),
                        ),
                        RaisedButton(
                          onPressed: () => navigatorKey.currentState
                              .pushNamed(Page.screenSearch.route),
                          child: Text('Search'),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class UserProfilePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          alignment: Alignment.center,
          child: Text(' screenProfile ...'),
        );
      }
    }
    
    class DashboardPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          alignment: Alignment.center,
          child: Text(' screenDashboard ...'),
        );
      }
    }
    
    class SearchPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          alignment: Alignment.center,
          child: Text(' screenSearch ...'),
        );
      }
    }
    

    【讨论】:

    • 太棒了,非常感谢。现在如何在嵌套页面中使用navigatorKey.currentState .pushNamed(Page.screenDashboard.route)?例如SearchPage ?我使用Provider 进行此操作
    • @DolDurma 对不起,我忘了提。在由您的自定义导航器控制的任何页面中,您都可以使用Navigator.of(context),因为BuildContext 包含自定义导航器。我编辑了答案以包含此内容。很高兴能帮到你。
    • 您能否更新您的帖子以帮助任何想要使用此解决方案的人?谢谢
    • @DolDurma 是的!我这样做了(:请参阅“访问嵌套页面内的自定义导航器”部分。
    • 您的代码和您的解决方案是我想要的,非常感谢。我有一些问题。如何避免浏览多个相同的页面?例如,当我在仪表板页面上单击任何其他按钮时应该无法再次导航,我的另一个问题是我可以有多个嵌套的导航策略吗?例如仪表板页面可以有另一个这样的导航,比如 instagram
    猜你喜欢
    • 1970-01-01
    • 2011-06-10
    • 1970-01-01
    • 2016-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-18
    相关资源
    最近更新 更多