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 Row(
40 mainAxisAlignment: .center,
41 mainAxisSize: .min,
42 spacing: 10,
43 children: [
44 FButton(
45 variant: .outline,
46 size: .sm,
47 mainAxisSize: .min,
48 onPress: onPress(.ltr),
49 child: const Text('Left'),
50 ),
51 FButton(
52 variant: .outline,
53 size: .sm,
54 mainAxisSize: .min,
55 onPress: onPress(.ttb),
56 child: const Text('Top'),
57 ),
58 FButton(
59 variant: .outline,
60 size: .sm,
61 mainAxisSize: .min,
62 onPress: onPress(.btt),
63 child: const Text('Bottom'),
64 ),
65 FButton(
66 variant: .outline,
67 size: .sm,
68 mainAxisSize: .min,
69 onPress: onPress(.rtl),
70 child: const Text('Right'),
71 ),
72 ],
73 );
74 }
75}
76
77class Form extends StatelessWidget {
78 final FLayout side;
79 final FPersistentSheetController controller;
80 const Form({required this.side, required this.controller, super.key});
81 @override
82 Widget build(BuildContext context) => Container(
83 height: .infinity,
84 width: .infinity,
85 decoration: BoxDecoration(
86 color: context.theme.colors.background,
87 border: side.vertical
88 ? .symmetric(
89 horizontal: BorderSide(color: context.theme.colors.border),
90 )
91 : .symmetric(
92 vertical: BorderSide(color: context.theme.colors.border),
93 ),
94 ),
95 child: Center(
96 child: Padding(
97 padding: const .symmetric(horizontal: 15, vertical: 8.0),
98 child: Column(
99 mainAxisAlignment: .center,
100 mainAxisSize: .min,
101 crossAxisAlignment: .start,
102 children: [
103 Text(
104 'Account',
105 style: context.theme.typography.xl2.copyWith(
106 fontWeight: .w600,
107 color: context.theme.colors.foreground,
108 height: 1.5,
109 ),
110 ),
111 Text(
112 'Make changes to your account here. Click save when you are done.',
113 style: context.theme.typography.sm.copyWith(
114 color: context.theme.colors.mutedForeground,
115 ),
116 ),
117 const SizedBox(height: 8),
118 SizedBox(
119 width: 450,
120 child: Column(
121 children: [
122 const FTextField(label: Text('Name'), hint: 'John Renalo'),
123 const SizedBox(height: 10),
124 const FTextField(label: Text('Email'), hint: 'john@doe.com'),
125 const SizedBox(height: 16),
126 FButton(
127 onPress: controller.toggle,
128 child: const Text('Save'),
129 ),
130 ],
131 ),
132 ),
133 ],
134 ),
135 ),
136 ),
137 );
138}
139

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 Row(
41 mainAxisAlignment: .center,
42 mainAxisSize: .min,
43 spacing: 10,
44 children: [
45 FButton(
46 variant: .outline,
47 size: .sm,
48 mainAxisSize: .min,
49 onPress: onPress(.ltr),
50 child: const Text('Left'),
51 ),
52 FButton(
53 variant: .outline,
54 size: .sm,
55 mainAxisSize: .min,
56 onPress: onPress(.ttb),
57 child: const Text('Top'),
58 ),
59 FButton(
60 variant: .outline,
61 size: .sm,
62 mainAxisSize: .min,
63 onPress: onPress(.btt),
64 child: const Text('Bottom'),
65 ),
66 FButton(
67 variant: .outline,
68 size: .sm,
69 mainAxisSize: .min,
70 onPress: onPress(.rtl),
71 child: const Text('Right'),
72 ),
73 ],
74 );
75 }
76}
77
78class Form extends StatelessWidget {
79 final FLayout side;
80 final FPersistentSheetController controller;
81 const Form({required this.side, required this.controller, super.key});
82 @override
83 Widget build(BuildContext context) => Container(
84 height: .infinity,
85 width: .infinity,
86 decoration: BoxDecoration(
87 color: context.theme.colors.background,
88 border: side.vertical
89 ? .symmetric(
90 horizontal: BorderSide(color: context.theme.colors.border),
91 )
92 : .symmetric(
93 vertical: BorderSide(color: context.theme.colors.border),
94 ),
95 ),
96 child: Center(
97 child: Padding(
98 padding: const .symmetric(horizontal: 15, vertical: 8.0),
99 child: Column(
100 mainAxisAlignment: .center,
101 mainAxisSize: .min,
102 crossAxisAlignment: .start,
103 children: [
104 Text(
105 'Account',
106 style: context.theme.typography.xl2.copyWith(
107 fontWeight: .w600,
108 color: context.theme.colors.foreground,
109 height: 1.5,
110 ),
111 ),
112 Text(
113 'Make changes to your account here. Click save when you are done.',
114 style: context.theme.typography.sm.copyWith(
115 color: context.theme.colors.mutedForeground,
116 ),
117 ),
118 const SizedBox(height: 8),
119 SizedBox(
120 width: 450,
121 child: Column(
122 children: [
123 const FTextField(label: Text('Name'), hint: 'John Renalo'),
124 const SizedBox(height: 10),
125 const FTextField(label: Text('Email'), hint: 'john@doe.com'),
126 const SizedBox(height: 16),
127 FButton(
128 onPress: controller.toggle,
129 child: const Text('Save'),
130 ),
131 ],
132 ),
133 ),
134 ],
135 ),
136 ),
137 ),
138 );
139}
140

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 variant: .outline,
18 size: .sm,
19 mainAxisSize: .min,
20 child: const Text('Click me'),
21 onPress: () {
22 if (controller != null) {
23 controller!.toggle();
24 return;
25 }
26
27 controller = showFPersistentSheet(
28 context: context,
29 side: .btt,
30 mainAxisMaxRatio: null,
31 builder: (context, _) => DraggableScrollableSheet(
32 expand: false,
33 builder: (context, controller) => ScrollConfiguration(
34 // This is required to enable dragging on desktop.
35 // See https://github.com/flutter/flutter/issues/101903 for more information.
36 behavior: ScrollConfiguration.of(
37 context,
38 ).copyWith(dragDevices: {.touch, .mouse, .trackpad}),
39 child: FTileGroup.builder(
40 count: 25,
41 scrollController: controller,
42 tileBuilder: (context, index) =>
43 FTile(title: Text('Tile $index')),
44 ),
45 ),
46 ),
47 );
48 },
49 );
50}
51

On this page