Flutter Programming Idioms Reference
Composition over inheritance
Prefer building complex widgets by combining simpler ones rather than extending widget classes. This approach promotes reusability and cleaner architecture.
class CustomButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Row(children: [Icon(...), Text(...)]),
onPressed: () {...}
);
}
}
Immutable state
Widgets are immutable and their properties cannot change after creation. When state needs to change, Flutter rebuilds the widget with new values. This ensures predictable state management and better performance.
// Bad
class MutableWidget extends StatelessWidget {
String text; // Mutable field
}
// Good
class ImmutableWidget extends StatelessWidget {
final String text; // Immutable field
const ImmutableWidget({required this.text});
}
Keys for stateful elements
Use keys to maintain widget state across rebuilds and element tree modifications. Essential when the position or existence of widgets might change, especially in lists and animations.
ListView(
children: items.map((item) =>
ListTile(
key: ValueKey(item.id), // Preserves state
title: Text(item.title),
)
).toList(),
)
Late initialization
Use `late` keyword for non-nullable variables that will be initialized after declaration. Particularly useful in StatefulWidgets where initialization happens in initState().
class MyWidget extends StatefulWidget {
late final StreamController _controller;
@override
void initState() {
_controller = StreamController();
super.initState();
}
}
Context propagation
Pass BuildContext down the widget tree for accessing inherited widgets and theme data. Essential for accessing MediaQuery, Theme, and other inherited widget data throughout your app.
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context),
child: Builder(
builder: (context) => Text(
'Themed text',
style: Theme.of(context).textTheme.bodyLarge,
),
),
);
}
Callbacks for state updates
Pass callback functions to child widgets instead of sharing state directly. This maintains unidirectional data flow and makes state changes more predictable and easier to debug.
// Parent
void _handleUpdate(String newValue) {
setState(() => _value = newValue);
}
// Child
Widget build(BuildContext context) {
return TextField(
onChanged: widget.onUpdate,
);
}
Lazy loading
Use ListView.builder() for efficient rendering of large lists. This approach renders items only when they become visible, significantly improving performance and memory usage.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
)
State management separation
Keep business logic separate from UI code using state management solutions. This makes the codebase more maintainable, testable, and scalable.
// Provider pattern
class DataProvider extends ChangeNotifier {
String _data = '';
String get data => _data;
void updateData(String newData) {
_data = newData;
notifyListeners();
}
}
Widget lifecycle hooks
Use appropriate lifecycle methods in StatefulWidget for setup and cleanup. Ensures proper resource management and prevents memory leaks.
class MyWidget extends StatefulWidget {
@override
void initState() {
super.initState();
// Initialize resources
}
@override
void dispose() {
// Cleanup resources
super.dispose();
}
}
Async/await for Future operations
Use async/await syntax for handling asynchronous operations. Makes asynchronous code more readable and easier to handle errors.
Future<void> loadData() async {
try {
final result = await api.fetchData();
setState(() => data = result);
} catch (e) {
print('Error: $e');
}
}
const constructors
Use const constructors for widgets that don't depend on variable data. Allows Flutter to optimize rebuilds by reusing widget instances.
// Optimized for rebuild
const SizedBox(height: 8.0);
// Will rebuild unnecessarily
SizedBox(height: 8.0);
Extension methods
Extend existing classes with new functionality using extensions. Useful for adding common operations to built-in types without subclassing.
extension StringExtension on String {
bool get isValidEmail {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(this);
}
}
Platform-specific code
Use platform checks for conditional rendering of platform-specific widgets. Ensures your app follows platform-specific design guidelines.
Widget build(BuildContext context) {
return Platform.isIOS
? CupertinoButton(...)
: ElevatedButton(...);
}
Error boundaries
Implement error handling widgets to gracefully handle widget tree errors. Prevents app crashes and provides better user experience.
class ErrorBoundary extends StatelessWidget {
final Widget child;
@override
Widget build(BuildContext context) {
return ErrorWidget.builder = (details) {
return Center(child: Text('An error occurred'));
};
return child;
}
}
Best Practices for Using These Idioms
- Always initialize state in initState() rather than in the constructor
- Use const constructors whenever possible to improve performance
- Implement proper cleanup in dispose() to prevent memory leaks
- Keep widgets focused on a single responsibility
- Use keys when managing dynamic lists or when widget state needs to persist
- Prefer composition over inheritance when building complex widgets
- Handle errors gracefully at appropriate levels in the widget tree
- Use appropriate state management solutions based on app complexity
- Follow platform-specific design guidelines when building cross-platform apps
- Maintain immutability unless there's a specific need for mutable state