从抽屉导航部分屏幕
Navigate part of screen from drawer
假设我有一个具有以下设置的应用程序:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
我想知道如何使用任何 .push() 方法从 MainMenu 仅导航 MainLoginScreen 小部件。
(我找到了一种从主登录屏幕内的上下文导航的方法,方法是用 MaterialApp 小部件将其包装起来,但是如果我想改用具有另一个上下文的 MainMenu 小部件怎么办)
一般认为 'screen' 是路由中最顶层的小部件。 'screen' 的实例是您传递给 Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen())
的内容。所以如果在Scaffold下,就不是屏幕了。也就是说,这里有选项:
1。如果你想使用带 'back' 按钮的导航
使用不同的屏幕。为避免代码重复,创建 MenuAndContentScreen
class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
然后为每个屏幕创建一对屏幕和一个嵌套小部件:
class MainLoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2。如果您不需要使用 'back' 按钮进行导航
您可以使用 IndexedStack
小部件。它可以包含多个小部件,一次只有一个可见。
class MenuAndContentScreen extends StatefulWidget {
@override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
通常首选第一种方式,因为它是声明式的,这是 Flutter 的主要思想。当您静态声明整个小部件树时,出错和需要跟踪的事情就会减少。一旦感受到,那真是一种享受。如果你想避免返回导航,请使用 ahmetakil 在评论中建议的替换:Navigator.of(context).pushReplacement(...)
第二种方式主要用于 MainMenu 需要保存一些需要在视图之间保留的状态,因此我们选择具有可互换内容的一个屏幕。
3。使用嵌套的导航器小部件
正如您特别询问的有关嵌套 Navigator
小部件的问题,您可以使用它代替 IndexedStack
:
class MenuAndContentScreen extends StatefulWidget {
@override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
这应该可以解决问题,但这是一种不好的做法,因为:
- MainMenu 知道特定的导航器存在并且应该与之交互。最好要么像 (2) 中那样使用回调抽象这些知识,要么像 (1) 中那样不使用特定的导航器。 Flutter 实际上是将信息向下而不是向上传递。
- 有时您想突出显示 MainMenu 中的活动项目,但 MainMenu 很难知道导航器中当前有哪个小部件。这将添加另一个非向下交互。
对于这样的交互,有 BLoC 模式
在 Flutter 中,BLoC 代表业务逻辑组件。在其最简单的形式中,它是一个在父小部件中创建的普通对象,然后传递给 MainMenu 和 Navigator,这些小部件然后可以通过它发送事件并监听它。
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
@override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
@override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
@override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
@override
void dispose() {
_subscription.cancel();
}
这样做的好处是:
- MainMenu 不知道谁将收到事件,如果有人的话。
- 任意数量的侦听器都可以侦听此类事件。
但是,Navigator 仍然存在根本性缺陷。它可以在没有 MainMenu 知识的情况下使用 'back' 按钮或通过其内部小部件进行导航。所以没有一个变量知道现在显示的是哪个页面。要突出显示活动菜单项,您将查询导航器的堆栈,这会消除 BLoC 的好处。
出于所有这些原因,我仍然建议前两个解决方案之一。
假设我有一个具有以下设置的应用程序:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
我想知道如何使用任何 .push() 方法从 MainMenu 仅导航 MainLoginScreen 小部件。
(我找到了一种从主登录屏幕内的上下文导航的方法,方法是用 MaterialApp 小部件将其包装起来,但是如果我想改用具有另一个上下文的 MainMenu 小部件怎么办)
一般认为 'screen' 是路由中最顶层的小部件。 'screen' 的实例是您传递给 Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen())
的内容。所以如果在Scaffold下,就不是屏幕了。也就是说,这里有选项:
1。如果你想使用带 'back' 按钮的导航
使用不同的屏幕。为避免代码重复,创建 MenuAndContentScreen
class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
然后为每个屏幕创建一对屏幕和一个嵌套小部件:
class MainLoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2。如果您不需要使用 'back' 按钮进行导航
您可以使用 IndexedStack
小部件。它可以包含多个小部件,一次只有一个可见。
class MenuAndContentScreen extends StatefulWidget {
@override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
通常首选第一种方式,因为它是声明式的,这是 Flutter 的主要思想。当您静态声明整个小部件树时,出错和需要跟踪的事情就会减少。一旦感受到,那真是一种享受。如果你想避免返回导航,请使用 ahmetakil 在评论中建议的替换:Navigator.of(context).pushReplacement(...)
第二种方式主要用于 MainMenu 需要保存一些需要在视图之间保留的状态,因此我们选择具有可互换内容的一个屏幕。
3。使用嵌套的导航器小部件
正如您特别询问的有关嵌套 Navigator
小部件的问题,您可以使用它代替 IndexedStack
:
class MenuAndContentScreen extends StatefulWidget {
@override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
这应该可以解决问题,但这是一种不好的做法,因为:
- MainMenu 知道特定的导航器存在并且应该与之交互。最好要么像 (2) 中那样使用回调抽象这些知识,要么像 (1) 中那样不使用特定的导航器。 Flutter 实际上是将信息向下而不是向上传递。
- 有时您想突出显示 MainMenu 中的活动项目,但 MainMenu 很难知道导航器中当前有哪个小部件。这将添加另一个非向下交互。
对于这样的交互,有 BLoC 模式
在 Flutter 中,BLoC 代表业务逻辑组件。在其最简单的形式中,它是一个在父小部件中创建的普通对象,然后传递给 MainMenu 和 Navigator,这些小部件然后可以通过它发送事件并监听它。
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
@override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
@override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
@override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
@override
void dispose() {
_subscription.cancel();
}
这样做的好处是:
- MainMenu 不知道谁将收到事件,如果有人的话。
- 任意数量的侦听器都可以侦听此类事件。
但是,Navigator 仍然存在根本性缺陷。它可以在没有 MainMenu 知识的情况下使用 'back' 按钮或通过其内部小部件进行导航。所以没有一个变量知道现在显示的是哪个页面。要突出显示活动菜单项,您将查询导航器的堆栈,这会消除 BLoC 的好处。
出于所有这些原因,我仍然建议前两个解决方案之一。