scaffold_plus 1.0.2 copy "scaffold_plus: ^1.0.2" to clipboard
scaffold_plus: ^1.0.2 copied to clipboard

Drop-in replacement for Flutter's Scaffold: background image layer, SafeArea toggles, AnimatedSwitcher body transitions, tap-to-unfocus; proxies core Scaffold params.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:scaffold_plus/scaffold_plus.dart';

// Run this file as your main.dart to see ScaffoldPlus in action.
// Assets: if you toggle the background image, make sure you have
// an asset at assets/images/bg.png and it is declared in pubspec.yaml.
//
// flutter:
//   assets:
//     - assets/images/bg.png
//
// This example shows:
// - background image layer
// - SafeArea toggles and paddings
// - tap-to-unfocus (try the TextField)
// - AnimatedSwitcher body transitions (list <-> details <-> settings)
// - proxied Scaffold params: AppBar, FAB, drawers, bottom nav, bottom sheet
void main() => runApp(const ScaffoldPlusDemoApp());

class ScaffoldPlusDemoApp extends StatefulWidget {
  const ScaffoldPlusDemoApp({super.key});
  @override
  State<ScaffoldPlusDemoApp> createState() => _ScaffoldPlusDemoAppState();
}

class _ScaffoldPlusDemoAppState extends State<ScaffoldPlusDemoApp> {
  ThemeMode _mode = ThemeMode.system;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'ScaffoldPlus Demo',
      themeMode: _mode,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
      ),
      home: DemoHome(
        onToggleTheme: () {
          setState(() {
            _mode = _mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
          });
        },
        themeMode: _mode,
      ),
    );
  }
}

class DemoHome extends StatefulWidget {
  const DemoHome({
    super.key,
    required this.onToggleTheme,
    required this.themeMode,
  });
  final VoidCallback onToggleTheme;
  final ThemeMode themeMode;

  @override
  State<DemoHome> createState() => _DemoHomeState();
}

enum BodyMode { list, details, settings }

class _DemoHomeState extends State<DemoHome> {
  // UI state
  int _selectedNav = 0; // 0: Explore, 1: Settings (same as BodyMode.settings)
  BodyMode _mode = BodyMode.list;
  int? _selectedId;
  int _counter = 0;

  // ScaffoldPlus extras
  bool _useBg = false;
  bool _safeTop = true;
  bool _safeBottom = false;
  double _hPad = 16;
  double _vPad = 12;

  // For bottom sheet demo
  void _showQuickSheet() {
    showModalBottomSheet<void>(
      context: context,
      showDragHandle: true,
      builder: (ctx) => Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Quick actions', style: Theme.of(ctx).textTheme.titleLarge),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: [
                FilledButton(
                  onPressed: () => Navigator.pop(ctx),
                  child: const Text('Close'),
                ),
                OutlinedButton(
                  onPressed: () {
                    Navigator.pop(ctx);
                    setState(() => _counter++);
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Counter incremented')),
                    );
                  },
                  child: const Text('Increment counter'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    // Decide which body to show; changing this triggers AnimatedSwitcher.
    Widget body;
    if (_selectedNav == 1 || _mode == BodyMode.settings) {
      body = _SettingsPanel(
        useBg: _useBg,
        safeTop: _safeTop,
        safeBottom: _safeBottom,
        hPad: _hPad,
        vPad: _vPad,
        onChanged: (useBg, safeTop, safeBottom, hPad, vPad) {
          setState(() {
            _useBg = useBg;
            _safeTop = safeTop;
            _safeBottom = safeBottom;
            _hPad = hPad;
            _vPad = vPad;
          });
        },
      );
    } else if (_mode == BodyMode.details && _selectedId != null) {
      body = _DetailView(
        key: ValueKey('detail-$_selectedId'),
        id: _selectedId!,
        onBack: () => setState(() => _mode = BodyMode.list),
      );
    } else {
      body = _MasterList(
        key: const ValueKey('master'),
        onOpen: (id) => setState(() {
          _selectedId = id;
          _mode = BodyMode.details;
        }),
      );
    }

    return ScaffoldPlus(
      appBar: AppBar(
        title: const Text('ScaffoldPlus Demo'),
        actions: [
          IconButton(
            tooltip: 'Theme: toggle light/dark',
            onPressed: widget.onToggleTheme,
            icon: Icon(
              widget.themeMode == ThemeMode.dark
                  ? Icons.dark_mode
                  : Icons.light_mode,
            ),
          ),
          IconButton(
            tooltip: 'Show bottom sheet',
            onPressed: _showQuickSheet,
            icon: const Icon(Icons.keyboard_arrow_up),
          ),
        ],
      ),
      drawer: Drawer(
        child: SafeArea(
          child: ListView(
            padding: EdgeInsets.zero,
            children: [
              const ListTile(
                title: Text('Demo drawer'),
                subtitle: Text('This proves Scaffold params are proxied'),
              ),
              SwitchListTile(
                title: const Text('Use background image'),
                value: _useBg,
                onChanged: (v) => setState(() => _useBg = v),
              ),
              SwitchListTile(
                title: const Text('SafeArea top'),
                value: _safeTop,
                onChanged: (v) => setState(() => _safeTop = v),
              ),
              SwitchListTile(
                title: const Text('SafeArea bottom'),
                value: _safeBottom,
                onChanged: (v) => setState(() => _safeBottom = v),
              ),
            ],
          ),
        ),
      ),
      endDrawer: Drawer(
        child: SafeArea(
          child: ListView(
            padding: const EdgeInsets.all(16),
            children: [
              Text('About', style: Theme.of(context).textTheme.titleLarge),
              const SizedBox(height: 8),
              const Text(
                'ScaffoldPlus is a drop-in replacement for Scaffold with handy extras.',
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => setState(() => _counter++),
        label: Text('$_counter'),
        icon: const Icon(Icons.add),
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedNav,
        onDestinationSelected: (i) => setState(() {
          _selectedNav = i;
          _mode = i == 1 ? BodyMode.settings : BodyMode.list;
        }),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.explore_outlined),
            selectedIcon: Icon(Icons.explore),
            label: 'Explore',
          ),
          NavigationDestination(
            icon: Icon(Icons.settings_outlined),
            selectedIcon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
      ),

      // ---- ScaffoldPlus extras ----
      useBackgroundImage: _useBg,
      backgroundImage:
          'assets/images/bg.png', // change or disable via drawer/settings
      safeAreaTop: _safeTop,
      safeAreaBottom: _safeBottom,
      horizontalPadding: _hPad,
      verticalPadding: _vPad,
      switchDuration: const Duration(milliseconds: 250),
      onTapOutside: () => debugPrint('Tap outside -> unfocus'),
      // The magic: AnimatedSwitcher will animate whenever "body" changes identity.
      body: body,
    );
  }
}

// ----------------- Widgets used inside the demo -----------------

class _MasterList extends StatelessWidget {
  const _MasterList({super.key, required this.onOpen});
  final ValueChanged<int> onOpen;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const SizedBox(height: 8),
        const _SearchField(),
        const SizedBox(height: 8),
        Expanded(
          child: ListView.separated(
            itemCount: 20,
            separatorBuilder: (_, __) => const Divider(height: 1),
            itemBuilder: (ctx, i) => ListTile(
              leading: CircleAvatar(child: Text('${i + 1}')),
              title: Text('Item #${i + 1}'),
              subtitle: const Text(
                'Tap to open details (AnimatedSwitcher demo)',
              ),
              onTap: () => onOpen(i + 1),
            ),
          ),
        ),
      ],
    );
  }
}

class _DetailView extends StatelessWidget {
  const _DetailView({super.key, required this.id, required this.onBack});
  final int id;
  final VoidCallback onBack;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Row(
          children: [
            IconButton(onPressed: onBack, icon: const Icon(Icons.arrow_back)),
            Text(
              'Details for item #$id',
              style: Theme.of(context).textTheme.titleLarge,
            ),
          ],
        ),
        const SizedBox(height: 8),
        Text(
          'This page is different from the master list. '
          'Switching between them uses AnimatedSwitcher provided by ScaffoldPlus.',
        ),
        const SizedBox(height: 16),
        FilledButton.icon(
          onPressed: onBack,
          icon: const Icon(Icons.chevron_left),
          label: const Text('Back to list'),
        ),
      ],
    );
  }
}

class _SearchField extends StatelessWidget {
  const _SearchField();
  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(
        hintText: 'Tap here, then tap outside to see unfocus behavior',
        prefixIcon: const Icon(Icons.search),
        border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
      ),
    );
  }
}

class _SettingsPanel extends StatelessWidget {
  const _SettingsPanel({
    required this.useBg,
    required this.safeTop,
    required this.safeBottom,
    required this.hPad,
    required this.vPad,
    required this.onChanged,
  });

  final bool useBg;
  final bool safeTop;
  final bool safeBottom;
  final double hPad;
  final double vPad;
  final void Function(
    bool useBg,
    bool safeTop,
    bool safeBottom,
    double hPad,
    double vPad,
  )
  onChanged;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        SwitchListTile(
          title: const Text('Use background image'),
          value: useBg,
          onChanged: (v) => onChanged(v, safeTop, safeBottom, hPad, vPad),
        ),
        SwitchListTile(
          title: const Text('SafeArea top'),
          value: safeTop,
          onChanged: (v) => onChanged(useBg, v, safeBottom, hPad, vPad),
        ),
        SwitchListTile(
          title: const Text('SafeArea bottom'),
          value: safeBottom,
          onChanged: (v) => onChanged(useBg, safeTop, v, hPad, vPad),
        ),
        ListTile(
          title: const Text('Horizontal padding'),
          subtitle: Slider(
            value: hPad,
            min: 0,
            max: 32,
            divisions: 32,
            label: hPad.round().toString(),
            onChanged: (v) => onChanged(useBg, safeTop, safeBottom, v, vPad),
          ),
        ),
        ListTile(
          title: const Text('Vertical padding'),
          subtitle: Slider(
            value: vPad,
            min: 0,
            max: 32,
            divisions: 32,
            label: vPad.round().toString(),
            onChanged: (v) => onChanged(useBg, safeTop, safeBottom, hPad, v),
          ),
        ),
        const Divider(),
        ListTile(
          title: const Text('Notes'),
          subtitle: const Text(
            'AnimatedSwitcher runs when the body widget changes. '
            'This happens when you switch between Explore (list/details) '
            'and Settings using the bottom navigation bar.',
          ),
        ),
      ],
    );
  }
}
2
likes
160
points
7
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Drop-in replacement for Flutter's Scaffold: background image layer, SafeArea toggles, AnimatedSwitcher body transitions, tap-to-unfocus; proxies core Scaffold params.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on scaffold_plus