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

  1. Always initialize state in initState() rather than in the constructor
  2. Use const constructors whenever possible to improve performance
  3. Implement proper cleanup in dispose() to prevent memory leaks
  4. Keep widgets focused on a single responsibility
  5. Use keys when managing dynamic lists or when widget state needs to persist
  6. Prefer composition over inheritance when building complex widgets
  7. Handle errors gracefully at appropriate levels in the widget tree
  8. Use appropriate state management solutions based on app complexity
  9. Follow platform-specific design guidelines when building cross-platform apps
  10. Maintain immutability unless there's a specific need for mutable state

This article was updated on January 18, 2025