12 KiB
12 KiB
Flutter Widget Patterns
Composition Patterns
1. Builder Pattern
Use when you need context-dependent widgets or performance optimization.
// LayoutBuilder for responsive design
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return _buildDesktopLayout();
} else {
return _buildMobileLayout();
}
},
)
// FutureBuilder for async data
FutureBuilder<String>(
future: _fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return Text(snapshot.data ?? 'No data');
},
)
2. State Management Pattern
Choose the right approach for your app complexity.
// Simple state with StatefulWidget
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _increment() => setState(() => _counter++);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_counter'),
ElevatedButton(
onPressed: _increment,
child: Text('Increment'),
),
],
);
}
}
// Complex state with Provider/Bloc
class CounterProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
// Usage
ChangeNotifierProvider(
create: (_) => CounterProvider(),
child: Consumer<CounterProvider>(
builder: (context, provider, child) {
return Text('Count: ${provider.counter}');
},
),
)
3. Custom Widget Pattern
Create reusable, themed components.
class CustomCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
final Color? backgroundColor;
final double? elevation;
const CustomCard({
required this.child,
this.padding,
this.backgroundColor,
this.elevation,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation ?? 4,
color: backgroundColor ?? Theme.of(context).cardColor,
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: child,
),
);
}
}
// Usage with theme integration
class ThemedCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
const ThemedCard({required this.child, this.padding});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Theme.of(context).shadowColor.withOpacity(0.1),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: child,
),
);
}
}
Layout Patterns
1. Responsive Grid
Adaptive grid layouts for different screen sizes.
class ResponsiveGrid extends StatelessWidget {
final List<Widget> children;
final int mobileColumns;
final int tabletColumns;
final int desktopColumns;
const ResponsiveGrid({
required this.children,
this.mobileColumns = 1,
this.tabletColumns = 2,
this.desktopColumns = 3,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
int columns;
if (constraints.maxWidth < 600) {
columns = mobileColumns;
} else if (constraints.maxWidth < 1200) {
columns = tabletColumns;
} else {
columns = desktopColumns;
}
return GridView.count(
crossAxisCount: columns,
children: children,
);
},
);
}
}
2. Expandable Section
Collapsible content with smooth animations.
class ExpandableSection extends StatefulWidget {
final Widget title;
final Widget content;
final bool initiallyExpanded;
const ExpandableSection({
required this.title,
required this.content,
this.initiallyExpanded = false,
});
@override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
if (widget.initiallyExpanded) {
_isExpanded = true;
_controller.value = 1.0;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: _toggle,
child: Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(child: widget.title),
AnimatedRotation(
turns: _isExpanded ? 0.5 : 0,
duration: Duration(milliseconds: 300),
child: Icon(Icons.expand_more),
),
],
),
),
),
SizeTransition(
sizeFactor: _animation,
child: widget.content,
),
],
);
}
}
Interaction Patterns
1. Swipeable List Item
Gestures for list interactions.
class SwipeableListItem extends StatefulWidget {
final Widget child;
final VoidCallback? onSwipeLeft;
final VoidCallback? onSwipeRight;
final Widget? leftAction;
final Widget? rightAction;
const SwipeableListItem({
required this.child,
this.onSwipeLeft,
this.onSwipeRight,
this.leftAction,
this.rightAction,
});
@override
_SwipeableListItemState createState() => _SwipeableListItemState();
}
class _SwipeableListItemState extends State<SwipeableListItem>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 200),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
if (details.delta.dx > 0 && widget.onSwipeRight != null) {
_controller.forward();
} else if (details.delta.dx < 0 && widget.onSwipeLeft != null) {
_controller.forward();
}
},
onPanEnd: (details) {
if (_controller.value > 0.5) {
if (details.velocity.pixelsPerSecond.dx > 0) {
widget.onSwipeRight?.call();
} else {
widget.onSwipeLeft?.call();
}
}
_controller.reverse();
},
child: Stack(
children: [
widget.child,
if (widget.leftAction != null)
Positioned.fill(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_animation.value * -100, 0),
child: widget.leftAction,
);
},
),
),
if (widget.rightAction != null)
Positioned.fill(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_animation.value * 100, 0),
child: widget.rightAction,
);
},
),
),
],
),
);
}
}
2. Pull to Refresh
Custom pull-to-refresh implementation.
class CustomPullToRefresh extends StatefulWidget {
final Future<void> Function() onRefresh;
final Widget child;
final double refreshTriggerOffset;
const CustomPullToRefresh({
required this.onRefresh,
required this.child,
this.refreshTriggerOffset = 80.0,
});
@override
_CustomPullToRefreshState createState() => _CustomPullToRefreshState();
}
class _CustomPullToRefreshState extends State<CustomPullToRefresh>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _isRefreshing = false;
double _dragOffset = 0.0;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _handleRefresh() async {
setState(() => _isRefreshing = true);
await widget.onRefresh();
setState(() => _isRefreshing = false);
_controller.reverse();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _handleRefresh,
child: CustomScrollView(
physics: AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _animation.value * _dragOffset),
child: child,
);
},
child: widget.child,
),
),
],
),
);
}
}
Performance Patterns
1. ListView Optimization
Efficient list rendering with builders.
class OptimizedListView extends StatelessWidget {
final List<Item> items;
const OptimizedListView({required this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return _buildItem(items[index]);
},
// Use const keys for better performance
key: ValueKey('list_${items.length}'),
);
}
Widget _buildItem(Item item) {
return Card(
key: ValueKey(item.id), // Unique key for each item
child: ListTile(
title: Text(item.title),
subtitle: Text(item.description),
// Use const for static widgets
leading: const Icon(Icons.article),
),
);
}
}
2. RepaintBoundary
Isolate expensive widget rebuilds.
class ComplexAnimation extends StatefulWidget {
@override
_ComplexAnimationState createState() => _ComplexAnimationState();
}
class _ComplexAnimationState extends State<ComplexAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: ComplexPainter(_controller.value),
child: child,
);
},
child: Container(), // Static child doesn't rebuild
),
);
}
}