Overlay

Persistent Sheet

A persistent sheet is displayed above another widget while still allowing users to interact with the widget below.

It is part of FScaffold, which should be preferred in most cases.

A closely related widget is a modal sheet which prevents the user from interacting with the rest of the app.

All calls to showFPersistentSheet(...) should be made inside widgets that have either FScaffold or FSheets as their ancestor.

1class _Sheet extends StatefulWidget {
2 @override
3 State<_Sheet> createState() => _SheetState();
4}
5
6class _SheetState extends State<_Sheet> {
7 final Map<FLayout, FPersistentSheetController> _controllers = {};
8
9 @override
10 void dispose() {
11 for (final controller in _controllers.values) {
12 controller.dispose();
13 }
14 super.dispose();
15 }
16
17 @override
18 Widget build(BuildContext context) {
19 VoidCallback onPress(FLayout side) => () {
20 for (final MapEntry(:key, :value) in _controllers.entries) {
21 if (key != side && value.status.isCompleted) {
22 return;
23 }
24 }
25
26 var controller = _controllers[side];
27 if (controller == null) {
28 controller = _controllers[side] ??= showFPersistentSheet(
29 context: context,
30 side: side,
31 builder: (context, controller) =>
32 Form(side: side, controller: controller),
33 );
34 } else {
35 controller.toggle();
36 }
37 };
38
39 return Column(
40 mainAxisAlignment: .center,
41 mainAxisSize: .min,
42 spacing: 5,
43 children: [
44 FButton(onPress: onPress(.ltr), child: const Text('Left')),
45 FButton(onPress: onPress(.ttb), child: const Text('Top')),
46 FButton(onPress: onPress(.rtl), child: const Text('Right')),
47 FButton(onPress: onPress(.btt), child: const Text('Bottom')),
48 ],
49 );
50 }
51}
52
53class Form extends StatelessWidget {
54 final FLayout side;
55 final FPersistentSheetController controller;
56 const Form({required this.side, required this.controller, super.key});
57 @override
58 Widget build(BuildContext context) => Container(
59 height: .infinity,
60 width: .infinity,
61 decoration: BoxDecoration(
62 color: context.theme.colors.background,
63 border: side.vertical
64 ? .symmetric(
65 horizontal: BorderSide(color: context.theme.colors.border),
66 )
67 : .symmetric(
68 vertical: BorderSide(color: context.theme.colors.border),
69 ),
70 ),
71 child: Padding(
72 padding: const .symmetric(horizontal: 15, vertical: 8.0),
73 child: Column(
74 mainAxisAlignment: .center,
75 mainAxisSize: .min,
76 crossAxisAlignment: .start,
77 children: [
78 Text(
79 'Account',
80 style: context.theme.typography.xl2.copyWith(
81 fontWeight: .w600,
82 color: context.theme.colors.foreground,
83 height: 1.5,
84 ),
85 ),
86 Text(
87 'Make changes to your account here. Click save when you are done.',
88 style: context.theme.typography.sm.copyWith(
89 color: context.theme.colors.mutedForeground,
90 ),
91 ),
92 const SizedBox(height: 8),
93 SizedBox(
94 width: 450,
95 child: Column(
96 children: [
97 const FTextField(label: Text('Name'), hint: 'John Renalo'),
98 const SizedBox(height: 10),
99 const FTextField(label: Text('Email'), hint: 'john@doe.com'),
100 const SizedBox(height: 16),
101 FButton(onPress: controller.toggle, child: const Text('Save')),
102 ],
103 ),
104 ),
105 ],
106 ),
107 ),
108 );
109}
110

CLI

To generate and customize this style:

dart run forui style create persistent-sheet

Usage

showFPersistentSheet(...)

1showFPersistentSheet(
2 context: context,
3 style: const .delta(flingVelocity: 700),
4 side: .btt,
5 builder: (context, controller) => Padding(
6 padding: const .all(16),
7 child: Column(
8 mainAxisSize: .min,
9 children: [
10 const Text('Sheet content'),
11 FButton(onPress: controller.hide, child: const Text('Close')),
12 ],
13 ),
14 ),
15)

FSheets(...)

1FSheets(
2 child: Placeholder(),
3)

Examples

With KeepAliveOffstage

1class _Sheet extends StatefulWidget {
2 @override
3 State<_Sheet> createState() => _SheetState();
4}
5
6class _SheetState extends State<_Sheet> {
7 final Map<FLayout, FPersistentSheetController> _controllers = {};
8
9 @override
10 void dispose() {
11 for (final controller in _controllers.values) {
12 controller.dispose();
13 }
14 super.dispose();
15 }
16
17 @override
18 Widget build(BuildContext context) {
19 VoidCallback onPress(FLayout side) => () {
20 for (final MapEntry(:key, :value) in _controllers.entries) {
21 if (key != side && value.status.isCompleted) {
22 return;
23 }
24 }
25
26 var controller = _controllers[side];
27 if (controller == null) {
28 controller = _controllers[side] ??= showFPersistentSheet(
29 context: context,
30 side: side,
31 keepAliveOffstage: true,
32 builder: (context, controller) =>
33 Form(side: side, controller: controller),
34 );
35 } else {
36 controller.toggle();
37 }
38 };
39
40 return Column(
41 mainAxisAlignment: .center,
42 mainAxisSize: .min,
43 spacing: 5,
44 children: [
45 FButton(onPress: onPress(.ltr), child: const Text('Left')),
46 FButton(onPress: onPress(.ttb), child: const Text('Top')),
47 FButton(onPress: onPress(.rtl), child: const Text('Right')),
48 FButton(onPress: onPress(.btt), child: const Text('Bottom')),
49 ],
50 );
51 }
52}
53
54class Form extends StatelessWidget {
55 final FLayout side;
56 final FPersistentSheetController controller;
57 const Form({required this.side, required this.controller, super.key});
58 @override
59 Widget build(BuildContext context) => Container(
60 height: .infinity,
61 width: .infinity,
62 decoration: BoxDecoration(
63 color: context.theme.colors.background,
64 border: side.vertical
65 ? .symmetric(
66 horizontal: BorderSide(color: context.theme.colors.border),
67 )
68 : .symmetric(
69 vertical: BorderSide(color: context.theme.colors.border),
70 ),
71 ),
72 child: Padding(
73 padding: const .symmetric(horizontal: 15, vertical: 8.0),
74 child: Column(
75 mainAxisAlignment: .center,
76 mainAxisSize: .min,
77 crossAxisAlignment: .start,
78 children: [
79 Text(
80 'Account',
81 style: context.theme.typography.xl2.copyWith(
82 fontWeight: .w600,
83 color: context.theme.colors.foreground,
84 height: 1.5,
85 ),
86 ),
87 Text(
88 'Make changes to your account here. Click save when you are done.',
89 style: context.theme.typography.sm.copyWith(
90 color: context.theme.colors.mutedForeground,
91 ),
92 ),
93 const SizedBox(height: 8),
94 SizedBox(
95 width: 450,
96 child: Column(
97 children: [
98 const FTextField(label: Text('Name'), hint: 'John Renalo'),
99 const SizedBox(height: 10),
100 const FTextField(label: Text('Email'), hint: 'john@doe.com'),
101 const SizedBox(height: 16),
102 FButton(onPress: controller.toggle, child: const Text('Save')),
103 ],
104 ),
105 ),
106 ],
107 ),
108 ),
109 );
110}
111

With DraggableScrollableSheet

1class DraggablePersistentSheetExample extends StatefulWidget {
2 @override
3 State<DraggablePersistentSheetExample> createState() => _DraggableState();
4}
5
6class _DraggableState extends State<DraggablePersistentSheetExample> {
7 FPersistentSheetController? controller;
8
9 @override
10 void dispose() {
11 controller?.dispose();
12 super.dispose();
13 }
14
15 @override
16 Widget build(BuildContext context) => FButton(
17 child: const Text('Click me'),
18 onPress: () {
19 if (controller != null) {
20 controller!.toggle();
21 return;
22 }
23
24 controller = showFPersistentSheet(
25 context: context,
26 side: .btt,
27 mainAxisMaxRatio: null,
28 builder: (context, _) => DraggableScrollableSheet(
29 expand: false,
30 builder: (context, controller) => ScrollConfiguration(
31 // This is required to enable dragging on desktop.
32 // See https://github.com/flutter/flutter/issues/101903 for more information.
33 behavior: ScrollConfiguration.of(
34 context,
35 ).copyWith(dragDevices: {.touch, .mouse, .trackpad}),
36 child: FTileGroup.builder(
37 count: 25,
38 scrollController: controller,
39 tileBuilder: (context, index) =>
40 FTile(title: Text('Tile $index')),
41 ),
42 ),
43 ),
44 );
45 },
46 );
47}
48

On this page