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

标签 flutter dart flutter-hooks

在 Flutter 中,我想制作像 Android 中的 Fragment 一样的屏幕,在我的代码中,我尝试将每个屏幕替换为当前屏幕,就像使用 Fragment.replecae 中的那样android,我使用了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();
  }
}

最佳答案

TL;DR

完整代码在最后。

使用Navigator相反

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

<小时/>

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

现在,您插入 Navigator您拥有Consumer的小部件之前:

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

导航器使用字符串管理其路线,这意味着我们需要有一种方法来转换您的 enum (我将其重命名为 Page )到 String s。我们可以使用 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与路由名称(a String )相对应的枚举条目,然后返回 MaterialPageRoute与适当的_fragments条目。

要推送新路线,您只需使用 navigatorKey pushNamed :

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)而不是使用全局 key 。这是可能的,因为这些路线是自定义导航器的子路线,因此 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 ...'),
    );
  }
}

关于flutter - 使用导航和堆栈将多个页面实现为单个页面,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60901865/

相关文章:

flutter - 如何验证 flutter 中的下拉按钮?

flutter - 如何从列表中删除字符串的最后一个字母? Dart

dart - 如何 typedef 泛型函数?

flutter - 使用 flutter HookWidget 和 didChangeAppLifecycleState

Flutter Hooks 使用 useEffect 获取数据 - setState() 或 markNeedsBuild() 在构建期间调用

flutter - 错误 : The argument type 'String Function(String)' can't be assigned to the parameter type 'String? Function(String?)?'

api - 在 flutter 中如果文本内容增加,我想在帖子底部显示更多文本?

mocking - 与 Dart mock

function - return_of_invalid_type 返回类型 'String' 不是方法 'void' 定义的 'getData'