Flutter MaterialPageRoute as fullscreenDialog appears underneath BottomNavigationBar

可紊 提交于 2020-08-17 12:09:31

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!