Search code examples
androidflutterdartflutter-showmodalbottomsheet

How can I create inner navigation in showModalBottomSheet?


In my app I am trying to implement Badoo-like sort/filter showBottomModalSheet feature. I managed to create 2 separate pages, which I can navigate back and forth. However, the problem I'm facing is the second page in showBottomModalSheet. Back button works fine until I try to touch outside of the modal, which takes back to the first page. Instead it should close modal.

User navigates to sort users modal, which shows the 1st page in showBottomModalSheet When user taps "Show gender" it navigates to the second page (the one with different genders). When back button is pressed it navigates to 1st screen until it closes modal completely. Touching outside of the modal also closes the modal

The best stackoverflow answer that I tried:

https://stackoverflow.com/questions/63602999/how-can-i-do-navigator-push-in-a-modal-bottom-sheet-only-not-the-parent-page/63603685#63603685

I also tried using modal_bottom_sheet package, but had no luck. https://pub.dev/packages/modal_bottom_sheet/example

Most of my code behind showBottomModalSheet:

class Page1 extends StatefulWidget {
  const Page1({
    Key? key
  }) : super(key: key);
  @override
  _Page1State createState() => _Page1State();
}

class _Page1State extends State<Page1> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
  int _currentView = 0;
  late List<Widget> pages;

  @override
  void initState() {
    pages = [
      page1(),
      page2(),
    ];
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
print("LOG build _currentView ${_currentView}");
    return pages[_currentView];


  }
  Widget page1() {
    return WillPopScope(
        onWillPop: () async {
          return true;
        },
        child: Container(
          decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.only(
                  topRight: Radius.circular(60), topLeft: Radius.circular(60))),
          height: 400,
          width: double.maxFinite,
          child: Center(
              child: Column(
            children: [
              Text("First page"),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _currentView = 1;
                    print("LOG page1 _currentView ${_currentView}");
                  });
                },
                child: Text("tap to navigate to 2nd page"),
              ),
            ],
          )),
        ));
  }

  Widget page2() {
    return WillPopScope(
        child: Container(
          decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.only(
                  topRight: Radius.circular(60), topLeft: Radius.circular(60))),
          height: 400,
          width: double.maxFinite,
      child: Center(
            child: InkWell(
              onTap: () {
                setState(() {
                  _currentView = 0;
                  print("LOG page2 _currentView ${_currentView}");
                });
              },
              child: Text("tap to navigate to 1st screen"),
            ),
          ),
        ),
        onWillPop: () async {
          print("LOG currentView jot $_currentView");
          if (_currentView == 0) {
            return true;
          }

          setState(() {
            _currentView = 0;
          });

          return false;
        });
  }
}

Solution

  • Solution 1

    Use the standard DraggableScrollableSheet or a 3rd-party widget to do it, there are a bunch of them. Here are some from https://pub.dev/

    1. awesome_select
    2. backdrop_modal_route
    3. bottom_sheet_expandable_bar
    4. bottom_sheet
    5. cupertino_modal_sheet
    6. modal_bottom_sheet
    7. just_bottom_sheet
    8. sheet

    Solution 2

    Anyway, if you'd like to do it manually I'd do it with Navigator.push/Navigator.pop instead with PageRouteBuilder with barrierDismissible=true.

    It would like the following. Check out the live demo on DartPad.

    Screenshot

    Here's the code:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key}) : super(key: key);
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      void _show() async {
        await Navigator.of(context).push(
          PageRouteBuilder(
            opaque: false,
            barrierDismissible: true,
            pageBuilder: (_, __, ___) => const Page1(),
          ),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: _show,
            tooltip: 'Settings',
            child: const Icon(Icons.settings),
          ),
        );
      }
    }
    
    class Page1 extends StatefulWidget {
      const Page1({Key? key}) : super(key: key);
    
      @override
      State<Page1> createState() => _Page1State();
    }
    
    class _Page1State extends State<Page1> {
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            Align(
              alignment: Alignment.bottomCenter,
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: const BorderRadius.only(
                      topRight: Radius.circular(60), topLeft: Radius.circular(60)),
                  boxShadow: [
                    BoxShadow(
                      color: Colors.grey.withOpacity(0.5),
                      spreadRadius: 5,
                      blurRadius: 7,
                      offset: const Offset(0, 3), // changes position of shadow
                    ),
                  ],
                ),
                height: 400,
                width: double.maxFinite,
                child: Center(
                  child: Material(
                    type: MaterialType.transparency,
                    child: Column(
                      children: [
                        const Text("First page"),
                        ElevatedButton(
                          onPressed: () async {
                            final backButton =
                                await Navigator.of(context).push<bool?>(
                              PageRouteBuilder(
                                opaque: false,
                                barrierDismissible: true,
                                pageBuilder: (_, __, ___) => const Page2(),
                              ),
                            );
    
                            if (backButton == null || backButton == false) {
                              if (mounted) Navigator.of(context).pop();
                            }
                          },
                          child: const Text("tap to navigate to 2nd page"),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ],
        );
      }
    }
    
    class Page2 extends StatelessWidget {
      const Page2({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Align(
          alignment: Alignment.bottomCenter,
          child: Container(
            decoration: const BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.only(
                  topRight: Radius.circular(60), topLeft: Radius.circular(60)),
            ),
            height: 400,
            width: double.maxFinite,
            child: Center(
              child: Material(
                type: MaterialType.transparency,
                child: InkWell(
                  onTap: () => Navigator.of(context).pop(true),
                  child: const Text("tap to navigate to 1st screen"),
                ),
              ),
            ),
          ),
        );
      }
    }