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.
Preview
Code
class Sheets extends StatefulWidget {
@override
State<Sheets> createState() => _State();
}
class _State extends State<Sheets> {
final Map<FLayout, FPersistentSheetController> _controllers = {};
@override
Widget build(BuildContext context) {
VoidCallback onPress(FLayout side) => () {
for (final MapEntry(:key, :value) in _controllers.entries) {
if (key != side && value.status.isCompleted) {
return;
}
}
var controller = _controllers[side];
if (controller == null) {
controller = _controllers[side] ??= showFPersistentSheet(
context: context,
side: FLayout.ltr,
builder: (context, controller) => Form(side: side),
);
} else {
controller.toggle();
}
};
return FScaffold( // This can be replaced with FSheets
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 5,
children: [
FButton(
onPress: onPress(FLayout.ltr),
child: const Text('Left'),
),
FButton(
onPress: onPress(FLayout.ttb),
child: const Text('Top'),
),
FButton(
onPress: onPress(FLayout.rtl),
child: const Text('Right'),
),
FButton(
onPress: onPress(FLayout.btt),
child: const Text('Bottom'),
),
],
),
);
}
@override
void dispose() {
for (final controller in _controllers.values) {
controller.dispose();
}
super.dispose();
}
}
class Form extends StatelessWidget {
final FLayout side;
const Form({required this.side, super.key});
@override
Widget build(BuildContext context) => Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
color: context.theme.colors.background,
border: side.vertical
? Border.symmetric(horizontal: BorderSide(color: context.theme.colors.border))
: Border.symmetric(vertical: BorderSide(color: context.theme.colors.border)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Account',
style: context.theme.typography.xl2.copyWith(
fontWeight: FontWeight.w600,
color: context.theme.colors.foreground,
height: 1.5,
),
),
Text(
'Make changes to your account here. Click save when you are done.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colors.mutedForeground,
),
),
const SizedBox(height: 8),
SizedBox(
width: 450,
child: Column(
children: [
const FTextField(
label: Text('Name'),
hint: 'John Renalo',
),
const SizedBox(height: 10),
const FTextField(
label: Text('Email'),
hint: '[email protected]',
),
const SizedBox(height: 16),
FButton(
onPress: () => Navigator.of(context).pop(),
child: const Text('Save'),
),
],
),
),
],
),
),
),
);
}
CLI
To generate and customize this style:
dart run forui style create sheet
Usage
showFPersistentSheet(...)
showFPersistentSheet(
context: context,
style: FSheetStyle(...),
side: FLayout.ltr,
useSafeArea: false,
keepAliveOffstage: true,
mainAxisMaxRatio: null,
constraints: const BoxConstraints(maxWidth: 450, maxHeight: 450),
draggable: true,
builder: (context, controller) => const Placeholder(),
);
Examples
With KeepAliveOffstage
Preview
Code
class Sheets extends StatefulWidget {
@override
State<Sheets> createState() => _State();
}
class _State extends State<Sheets> {
final Map<FLayout, FPersistentSheetController> _controllers = {};
@override
Widget build(BuildContext context) {
VoidCallback onPress(FLayout side) => () {
for (final MapEntry(:key, :value) in _controllers.entries) {
if (key != side && value.status.isCompleted) {
return;
}
}
var controller = _controllers[side];
if (controller == null) {
controller = _controllers[side] ??= showFPersistentSheet(
context: context,
side: FLayout.ltr,
keepAliveOffstage: true,
builder: (context, controller) => Form(side: side),
);
} else {
controller.toggle();
}
};
return FScaffold( // This can be replaced with FSheets
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 5,
children: [
FButton(
onPress: onPress(FLayout.ltr),
child: const Text('Left'),
),
FButton(
onPress: onPress(FLayout.ttb),
child: const Text('Top'),
),
FButton(
onPress: onPress(FLayout.rtl),
child: const Text('Right'),
),
FButton(
onPress: onPress(FLayout.btt),
child: const Text('Bottom'),
),
],
),
);
}
@override
void dispose() {
for (final controller in _controllers.values) {
controller.dispose();
}
super.dispose();
}
}
class Form extends StatelessWidget {
final FLayout side;
const Form({required this.side, super.key});
@override
Widget build(BuildContext context) => Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
color: context.theme.colors.background,
border: side.vertical
? Border.symmetric(horizontal: BorderSide(color: context.theme.colors.border))
: Border.symmetric(vertical: BorderSide(color: context.theme.colors.border)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Account',
style: context.theme.typography.xl2.copyWith(
fontWeight: FontWeight.w600,
color: context.theme.colors.foreground,
height: 1.5,
),
),
Text(
'Make changes to your account here. Click save when you are done.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colors.mutedForeground,
),
),
const SizedBox(height: 8),
SizedBox(
width: 450,
child: Column(
children: [
const FTextField(
label: Text('Name'),
hint: 'John Renalo',
),
const SizedBox(height: 10),
const FTextField(
label: Text('Email'),
hint: '[email protected]',
),
const SizedBox(height: 16),
FButton(
onPress: () => Navigator.of(context).pop(),
child: const Text('Save'),
),
],
),
),
],
),
),
),
);
}
With DraggableScrollableSheet
Preview
Code
class DraggableSheets extends StatefulWidget {
@override
State<DraggableSheets> createState() => _State();
}
class _State extends State<DraggableSheets> {
FPersistentSheetController? controller;
@override
Widget build(BuildContext context) => FScaffold(
child: FButton(
child: const Text('Click me'),
onPress: () {
if (controller != null) {
controller!.toggle();
return;
}
controller = showFPersistentSheet(
context: context,
side: FLayout.btt,
mainAxisMaxRatio: null,
builder: (context, _) => DraggableScrollableSheet(
expand: false,
builder: (context, controller) => ScrollConfiguration(
// This is required to enable dragging on desktop.
// See https://github.com/flutter/flutter/issues/101903 for more information.
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
},
),
child: FTileGroup.builder(
count: 25,
scrollController: controller,
tileBuilder: (context, index) => FTile(title: Text('Tile $index')),
),
),
),
);
},
),
);
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}
Last updated on