问题
There's a similar unanswered question here: FullscreenDialog screen not covering bottomnavigationbar
but I want to provide more context, to see if that helps find a solution.
We'll start at the top with my main.dart
, I am building a MaterialApp
that builds a custom DynamicApp
. Here's the important stuff:
Widget build(BuildContext context) {
var _rootScreenSwitcher = RootScreenSwitcher(key: switcherKey);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
builder: (context, child) {
return DynamicApp(
navigator: locator<NavigationService>().navigatorKey,
child: child,
switcher: _rootScreenSwitcher,
);
},
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
},
home: _rootScreenSwitcher,
);
}
My DynamicApp
sets up the root Scaffold
like so:
Widget build(BuildContext context) {
return Scaffold(
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
drawerItems: widget.drawerItems,
headerView: Container(
child: Text('Drawer Header'),
decoration: BoxDecoration(color: Colors.blue),
),
onNavigationItemSelect: (index) {
onTapped(index);
return true; // true means that drawer must close and false is Vice versa
},
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
onTapped(index);
},
currentIndex: _selectedIndex,
items: bottomNavBarItems,
showUnselectedLabels: false,
),
body: widget.child,
);
}
The child of the DynamicApp
is a widget called RootScreenSwitcher
which is an IndexedStack
and controls the switching of screens from my BottomNavigationBar
and also when items are selected in the Drawer
. Here's the RootScreenSwitcher
:
class RootScreenSwitcherState extends State<RootScreenSwitcher> {
int _currentIndex = 0;
int get currentIndex => _currentIndex;
set currentIndex(index) {
setState(() {
_currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: IndexedStack(
index: _currentIndex,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
),
);
}
void switchScreen(int index) {
setState(() {
_currentIndex = index;
});
}
}
Each of the main section of the app has its own Navigator
and root screen. This all works fine, and I'm happy with the overall navigation structure. Each root screen has its own AppBar
but the global Drawer
and BottomNavigationBar
are handled in the DynamicApp
so I don't have to keep setting them in the other Scaffold
screens.
So, then it came to start to introduce other sections of the app that are not serviced by the bottom tab bar, and can be presented from the Drawer
or from other action buttons. Each of these new sections would have to be modal fullscreenDialog
screens so they slide up from the bottom, but have their own navigation and scaffold.
My issue is that when I navigate to my SettingsNavigator
screen it slides up from behind the BottomNavigationBar
, and not on top of everything. Here's the onGenerateRoute
method of the MaterialApp
:
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
}
I'm new to Flutter and don't quite understand how routing works with contexts, so I am wondering whether the context of the screen that calls the navigateTo
method of the Navigator
becomes the main build context, and is therefore not on top of the widget tree?
gif here: https://www.dropbox.com/s/nwgzo0q28cqk61p/FlutteRModalProb.gif?dl=0
Here's the tree structure that shows that the Scaffold for the Settings screen has been placed inside the DynamicApp
Scaffold
. The modal needs to sit above DynamicApp
.
Can anyone shed some light on this?
UPDATE: I have tried creating and sharing a unique ScaffoldState
key for the tab bar screens, and then the Settings page has a different key. It made no difference. I wonder now if it is the BuildContext
having the same parent.
UPDATE UPDATE:
I had a breakthrough last night which has made me realise that it just isn't going to be possible to use embedded Scaffolds in the way I have them at the moment. The problem is that i have a root scaffold called DynamicApp
which persists my Drawer
and BottomNavigationBar
, but loading in other Scaffold
pages into the body means the modals are slotting into that body and behind the BottomNavigationBar
. To solve the problem you have to subclass BottomNavigationBar
and reference it in every Scaffold
; which means encapsulating all of the business logic so it uses ChangeNotifier
to change state when the nav is interacted with. Basically, Flutter forces a separation of concerns on your architecture, which I guess is a good thing. I'll compose a better answer when I've done all the extra work.
回答1:
After many hours tearing my hair out trying to pass ScaffoldState
keys around, and Navigators
the answer was to build the BottomNavigationBar
into every Scaffold
. But to do this I've had to change how my architecture works ... for the better! I now have a BottomNavigationBar
and RootScreenSwitcher
that listens for updates from an AppState
ChangeNotifier
and rebuilds itself when the page index changes. So, I only have to change state in one place for the app to adapt automatically. This is the AppState
class:
import 'package:flutter/material.dart';
class AppState extends ChangeNotifier {
int _pageIndex = 0;
int get pageIndex {
return _pageIndex;
}
set setpageIndex(int index) {
_pageIndex = index;
notifyListeners();
}
}
and this is my custom BottomNavigationBar
called AppBottomNavigationBar
:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppBottomNavigationBar extends StatefulWidget {
AppBottomNavigationBar({Key key}) : super(key: key);
@override
_AppBottomNavigationBarState createState() => _AppBottomNavigationBarState();
}
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
@override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex ?? 0,
items: bottomNavBarItems,
showUnselectedLabels: false,
onTap: (int index) {
setState(() {
state.setpageIndex = index;
});
},
);
}
}
So, now in my other Scaffold
pages I only need to include this line to make sure the BottomNavigationBar is in the
Scaffold`:
bottomNavigationBar: AppBottomNavigationBar(),
Which means absolute minimal boilerplate.
I changed the name of the DynamicApp
class to AppRootScaffold
and this now contains a Scaffold
, Drawer
, and then set the body of the Scaffold
as the RootScreenSwitcher
:
class RootScreenSwitcher extends StatelessWidget {
RootScreenSwitcher({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return SafeArea(
top: false,
child: IndexedStack(
index: currentIndex ?? 0,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
);
}
}
I still have a lot to do to streamline this architecture, but it is definitely the better way to go.
UPDATE:
Can you spot the problem with the new Scaffold
architecture?
This is still not great. Ironically, I need the BottomNavigationBar
back in the root Scaffold
for this to work as expected. But then the modals wont appear over the top of the bar again.
来源:https://stackoverflow.com/questions/63231337/flutter-materialpageroute-as-fullscreendialog-appears-underneath-bottomnavigatio