expandable_layout_engine 0.0.2
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.
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.'),
],
),
),
),
],
),
);
}
}