使用导航和堆栈将多个页面实现为单个页面

Posted

技术标签:

【中文标题】使用导航和堆栈将多个页面实现为单个页面【英文标题】: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();
  


【问题讨论】:

【参考方案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

以上是关于使用导航和堆栈将多个页面实现为单个页面的主要内容,如果未能解决你的问题,请参考以下文章

从导航堆栈中删除页面 - xamarin.forms

如何使用scrapy将来自多个页面的数据收集到单个数据结构中

使用ivx实现页面导航的经验总结

突出显示单个页面的 wordpress 导航链接

将 s-s-rS 报告中的多个页面呈现为单个页面

FreshMVVM 在 ContentPage 和 TabbedPage 之间切换导航堆栈