Open kmorl opened 2 weeks ago
Hi @kmorl,
Can you post the code to reproduce this error?
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);
}
}
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...
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.
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