fujidaiti / smooth_sheets

Sheet widgets with smooth motion and great flexibility.
https://pub.dev/packages/smooth_sheets
MIT License
159 stars 11 forks source link

Open modal before another is closed #152

Open kmorl opened 2 weeks ago

kmorl commented 2 weeks ago

We have a problem that when a modal is currently closing and the user opens another one at the same time, an error is thrown and the app no longer works properly. I added an example for this behavior.

Using the version: 0.7.0

https://github.com/fujidaiti/smooth_sheets/assets/55160672/e0f316f0-16bd-4019-9cf5-86a4d8675e37

`══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building NavigationSheetRouteContent(dirty, dependencies:
[_InheritedSheetExtentScope, _ModalScopeStatus]):
'package:smooth_sheets/src/navigation/navigation_sheet_extent.dart': Failed assertion: line 45 pos
12: '_localExtentScopeKeyRegistry.containsKey(route)': is not true.

The relevant error-causing widget was:
  NavigationSheetRouteContent
  NavigationSheetRouteContent:file:///Users/klaudia.morleo/.pub-cache/hosted/pub.dev/smooth_sheets-0.7.0/lib/src/navigation/navigation_routes.dart:73:12

When the exception was thrown, this was the stack:
#2      NavigationSheetExtent.getLocalExtentScopeKey (package:smooth_sheets/src/navigation/navigation_sheet_extent.dart:45:12)
#3      NavigationSheetRouteContent.build (package:smooth_sheets/src/navigation/navigation_route.dart:131:40)
#4      StatelessElement.build (package:flutter/src/widgets/framework.dart:5550:49)
#5      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5480:15)
#6      Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#7      StatelessElement.update (package:flutter/src/widgets/framework.dart:5556:5)
#8      Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#9      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#10     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#11     StatelessElement.update (package:flutter/src/widgets/framework.dart:5556:5)
#12     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#13     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#14     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#15     StatelessElement.update (package:flutter/src/widgets/framework.dart:5556:5)
#16     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#17     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#18     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#19     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#20     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#21     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#22     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#23     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#24     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#25     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#26     Element.updateChildren (package:flutter/src/widgets/framework.dart:3973:32)
#27     MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6918:17)
#28     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#29     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#30     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#31     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#32     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#33     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#34     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#35     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#36     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#37     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#38     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#39     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#40     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#41     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#42     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#43     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#44     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#45     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#46     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#47     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#48     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#49     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#50     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#51     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#52     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#53     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#54     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#55     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#56     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#57     StatelessElement.update (package:flutter/src/widgets/framework.dart:5556:5)
#58     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#59     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#60     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#61     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#62     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#63     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#64     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#65     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#66     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#67     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#68     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#69     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#70     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#71     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#72     ProxyElement.update (package:flutter/src/widgets/framework.dart:5809:5)
#73     _InheritedNotifierElement.update (package:flutter/src/widgets/inherited_notifier.dart:105:11)
#74     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#75     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#76     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#77     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#78     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#79     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#80     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#81     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#82     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#83     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#84     ProxyElement.update (package:flutter/src/widgets/framework.dart:5809:5)
#85     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#86     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#87     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#88     ProxyElement.update (package:flutter/src/widgets/framework.dart:5809:5)
#89     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#90     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#91     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#92     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#93     StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#94     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#95     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#96     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#97     StatelessElement.update (package:flutter/src/widgets/framework.dart:5556:5)
#98     Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#99     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#100    Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#101    StatelessElement.update (package:flutter/src/widgets/framework.dart:5556:5)
#102    Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#103    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6765:14)
#104    Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#105    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#106    Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#107    ProxyElement.update (package:flutter/src/widgets/framework.dart:5809:5)
#108    Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#109    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#110    Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#111    ProxyElement.update (package:flutter/src/widgets/framework.dart:5809:5)
#112    Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#113    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#114    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#115    Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#116    StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#117    Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#118    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#119    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#120    Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#121    StatefulElement.update (package:flutter/src/widgets/framework.dart:5666:5)
#122    Element.updateChild (package:flutter/src/widgets/framework.dart:3824:15)
#123    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5505:16)
#124    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#125    Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#126    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2904:19)
#127    WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:989:21)
#128    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:448:5)
#129    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1386:15)
#130    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1311:9)
#131    SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1169:5)
#132    _invoke (dart:ui/hooks.dart:312:13)
#133    PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:399:5)
#134    _drawFrame (dart:ui/hooks.dart:283:31)
(elided 2 frames from class _AssertionError)

════════════════════════════════════════════════════════════════════════════════════════════════════

Another exception was thrown: The sheet extent and the dimensions values must be finalized during the layout phase.
Another exception was thrown: Duplicate GlobalKey detected in widget tree.
Another exception was thrown: NavigationSheet: markAsDimensionsWillChange() was called more times than markAsDimensionsChanged() in a frame.`
fujidaiti commented 2 weeks ago

Hi @kmorl,

Can you post the code to reproduce this error?

kmorl commented 2 weeks ago

It is basically a minimal version of the ai_playlist_generator from the cookbook.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:smooth_sheets/smooth_sheets.dart';

void main() {
  // Make the system navigation bar transparent on Android.
  if (Platform.isAndroid) {
    WidgetsFlutterBinding.ensureInitialized();
    SystemChrome.setEnabledSystemUIMode(
      SystemUiMode.edgeToEdge,
      overlays: [SystemUiOverlay.top],
    ).then((_) {
      SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark.copyWith(
        systemNavigationBarColor: Colors.transparent,
        systemNavigationBarDividerColor: Colors.transparent,
      ));
    });
  }

  runApp(const _AiPlaylistGeneratorExample());
}

class _AiPlaylistGeneratorExample extends StatelessWidget {
  const _AiPlaylistGeneratorExample();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: router,
    );
  }
}

// ----------------------------------------------------------
// Routes
// ----------------------------------------------------------

final sheetTransitionObserver = NavigationSheetTransitionObserver();

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const _Root(),
      routes: [_sheetShellRoute],
    ),
  ],
);

// A ShellRoute is used to create a new Navigator for nested navigation in the sheet.
final _sheetShellRoute = ShellRoute(
  observers: [sheetTransitionObserver],
  pageBuilder: (context, state, navigator) {
    // Use ModalSheetPage to show a modal sheet.
    return ModalSheetPage(
      swipeDismissible: true,
      child: _SheetShell(
        navigator: navigator,
        transitionObserver: sheetTransitionObserver,
      ),
    );
  },
  routes: [_introRoute],
);

final _introRoute = GoRoute(
  path: 'intro',
  pageBuilder: (context, state) {
    return const DraggableNavigationSheetPage(child: _IntroPage());
  },
);

// ----------------------------------------------------------
// Pages
// ----------------------------------------------------------

class _Root extends StatelessWidget {
  const _Root();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: ElevatedButton(
        onPressed: () => context.go('/intro'),
        child: const Text('Generate Playlist'),
      ),
    ));
  }
}

class _SheetShell extends StatelessWidget {
  const _SheetShell({
    required this.transitionObserver,
    required this.navigator,
  });

  final NavigationSheetTransitionObserver transitionObserver;
  final Widget navigator;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      bottom: false,
      child: PopScope(
        canPop: false,
        onPopInvoked: (didPop) async {
          if (!didPop) {
            if (context.mounted) {
              context.go('/');
            }
          }
        },
        child: NavigationSheet(
          transitionObserver: sheetTransitionObserver,
          child: Material(
            // Add circular corners to the sheet.
            borderRadius: BorderRadius.circular(16),
            clipBehavior: Clip.antiAlias,
            color: Theme.of(context).colorScheme.surface,
            child: navigator,
          ),
        ),
      ),
    );
  }
}

class _IntroPage extends StatelessWidget {
  const _IntroPage();

  @override
  Widget build(BuildContext context) {
    return SheetContentScaffold(
      appBar: _SharedAppBarHero(
        appbar: AppBar(
          leading: IconButton(
            onPressed: () => context.go('/'),
            icon: const Icon(Icons.close),
          ),
        ),
      ),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(
            horizontal: 32,
            vertical: 8,
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                "Hello there!\n"
                "I'm your AI music assistant. "
                "Ready to create the perfect playlist for you. 😊",
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.headlineMediumBold,
              ),
              const SizedBox(height: 64),
              FilledButton(
                onPressed: () => context.go('/intro/genre'),
                style: _largeFilledButtonStyle,
                child: const Text('Continue'),
              ),
              const SizedBox(height: 16),
              TextButton(
                onPressed: () => context.go('/'),
                style: _largeTextButtonStyle,
                child: const Text('No, thanks'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ----------------------------------------------------------
// Utilities
// ----------------------------------------------------------

extension on TextTheme {
  TextStyle? get headlineMediumBold => headlineMedium?.copyWith(fontWeight: FontWeight.bold);
}

final _largeFilledButtonStyle = FilledButton.styleFrom(
  minimumSize: const Size.fromHeight(56),
);

final _largeTextButtonStyle = TextButton.styleFrom(
  minimumSize: const Size.fromHeight(56),
);

class _SelectableChip extends StatefulWidget {
  const _SelectableChip({
    required this.label,
  });

  final Widget label;

  @override
  State<_SelectableChip> createState() => _SelectableChipState();
}

class _SelectableChipState extends State<_SelectableChip> {
  bool isSelected = false;

  @override
  Widget build(BuildContext context) {
    return FilterChip(
      onSelected: (isSelected) {
        setState(() => this.isSelected = isSelected);
      },
      selected: isSelected,
      label: widget.label,
    );
  }
}

class _SelectableListTile extends StatefulWidget {
  const _SelectableListTile({
    required this.title,
    required this.padding,
  });

  final String title;
  final EdgeInsets padding;

  @override
  State<_SelectableListTile> createState() => _SelectableListTileState();
}

class _SelectableListTileState extends State<_SelectableListTile> {
  bool isSelected = false;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: widget.padding,
      child: CheckboxListTile(
        title: Text(widget.title),
        value: isSelected,
        onChanged: (selected) {
          setState(() => isSelected = selected!);
        },
      ),
    );
  }
}

/// This widget makes it possible to create a (visually) shared appbar across the pages.
///
/// For better maintainability, it is recommended to create a page-specific app bar for each page
/// instead of a single 'super' shared app bar that includes all the functionality for every page.
class _SharedAppBarHero extends StatelessWidget implements PreferredSizeWidget {
  const _SharedAppBarHero({
    required this.appbar,
  });

  final AppBar appbar;

  @override
  Size get preferredSize => appbar.preferredSize;

  @override
  Widget build(BuildContext context) {
    return Hero(tag: 'HeroAppBar', child: appbar);
  }
}
fujidaiti commented 4 days ago

I created more simplified version and can reproduce the same problem. With the code below, I got another error in addition to the errors you reported:

'package:flutter/src/widgets/navigator.dart': Failed assertion: line 3577 pos 14: 'observer.navigator == null': is not true.

This indicates that a Navigator tried to attach itself to the sheetTransitionObserver, but it already had another navigator instance.

We have a problem that when a modal is currently closing and the user opens another one at the same time

As you mentioned, there is a time when two sheets exist, plus they share a globally defined NavigationObserver (sheetTransitionObserver). But Navigator doesn't allow to share an observer between multiple navigators as the error message suggests.

If your sheet directly creates a Navigator, we can fix this problem by moving the global navigation observer to a State:

class _SheetState extends State<_Sheet> {
  final sheetTransitionObserver = NavigationSheetTransitionObserver();

  @override
  Widget build(BuildContext context) {...}
}

But for go_router, there's no way to dynamically create observers per navigator. I don't have a good solution for this right now, but I'll take a closer look this weekend...

Simplified reproduction code ```dart import 'package:flutter/material.dart'; import 'package:smooth_sheets/smooth_sheets.dart'; void main() { runApp(const _Issue152()); } class _Issue152 extends StatelessWidget { const _Issue152(); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { return SafeArea( child: Align( alignment: Alignment.topCenter, child: ElevatedButton( onPressed: () => Navigator.of(context).push( ModalSheetRoute( builder: (context) => const _Sheet(), ), ), child: const Text('Show Navigation Sheet'), )), ); }, ), ), ); } } final sheetTransitionObserver = NavigationSheetTransitionObserver(); class _Sheet extends StatelessWidget { const _Sheet(); @override Widget build(BuildContext context) { final nestedNavigator = Navigator( observers: [sheetTransitionObserver], onGenerateRoute: (settings) { return DraggableNavigationSheetRoute( builder: (context) { return Container( width: double.infinity, height: 400, color: Colors.white, ); }, ); }, ); return NavigationSheet( transitionObserver: _transitionObserver, child: Material( child: nestedNavigator, ), ); } } ```
fujidaiti commented 2 days ago

Related go_router issues:

I think we will have to wait for those issues to be fixed. The essential problem here is, there a time when temporarily two navigator instances exist in a frame, and the global key and the navigator observers are shared with those navigators, but which is not allowed, and current go_router doesn't provide a way to create a global key and navigator observers per navigator instance.