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

Implementing Multiple Pages into a Single Page using Navigation and a Stack

在 Flutter 中,我想制作类似于 android 中的 Fragment 的屏幕,在我的代码中,我尝试将每个屏幕替换为当前屏幕,就像 Fragment.replecae 中的 [=] 26=],我使用了 HookProvider,当我点击按钮在它们之间切换时,我的代码工作正常,但我无法实现返回堆栈,这意味着当我点击 Back phone 上的按钮,我的代码应该显示我存储到 _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();
  }
}

TL;DR

完整代码在最后。

改用Navigator

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


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

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

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

导航器使用字符串管理其路线,这意味着我们需要有一种方法将您的 enum(我重命名为 Page)转换为 String。我们可以使用 describeEnum for that and put that into an :

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. Usually we would have a StatefulWidget and manage the GlobalKey like that. Since you want to use flutter_hooks, I opted to use a 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),然后 return 一个 MaterialPageRoute 与适当的 _fragments条目。

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

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

后退按钮

我们还需要自定义调用pop on our custom navigator. For this purpose, a WillPopScope需要:

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

    return true;
  },
  child: ..,
)

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

在传递给 onGenerateRoute 的任何页面中,即在您的任何 "fragments" 中,您可以只调用 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 ...'),
    );
  }
}