jamesblasco / modal_bottom_sheet

Flutter | Create advanced modal bottom sheets. Material, Cupertino or your own style
https://pub.dev/packages/modal_bottom_sheet
MIT License
1.86k stars 468 forks source link

Drag to dismiss and nested navigation issue #194

Open petrnymsa opened 3 years ago

petrnymsa commented 3 years ago

In case we have nested navigation within modal and user drags modal to bottom we expect that modal should be closed.

In current implementation, it will correctly call WillPopScope (if any) but in case of nested navigation it only goes back one nested page.

Is there anything what we can change / set to have this behavior 1: If user swipe back, / press navigation back -> nested navigator goes back one page

  1. If user drags modal to bottom / taps outside to dismiss -> modal is closed -> that is it should call root navigator instead of nested one.
ziqq commented 3 years ago

Any new? How can i use nested navigation?

DaleLaw commented 2 years ago

I have a solution.

The problem is that WillPopScope() only adds callback to the current route. For nested navigation, it should be added to the navigator's MaterialWithModalsPageRoute or CupertinoWithModalsPageRoute instead. To do so, I wrote a custom ModalWillPopScope widget:

import 'package:flutter/widgets.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';

///
/// This class is same as WillPopScope except we use getModalRoute() for getting the modal route
///
class ModalWillPopScope extends StatefulWidget {
  const ModalWillPopScope({
    Key? key,
    required this.child,
    required this.onWillPop,
  })  : assert(child != null),
        super(key: key);

  final Widget child;
  final WillPopCallback? onWillPop;

  @override
  State<ModalWillPopScope> createState() => _ModalWillPopScopeState();
}

class _ModalWillPopScopeState extends State<ModalWillPopScope> {
  ModalRoute<dynamic>? _route;

  ModalRoute<dynamic>? getModalRoute() {
    final route = ModalRoute.of(context);
    final navigator = Navigator.of(context);
    final navigatorRoute = ModalRoute.of(navigator.context);
    if (route is CupertinoModalBottomSheetRoute ||
        route is MaterialWithModalsPageRoute) {
      return route;
    }
    if (navigatorRoute is CupertinoModalBottomSheetRoute ||
        navigatorRoute is MaterialWithModalsPageRoute) {
      return navigatorRoute;
    }
    return null;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (widget.onWillPop != null)
      _route?.removeScopedWillPopCallback(widget.onWillPop!);
    _route = getModalRoute();
    if (widget.onWillPop != null)
      _route?.addScopedWillPopCallback(widget.onWillPop!);
  }

  @override
  void didUpdateWidget(ModalWillPopScope oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(_route == getModalRoute());
    if (widget.onWillPop != oldWidget.onWillPop && _route != null) {
      if (oldWidget.onWillPop != null)
        _route!.removeScopedWillPopCallback(oldWidget.onWillPop!);
      if (widget.onWillPop != null)
        _route!.addScopedWillPopCallback(widget.onWillPop!);
    }
  }

  @override
  void dispose() {
    if (widget.onWillPop != null)
      _route?.removeScopedWillPopCallback(widget.onWillPop!);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

Then we can use it like this:

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: onWillPop,
      child: ModalWillPopScope(
        onWillPop: onWillPop,
        child: ...,
      ),
    );
  }
kamami commented 1 year ago

@DaleLaw Could you provide a complete example of your solution or elaborate a little bit more. What widgets do I need to wrap and what would "onWillPop" look like? Appreciate!