Flutter Widget Patterns
StatelessWidget vs StatefulWidget
Use StatelessWidget when UI depends only on props. Use StatefulWidget when local mutable state is needed.
// Prefer StatelessWidget + external state (Riverpod/BLoC)
class UserCard extends StatelessWidget {
const UserCard({super.key, required this.user});
final User user;
@override
Widget build(BuildContext context) {
return Card(child: Text(user.name));
}
}
// StatefulWidget for genuinely local UI state (animations, focus, text input)
class ExpandableCard extends StatefulWidget {
const ExpandableCard({super.key, required this.title, required this.child});
final String title;
final Widget child;
@override
State<ExpandableCard> createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Column(children: [
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Text(widget.title),
),
if (_expanded) widget.child,
]);
}
}
BuildContext Anti-Patterns
// BAD: storing context across async gaps
Future<void> submit(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
Navigator.of(context).pop(); // context may be stale
}
// GOOD: check mounted before using context after await
Future<void> submit(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
if (!context.mounted) return;
Navigator.of(context).pop();
}
InheritedWidget for DI
class ThemeModel extends InheritedWidget {
const ThemeModel({super.key, required this.primaryColor, required super.child});
final Color primaryColor;
static ThemeModel of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeModel>()!;
}
@override
bool updateShouldNotify(ThemeModel old) => primaryColor != old.primaryColor;
}
// Usage
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = ThemeModel.of(context).primaryColor;
return Container(color: color);
}
}
Key Usage
// GlobalKey: access state or RenderObject from outside widget tree
final formKey = GlobalKey<FormState>();
Form(key: formKey, child: ...);
formKey.currentState?.validate();
// ValueKey: differentiate list items by value (triggers rebuild correctly)
ListView(children: items.map((i) => ListTile(key: ValueKey(i.id), title: Text(i.name))).toList());
// UniqueKey: force widget recreation (e.g., reset animation)
Image(key: UniqueKey(), image: NetworkImage(url));
Composition over Inheritance
// BAD: subclassing widgets
class BigRedButton extends ElevatedButton { ... } // Avoid
// GOOD: compose with wrapper widgets
class PrimaryButton extends StatelessWidget {
const PrimaryButton({super.key, required this.label, required this.onPressed});
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) => ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: onPressed,
child: Text(label),
);
}
Layout Patterns
// Flexible vs Expanded
Row(children: [
Flexible(flex: 1, child: Container(color: Colors.red)), // can be smaller
Expanded(flex: 2, child: Container(color: Colors.blue)), // must fill
]);
// Sliver-based scrolling for performance
CustomScrollView(slivers: [
SliverAppBar(pinned: true, title: const Text('Title')),
SliverList(delegate: SliverChildBuilderDelegate(
(context, i) => ListTile(title: Text('Item $i')),
childCount: 100,
)),
]);
const Constructors
Always use const where possible — Flutter skips rebuilding const widgets.
// GOOD
const Text('Hello');
const SizedBox.shrink();
const EdgeInsets.all(16);
// Widget with const constructor
class MyWidget extends StatelessWidget {
const MyWidget({super.key}); // required for const usage at call site
}