expandable_layout_engine 0.0.2 copy "expandable_layout_engine: ^0.0.2" to clipboard
expandable_layout_engine: ^0.0.2 copied to clipboard

A powerful, lightweight, and customizable Flutter package for building expandable and collapsible sections, accordions, and dynamic layouts.

example/lib/main.dart

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

void main() {
  runApp(const ComplexExampleApp());
}

class ComplexExampleApp extends StatelessWidget {
  const ComplexExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Expandable Layout Engine Showcase',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.light,
        ),
        cardTheme: const CardTheme(
          elevation: 0,
          clipBehavior: Clip.antiAlias,
        ),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.dark,
        ),
      ),
      home: const ShowcaseHome(),
    );
  }
}

class ShowcaseHome extends StatefulWidget {
  const ShowcaseHome({super.key});

  @override
  State<ShowcaseHome> createState() => _ShowcaseHomeState();
}

class _ShowcaseHomeState extends State<ShowcaseHome> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const SettingsDashboard(),
    const CheckoutFormFlow(),
    const NestedPlayground(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          NavigationRail(
            selectedIndex: _currentIndex,
            onDestinationSelected: (int index) {
              setState(() {
                _currentIndex = index;
              });
            },
            labelType: NavigationRailLabelType.all,
            destinations: const [
              NavigationRailDestination(
                icon: Icon(Icons.dashboard_outlined),
                selectedIcon: Icon(Icons.dashboard),
                label: Text('Settings'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.shopping_cart_outlined),
                selectedIcon: Icon(Icons.shopping_cart),
                label: Text('Checkout'),
              ),
              NavigationRailDestination(
                icon: Icon(Icons.layers_outlined),
                selectedIcon: Icon(Icons.layers),
                label: Text('Nested'),
              ),
            ],
          ),
          const VerticalDivider(thickness: 1, width: 1),
          Expanded(
            child: _pages[_currentIndex],
          ),
        ],
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// PAGE 1: Settings Dashboard (Lists, Switches, Simple Expandables)
// -----------------------------------------------------------------------------
class SettingsDashboard extends StatefulWidget {
  const SettingsDashboard({super.key});

  @override
  State<SettingsDashboard> createState() => _SettingsDashboardState();
}

class _SettingsDashboardState extends State<SettingsDashboard> {
  final ExpandableController _profileController = ExpandableController();

  @override
  void dispose() {
    _profileController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('App Settings')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildInfoCard(context),
          const SizedBox(height: 20),
          const Text('PREFERENCES',
              style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2)),
          const SizedBox(height: 10),
          Card(
            color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
            child: Column(
              children: [
                ExpandableListTile(
                  leading: const Icon(Icons.notifications),
                  title: const Text('Notifications'),
                  subtitle: const Text('Manage alerts & sounds'),
                  body: Padding(
                    padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
                    child: Column(
                      children: [
                        SwitchListTile(
                          title: const Text('Push Notifications'),
                          value: true,
                          onChanged: (_) {},
                        ),
                        SwitchListTile(
                          title: const Text('Email Digest'),
                          value: false,
                          onChanged: (_) {},
                        ),
                      ],
                    ),
                  ),
                ),
                const Divider(height: 1),
                ExpandableListTile(
                  leading: const Icon(Icons.lock),
                  title: const Text('Privacy & Security'),
                  body: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text('Data Sharing'),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            FilterChip(
                                label: const Text('Analytics'),
                                selected: true,
                                onSelected: (_) {}),
                            const SizedBox(width: 8),
                            FilterChip(
                                label: const Text('Marketing'),
                                selected: false,
                                onSelected: (_) {}),
                          ],
                        ),
                        const SizedBox(height: 16),
                        FilledButton.tonal(
                          onPressed: () {},
                          child: const Text('Change Password'),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoCard(BuildContext context) {
    final theme = Theme.of(context);
    // Custom built card for styling and explicit controller
    return ExpandableCard(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      controller: _profileController,
      header: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Row(
          children: [
            CircleAvatar(
              backgroundColor: theme.colorScheme.primaryContainer,
              child: Icon(Icons.person, color: theme.colorScheme.primary),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('User Profile', style: theme.textTheme.titleMedium),
                  Text('Tap to view details', style: theme.textTheme.bodySmall),
                ],
              ),
            ),
            ExpandableIcon(controller: _profileController),
          ],
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
        child: Column(
          children: [
            const Divider(),
            _buildRow('Email', 'user@example.com'),
            _buildRow('Member Since', 'Jan 2024'),
            _buildRow('Status', 'Active Pro Member'),
          ],
        ),
      ),
    );
  }

  Widget _buildRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: const TextStyle(fontWeight: FontWeight.w500)),
          Text(value, style: const TextStyle(color: Colors.grey)),
        ],
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// PAGE 2: Checkout Form Flow (Accordion / Group)
// -----------------------------------------------------------------------------
class CheckoutFormFlow extends StatefulWidget {
  const CheckoutFormFlow({super.key});

  @override
  State<CheckoutFormFlow> createState() => _CheckoutFormFlowState();
}

class _CheckoutFormFlowState extends State<CheckoutFormFlow> {
  // Controllers explicitly managed to allow ExpandableIcon access
  final ExpandableController _shippingController = ExpandableController(initialExpanded: true);
  final ExpandableController _paymentController = ExpandableController();
  final ExpandableController _reviewController = ExpandableController();

  @override
  void dispose() {
    _shippingController.dispose();
    _paymentController.dispose();
    _reviewController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Checkout Flow')),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {},
        icon: const Icon(Icons.check),
        label: const Text('Complete Order'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            const Text(
              'Only one section can be open at a time.',
              style: TextStyle(fontStyle: FontStyle.italic),
            ),
            const SizedBox(height: 16),
            ExpandableGroup(
              children: [
                _buildStep(
                  context,
                  title: '1. Shipping Address',
                  icon: Icons.local_shipping,
                  controller: _shippingController,
                  content: Column(
                    children: [
                      TextFormField(
                        decoration: const InputDecoration(
                          labelText: 'Street Address',
                          border: OutlineInputBorder(),
                        ),
                      ),
                      const SizedBox(height: 12),
                      Row(
                        children: [
                          Expanded(
                            child: TextFormField(
                              decoration: const InputDecoration(
                                labelText: 'City',
                                border: OutlineInputBorder(),
                              ),
                            ),
                          ),
                          const SizedBox(width: 12),
                          Expanded(
                            child: TextFormField(
                              decoration: const InputDecoration(
                                labelText: 'ZIP Code',
                                border: OutlineInputBorder(),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                _buildStep(
                  context,
                  title: '2. Payment Method',
                  icon: Icons.credit_card,
                  controller: _paymentController,
                  content: Column(
                    children: [
                      SegmentedButton<String>(
                        segments: const [
                          ButtonSegment(value: 'card', label: Text('Card')),
                          ButtonSegment(value: 'paypal', label: Text('PayPal')),
                          ButtonSegment(value: 'apple', label: Text('Apple Pay')),
                        ],
                        selected: const {'card'},
                        // onSelectionChanged: ...
                      ),
                      const SizedBox(height: 20),
                      TextFormField(
                        decoration: const InputDecoration(
                          hintText: '0000 0000 0000 0000',
                          labelText: 'Card Number',
                          border: OutlineInputBorder(),
                          prefixIcon: Icon(Icons.credit_card),
                        ),
                      ),
                    ],
                  ),
                ),
                _buildStep(
                  context,
                  title: '3. Order Review',
                  icon: Icons.receipt_long,
                  controller: _reviewController,
                  content: Container(
                    padding: const EdgeInsets.all(12),
                    decoration: BoxDecoration(
                      color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Column(
                      children: [
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [Text('Subtotal'), Text('\$120.00')],
                        ),
                        SizedBox(height: 8),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [Text('Shipping'), Text('\$5.00')],
                        ),
                        Divider(),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Text(
                              'Total',
                              style: TextStyle(fontWeight: FontWeight.bold),
                            ),
                            Text(
                              '\$125.00',
                              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStep(
    BuildContext context, {
    required String title,
    required IconData icon,
    required Widget content,
    required ExpandableController controller,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Material(
        elevation: 2,
        borderRadius: BorderRadius.circular(12),
        color: Theme.of(context).cardColor,
        child: ExpandableSection(
          controller: controller, // Explicit controller
          header: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Icon(icon, color: Theme.of(context).colorScheme.primary),
                const SizedBox(width: 16),
                Expanded(
                    child: Text(title,
                        style: const TextStyle(
                            fontSize: 16, fontWeight: FontWeight.bold))),
                ExpandableIcon(controller: controller),
              ],
            ),
          ),
          body: Padding(
            padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
            child: content,
          ),
        ),
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// PAGE 3: Nested Playground (Programmatic Control & Nesting)
// -----------------------------------------------------------------------------
class NestedPlayground extends StatefulWidget {
  const NestedPlayground({super.key});

  @override
  State<NestedPlayground> createState() => _NestedPlaygroundState();
}

class _NestedPlaygroundState extends State<NestedPlayground> {
  final ExpandableController _mainController = ExpandableController();
  final ExpandableController _nestedController = ExpandableController();

  @override
  void dispose() {
    _mainController.dispose();
    _nestedController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Controller & Nesting')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              FilledButton.tonal(
                onPressed: _mainController.toggle,
                child: const Text('Toggle Main'),
              ),
              OutlinedButton(
                onPressed: _nestedController.expand,
                child: const Text('Open Inner'),
              ),
            ],
          ),
          const SizedBox(height: 20),
          ExpandableSection(
            controller: _mainController,
            header: Container(
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Colors.blue.shade100,
                borderRadius: BorderRadius.circular(12),
                border: Border.all(color: Colors.blue.shade300),
              ),
              child: Row(
                children: [
                  const Icon(Icons.layers, color: Colors.blue),
                  const SizedBox(width: 12),
                  const Text('Outer Parent Section',
                      style: TextStyle(fontSize: 18, color: Colors.blue)),
                  const Spacer(),
                  ExpandableIcon(controller: _mainController),
                ],
              ),
            ),
            body: Container(
              margin: const EdgeInsets.only(top: 8),
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.blue.shade50,
                borderRadius: BorderRadius.circular(12),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'This is the content of the outer section. It contains another expandable section inside it.',
                    style: TextStyle(height: 1.5),
                  ),
                  const SizedBox(height: 16),
                  
                  // NESTED SECTION
                  ExpandableSection(
                    controller: _nestedController,
                    header: Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Colors.purple.shade100,
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(color: Colors.purple.shade300),
                      ),
                      child: Row(
                        children: [
                          const Icon(Icons.subdirectory_arrow_right,
                              color: Colors.purple),
                          const SizedBox(width: 12),
                          const Text('Inner Nested Section',
                              style: TextStyle(color: Colors.purple, fontWeight: FontWeight.bold)),
                          const Spacer(),
                          ExpandableIcon(controller: _nestedController),
                        ],
                      ),
                    ),
                    body: Container(
                      margin: const EdgeInsets.only(top: 8),
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Colors.purple.shade50,
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: const Text(
                        'This content is double-nested! \n\nThe parent container automatically adjusts its height when this inner section animates.\n\nTry toggling this section or the parent section independently.',
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  const Text('End of outer section content.'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
160
points
64
downloads

Publisher

verified publisherdivyanshdev.tech

Weekly Downloads

A powerful, lightweight, and customizable Flutter package for building expandable and collapsible sections, accordions, and dynamic layouts.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on expandable_layout_engine