This guide demonstrates different extension patterns you can use with
AsyncRedux's context.select() method to create clean, type-safe selectors in
your Flutter apps.
- Pattern 1: Basic Extension (Recommended Minimum)
- Pattern 2: Type-Specific Selectors
- Pattern 3: Domain-Specific Selectors
- Pattern 4: Combined Selectors for Complex State
- Pattern 5: Nullable State Handling
- Recommendations
All patterns below assume the following app state structure:
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
// Your app state
class AppState {
final User user;
final List<Product> products;
final Cart cart;
final Settings settings;
AppState({
required this.user,
required this.products,
required this.cart,
required this.settings,
});
}
class User {
final String name;
final int age;
final bool isPremium;
User({required this.name, required this.age, required this.isPremium});
}
class Product {
final String id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
class Cart {
final List<Product> items;
Cart({required this.items});
}
class Settings {
final bool darkMode;
final String language;
Settings({required this.darkMode, required this.language});
}This is the recommended starting point for most apps. It provides a clean, simple API with full type inference.
extension BuildContextExtension on BuildContext {
AppState get state => getState<AppState>();
AppState read() => getRead<AppState>();
R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);
R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);
}class BasicExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Clean and simple - types are inferred!
final userName = context.select((st) => st.user.name);
final userAge = context.select((st) => st.user.age);
final isPremium = context.select((st) => st.user.isPremium);
return Column(
children: [
Text('Name: $userName'),
Text('Age: $userAge'),
Text('Premium: $isPremium'),
],
);
}
}- Simple and clean API
- Full type inference - no need to specify types repeatedly
- Minimal boilerplate
- Access to full state via
context.statewhen needed
Add type-specific methods for common types to get better IDE autocomplete and type safety.
extension TypedContextExtension on BuildContext {
AppState get state => getState<AppState>();
R _select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
// Type-specific methods for common types
String selectString(String Function(AppState state) selector) =>
_select(selector);
int selectInt(int Function(AppState state) selector) => _select(selector);
bool selectBool(bool Function(AppState state) selector) => _select(selector);
double selectDouble(double Function(AppState state) selector) =>
_select(selector);
List<T> selectList<T>(List<T> Function(AppState state) selector) =>
_select(selector);
Map<K, V> selectMap<K, V>(Map<K, V> Function(AppState state) selector) =>
_select(selector);
}class TypedExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Explicit type methods can help with IDE autocomplete
final userName = context.selectString((state) => state.user.name);
final userAge = context.selectInt((state) => state.user.age);
final isPremium = context.selectBool((state) => state.user.isPremium);
final prices = context.selectList<double>(
(state) => state.products.map((p) => p.price).toList(),
);
return Column(
children: [
Text('Name: $userName'),
Text('Age: $userAge'),
Text('Premium: $isPremium'),
Text('Prices: ${prices.join(', ')}'),
],
);
}
}- Better IDE autocomplete
- Explicit type declarations can help with complex nested types
- Still maintains type safety
Create domain-specific getters for commonly accessed data. This is ideal for large apps with many screens that repeatedly access the same state slices.
extension DomainContextExtension on BuildContext {
AppState get state => getState<AppState>();
R _select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
// User-specific selectors
User get user => _select((state) => state.user);
String get userName => _select((state) => state.user.name);
int get userAge => _select((state) => state.user.age);
bool get isPremiumUser => _select((state) => state.user.isPremium);
// Cart-specific selectors
List<Product> get cartItems => _select((state) => state.cart.items);
int get cartItemCount => _select((state) => state.cart.items.length);
double get cartTotal => _select(
(state) => state.cart.items.fold(0.0, (sum, item) => sum + item.price),
);
// Settings-specific selectors
bool get isDarkMode => _select((state) => state.settings.darkMode);
String get appLanguage => _select((state) => state.settings.language);
// Computed selectors
bool get hasItemsInCart => _select((state) => state.cart.items.isNotEmpty);
bool get isEligibleForFreeShipping => _select(
(state) =>
state.cart.items.fold(0.0, (sum, item) => sum + item.price) > 50,
);
}class DomainExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Super clean - like accessing properties!
return Column(
children: [
Text('User: ${context.userName}'),
Text('Age: ${context.userAge}'),
Text('Premium: ${context.isPremiumUser}'),
Text('Cart Items: ${context.cartItemCount}'),
Text('Cart Total: \$${context.cartTotal}'),
Text('Dark Mode: ${context.isDarkMode}'),
if (context.hasItemsInCart)
Text('Free Shipping: ${context.isEligibleForFreeShipping}'),
],
);
}
}- Extremely clean usage - reads like natural properties
- Encapsulates complex selector logic
- Great for large apps with repeated access patterns
- Centralizes state access logic
Use records or view models to select multiple related values at once, reducing the number of selector calls.
extension CombinedContextExtension on BuildContext {
R _select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
// Select multiple related values at once using records
({String name, int age, bool isPremium}) get userInfo => _select(
(state) => (
name: state.user.name,
age: state.user.age,
isPremium: state.user.isPremium,
),
);
// Select computed view models
CartSummary get cartSummary => _select(
(state) => CartSummary(
itemCount: state.cart.items.length,
total: state.cart.items.fold(0.0, (sum, item) => sum + item.price),
isEmpty: state.cart.items.isEmpty,
),
);
}
class CartSummary {
final int itemCount;
final double total;
final bool isEmpty;
CartSummary({
required this.itemCount,
required this.total,
required this.isEmpty,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CartSummary &&
itemCount == other.itemCount &&
total == other.total &&
isEmpty == other.isEmpty;
@override
int get hashCode => Object.hash(itemCount, total, isEmpty);
}class CombinedExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Get multiple values with one selector
final user = context.userInfo;
final cart = context.cartSummary;
return Column(
children: [
Text('User: ${user.name}, ${user.age} years old'),
Text('Premium: ${user.isPremium}'),
Text('Cart: ${cart.itemCount} items, \$${cart.total}'),
if (cart.isEmpty) Text('Your cart is empty'),
],
);
}
}- Reduces number of selector calls
- Groups related data logically
- View models can encapsulate complex computations
- Better performance when multiple values change together
Remember to implement == and hashCode for view model classes to ensure
proper change detection and prevent unnecessary rebuilds.
Handle optional or nullable state gracefully with default values and safe selectors.
extension NullableContextExtension on BuildContext {
R _select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
// Safe selectors with default values
String selectUserName({String defaultValue = 'Guest'}) => _select(
(state) => state.user.name.isEmpty ? defaultValue : state.user.name);
int selectUserAge({int defaultValue = 0}) =>
_select((state) => state.user.age > 0 ? state.user.age : defaultValue);
// Optional selectors
T? selectOptional<T>(T? Function(AppState state) selector) =>
_select(selector);
}class NullableExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Get values with fallbacks
final userName = context.selectUserName(defaultValue: 'Anonymous');
final userAge = context.selectUserAge(defaultValue: 18);
return Column(
children: [
Text('Name: $userName'),
Text('Age: $userAge'),
],
);
}
}- Gracefully handles missing or empty data
- Provides sensible defaults
- Reduces null checks in UI code
Use Pattern 1 (Basic Extension) for most apps:
extension BuildContextExtension on BuildContext {
AppState get state => getState<AppState>();
R select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
}This gives you:
context.statefor full state accesscontext.select((state) => ...)with automatic type inference- No need to specify AppState or return type repeatedly
If you find yourself repeatedly selecting the same types and want better IDE support, add typed methods (Pattern 2).
For complex apps with many screens, create domain-specific getters (Pattern 3) for commonly accessed data. This makes your code more readable and maintainable.
- The selector function is called on every state change to check if a rebuild is needed
- Keep selectors simple and fast
- For expensive computations, consider caching/memoization
- Avoid creating new objects in selectors unless necessary (or implement proper
==andhashCode)
Extensions make testing easier:
- You can mock the context
- Create test-specific extensions
- Selectors are pure functions that are easy to test
You can combine multiple patterns in a single extension:
extension AppContextExtension on BuildContext {
AppState get state => getState<AppState>();
// Pattern 1: Generic selector
R select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
// Pattern 3: Domain-specific selectors for common use cases
String get userName => select((state) => state.user.name);
int get cartItemCount => select((state) => state.cart.items.length);
}This provides both flexibility and convenience where you need it most.