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.

class Sheets extends StatefulWidget {
  @override
  State<Sheets> createState() => _SheetsState();
}

class _SheetsState extends State<Sheets> {
  final Map<FLayout, FPersistentSheetController> _controllers = {};

  @override
  void dispose() {
    for (final controller in _controllers.values) {
      controller.dispose();
    }
    super.dispose();
  }

  @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: side,
          builder: (context, controller) => Form(side: side, controller: controller),
        );
      } else {
        controller.toggle();
      }
    };

    return 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')),
      ],
    );
  }
}

class Form extends StatelessWidget {
  final FLayout side;
  final FPersistentSheetController controller;

  const Form({required this.side, required this.controller, 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: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        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: 'john@doe.com'),
                const SizedBox(height: 16),
                FButton(onPress: controller.toggle, child: const Text('Save')),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

CLI

To generate and customize this style:

dart run forui style create persistent-sheet

Usage

showFPersistentSheet(...)

showFPersistentSheet(
  context: context,
  side: FLayout.ltr,
  style: (style) => style.copyWith(...),
  mainAxisMaxRatio: 9 / 16,
  constraints: const BoxConstraints(maxWidth: 450, maxHeight: 450),
  draggable: true,
  anchorPoint: Offset.zero,
  useSafeArea: false,
  resizeToAvoidBottomInset: true,
  keepAliveOffstage: false,
  onClosing: () {},
  builder: (context, controller) => const Placeholder(),
);

Examples

With KeepAliveOffstage

class Sheets extends StatefulWidget {
  @override
  State<Sheets> createState() => _SheetsState();
}

class _SheetsState extends State<Sheets> {
  final Map<FLayout, FPersistentSheetController> _controllers = {};

  @override
  void dispose() {
    for (final controller in _controllers.values) {
      controller.dispose();
    }
    super.dispose();
  }

  @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: side,
          keepAliveOffstage: true,
          builder: (context, controller) => Form(side: side, controller: controller),
        );
      } else {
        controller.toggle();
      }
    };

    return 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')),
      ],
    );
  }
}

class Form extends StatelessWidget {
  final FLayout side;
  final FPersistentSheetController controller;

  const Form({required this.side, required this.controller, 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: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        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: 'john@doe.com'),
                const SizedBox(height: 16),
                FButton(onPress: controller.toggle, child: const Text('Save')),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

With DraggableScrollableSheet

class DraggableSheets extends StatefulWidget {
  @override
  State<DraggableSheets> createState() => _DraggableSheetsState();
}

class _DraggableSheetsState extends State<DraggableSheets> {
  FPersistentSheetController? controller;

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => 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')),
            ),
          ),
        ),
      );
    },
  );
}

On this page