5

在 Flutter 中,我想制作类似于 android 中的屏幕Fragment,在我的代码中,我尝试将每个屏幕替换为当前屏幕,就像Fragment.replecae在 android 中一样,我使用Hook并且Provider我的代码在单击按钮以在它们之间切换时工作正常,但我无法实现回栈,这意味着当我点击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();
  }
}

4

1 回答 1

16

TL;博士

完整的代码在最后。

Navigator改为使用

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


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

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

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

现在,您可以使用 eg 获取页面的字符串表示形式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,我选择使用 aGlobalObjectKey代替:

  @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条目的 a。

要推送新路由,您只需使用navigatorKeyand pushNamed

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

返回键

我们还需要自定义调用pop我们的自定义导航器。为此,WillPopScope需要 a:

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 ...'),
    );
  }
}
于 2020-03-30T21:32:23.643 回答