Flutter State Management Mastery
State management is the **cornerstone of modern Flutter development**. It's the difference between a chaotic, unmaintainable codebase and an elegant, scalable architecture that grows with your busines
$ Installer
git clone https://github.com/pluginagentmarketplace/custom-plugin-flutter ~/.claude/skills/custom-plugin-flutter// tip: Run this command in your terminal to install the skill
Flutter State Management Mastery
The Complete Roadmap for Building Scalable, Performant Applications
Executive Summary
State management is the cornerstone of modern Flutter development. It's the difference between a chaotic, unmaintainable codebase and an elegant, scalable architecture that grows with your business. This comprehensive guide explores proven patterns, industry best practices, and production-tested strategies for managing state across Flutter applications of any scale.
Whether you're building a startup MVP or an enterprise-grade platform, understanding state management will elevate your development capabilities and enable you to architect applications that are:
- Performant: Minimal rebuilds, optimized memory usage
- Testable: Pure functions, mockable dependencies
- Maintainable: Clear separation of concerns
- Scalable: Handle complexity without spaghetti code
- Resilient: Proper error handling and state recovery
Table of Contents
- State Management Patterns: A Comprehensive Overview
- Decision Matrix: Choosing Your Pattern
- Context & Dependency Injection Deep Dive
- State Persistence & Data Serialization
- Testing Stateful Widgets at Scale
- Performance Optimization Strategies
- Enterprise-Grade Best Practices
State Management Patterns: Comprehensive Overview
1. setState - The Foundation Every Developer Must Master
setState is Flutter's built-in mechanism for managing widget-level state. It's the simplest approach and forms the foundation for understanding more advanced patterns.
How It Works
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int count = 0;
void increment() {
setState(() {
count++; // Mark widget as dirty
});
// Framework rebuilds only this widget
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: increment,
child: const Text('Increment'),
),
],
);
}
}
Key Characteristics
- Local Scope: State lives exclusively within the widget
- Direct Manipulation: Modify state directly in callbacks
- Synchronous Updates: UI updates immediately after setState
- Minimal Boilerplate: Zero external dependencies
- Widget Lifecycle: Tightly coupled to widget's lifespan
Advantages
โ Zero learning curve for beginners
โ No external dependencies
โ Perfect for isolated components
โ Excellent for teaching Flutter fundamentals
Disadvantages
โ Limited to single widget scope
โ Poor for sharing state across screens
โ Difficult to test business logic
โ Causes unnecessary rebuilds in large trees
โ Not suitable for complex applications
Best For
- Simple, isolated widgets (buttons, toggles, local counters)
- Single-screen features (form validation, input fields)
- Rapid prototyping and learning
- Teaching Flutter basics
2. Provider - The Industry Standard for Modern Flutter
Provider elegantly combines dependency injection with reactive programming, offering the perfect balance of simplicity and power for most Flutter applications.
The Provider Philosophy
Provider is built on three core concepts:
- InheritedWidget: Flutter's native mechanism for propagating values down the widget tree
- ChangeNotifier: Observable pattern for state changes
- Consumer: Reactive widgets that rebuild when dependencies change
Architecture Deep Dive
// Step 1: Define your state model
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Trigger rebuilds for all listeners
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}
// Step 2: Provide to widget tree
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
// Step 3: Consume in UI widgets
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
// Option A: Direct watch + rebuild entire widget
child: Consumer<CounterModel>(
builder: (context, counter, child) {
return Text('Count: ${counter.count}');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
);
}
}
Advanced Provider Patterns
Selector for Fine-Grained Performance Control
// Only rebuild when specific property changes
// This is critical for performance in large applications
Selector<CounterModel, int>(
selector: (context, model) => model.count,
builder: (context, count, child) {
debugPrint('Counter widget rebuilt'); // Prints only when count changes
return Text('Count: $count');
},
)
ProxyProvider for Combining Dependencies
// Elegantly compose multiple providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthService()),
ChangeNotifierProvider(create: (_) => UserService()),
// UserViewModel depends on both AuthService and UserService
ChangeNotifierProxyProvider2<AuthService, UserService, UserViewModel>(
create: (context) => UserViewModel(),
update: (context, authService, userService, previousViewModel) {
return previousViewModel
..authService = authService
..userService = userService;
},
),
],
child: const MyApp(),
)
FutureProvider for Async Data Loading
// Perfect for API calls, database queries
final userProvider = FutureProvider.autoDispose<User>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/user/123'));
return User.fromJson(jsonDecode(response.body));
});
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<AsyncValue<User>>(
builder: (context, userAsync, child) {
return userAsync.when(
data: (user) => Text('Hello, ${user.name}'),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
},
);
}
}
Key Characteristics
- Flexible: Works with ChangeNotifier, Future, or Stream
- Scalable: Manages complex state hierarchies elegantly
- Testable: Easy dependency injection for testing
- Performance: Fine-grained rebuilds with Selector
- Ecosystem: Excellent documentation and community
Advantages
โ Excellent documentation and learning resources
โ Proven in hundreds of production applications
โ Works with simple state (ChangeNotifier) and complex (Stream, Future)
โ Great for team projects with clear patterns
โ Easy to scale from simple to complex apps
Disadvantages
โ Requires BuildContext in some scenarios
โ Not compile-safe (runtime discovery)
โ Can become complex with many interdependent providers
โ Learning curve for dependency injection concepts
Best For
- Medium to large applications
- Shared state across multiple screens
- Teams prioritizing maintainability
- Projects that need to grow incrementally
3. Riverpod - The Modern Evolution & Future of State Management
Riverpod is the next-generation state management solution addressing Provider's limitations while maintaining familiar patterns. It's compile-safe, efficient, and perfect for production applications.
Revolutionary Design Principles
Riverpod introduces game-changing features:
- Compile-Time Safety: Caught at compile time, not runtime
- No BuildContext: Eliminates entire class of bugs
- Powerful Composition: Providers combine seamlessly
- Family Modifiers: Dynamic provider parameterization
- Native Async: First-class support for Future and Stream
Core Concepts
// 1. Simple State Provider
final counterProvider = StateProvider<int>((ref) => 0);
// 2. State with Business Logic (StateNotifier)
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
final counterProvider = StateNotifierProvider<CounterNotifier, int>(
(ref) => CounterNotifier(),
);
// 3. Async Data Provider (Native async handling!)
final userProvider = FutureProvider.autoDispose<User>((ref) async {
final apiService = ref.watch(apiServiceProvider);
return apiService.fetchUser('123');
});
// 4. Family for Parameterized Providers
final userProvider = FutureProvider.family.autoDispose<User, String>(
(ref, userId) async {
final apiService = ref.watch(apiServiceProvider);
return apiService.fetchUser(userId);
},
);
// 5. Computed Providers (Derived State)
final isUserPremiumProvider = Provider.autoDispose<bool>((ref) {
final user = ref.watch(userProvider);
return user.when(
data: (user) => user.isPremium,
loading: () => false,
error: (_, __) => false,
);
});
// Usage in widgets
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: const Text('Increment'),
),
],
);
}
}
// Async data with automatic loading/error states
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => UserProfile(user: user),
loading: () => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
error: (error, stackTrace) => Scaffold(
body: Center(child: Text('Error: $error')),
),
);
}
}
Advanced Riverpod Patterns
Stream Providers for Real-Time Data
// Watch real-time updates from backend
final liveNotificationsProvider = StreamProvider.autoDispose<Notification>((ref) {
return ref.watch(webSocketServiceProvider).notificationStream;
});
class NotificationCenter extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifications = ref.watch(liveNotificationsProvider);
return notifications.when(
data: (notification) => Text('New: ${notification.message}'),
loading: () => const Text('Waiting for notifications...'),
error: (error, _) => Text('Connection error: $error'),
);
}
}
Provider Invalidation & Refresh
class UserController extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
return user.when(
data: (user) => Column(
children: [
Text(user.name),
ElevatedButton(
onPressed: () {
// Invalidate provider to refetch
ref.refresh(userProvider);
},
child: const Text('Refresh'),
),
],
),
loading: () => const CircularProgressIndicator(),
error: (error, _) => Text('Error: $error'),
);
}
}
Key Characteristics
- Type-Safe: Compile-time checking prevents entire classes of bugs
- Powerful: Native async, side effects, computed state
- Modular: Providers compose like building blocks
- Efficient: Advanced caching and invalidation
- Modern: Designed for async-first applications
Advantages
โ Compile-time safety (catch bugs before runtime)
โ No BuildContext needed (eliminates whole categories of bugs)
โ Native async/Future handling
โ Family modifiers for dynamic providers
โ Superior performance for complex apps
โ Future of Flutter state management
Disadvantages
โ Steeper learning curve than Provider
โ Smaller ecosystem (but growing rapidly)
โ Breaking changes in major versions
โ Requires hooks_riverpod for some features
Best For
- Production applications requiring type safety
- Complex async state management
- Teams that prioritize testing
- APIs with data fetching and real-time updates
- New projects starting with best practices
4. GetX - The All-in-One Rapid Development Framework
GetX provides a comprehensive solution combining state management, routing, dependency injection, and utilities into one cohesive package. It's perfect for teams prioritizing rapid development.
Integrated Ecosystem
// Step 1: Define Reactive State with GetX
class CounterController extends GetxController {
// Reactive variables using .obs
final RxInt count = 0.obs;
final RxString message = 'Hello'.obs;
final RxList<String> items = <String>[].obs;
// Reactive getters (computed values)
RxBool get isPositive => count > 0 ? true.obs : false.obs;
// Methods
void increment() => count++;
void updateMessage(String text) => message.value = text;
void addItem(String item) => items.add(item);
// Lifecycle
@override
void onInit() {
super.onInit();
// Initialize when controller is created
}
@override
void onReady() {
super.onReady();
// Called after widget is rendered
}
@override
void onClose() {
super.onClose();
// Clean up resources
}
}
// Step 2: Inject and Use
class CounterScreen extends StatelessWidget {
final controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GetX Counter')),
body: Center(
// Reactive widget - rebuilds automatically
child: Obx(
() => Text('Count: ${controller.count.value}'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}
// Step 3: Navigation (Built-in!)
Get.to(() => const DetailScreen());
Get.toNamed('/detail');
Get.back();
Advanced GetX Features
Workers for Side Effects & Reactions
class UserController extends GetxController {
final user = Rxn<User>();
final searchQuery = ''.obs;
@override
void onInit() {
super.onInit();
// React immediately to changes
ever(user, (callback) {
debugPrint('User changed: $callback');
});
// React only once
once(user, (callback) {
debugPrint('User set for the first time: $callback');
});
// Debounced reactions (perfect for search)
debounce(searchQuery, (callback) {
performSearch(callback);
}, time: const Duration(milliseconds: 500));
// Periodic reactions
interval(user, (callback) {
syncUserData();
}, time: const Duration(seconds: 30));
}
void performSearch(String query) {
// Debounced search implementation
}
void syncUserData() {
// Periodic sync implementation
}
}
Integrated Service Locator
// Register dependencies
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(() => HomeController());
Get.lazyPut<HomeRepository>(() => HomeRepository());
}
}
class AppBinding extends Bindings {
@override
void dependencies() {
Get.put<AuthService>(AuthService());
Get.put<UserService>(UserService());
}
}
// Use in main
void main() {
runApp(
GetMaterialApp(
initialBinding: AppBinding(),
home: const HomeScreen(),
getPages: [
GetPage(
name: '/home',
page: () => const HomeScreen(),
binding: HomeBinding(),
),
],
),
);
}
// Access from anywhere
class UserProfile extends StatelessWidget {
final authService = Get.find<AuthService>();
@override
Widget build(BuildContext context) {
return Text(authService.currentUser?.name ?? 'Guest');
}
}
Key Characteristics
- All-in-One: State management, routing, DI, utilities
- Lightweight: Minimal boilerplate for rapid development
- Reactive: Real-time UI updates with .obs
- Utilities: Built-in dialogs, snackbars, bottom sheets
- Performance: Optimized for mobile applications
Advantages
โ All-in-one solution (no need for multiple packages)
โ Minimal boilerplate code
โ Fast development iteration
โ Excellent for rapid prototyping
โ Strong community in startup/mobile circles
โ Built-in navigation and utilities
Disadvantages
โ Large package size compared to alternatives
โ Less testable without careful architecture
โ Not composable (everything in one package)
โ Can lead to tight coupling if not careful
โ Less suitable for enterprise applications
Best For
- Rapid application development (MVP, startups)
- Teams wanting unified framework
- Projects requiring routing + state management
- Mobile-first applications
- Rapid prototyping and iteration
5. BLoC - The Enterprise-Grade Enterprise Pattern
BLoC (Business Logic Component) is the most sophisticated pattern, separating business logic from UI through event-driven architecture and streams. It's the gold standard for enterprise applications.
The BLoC Architecture
// Step 1: Define Events (User Intents)
abstract class CounterEvent extends Equatable {
const CounterEvent();
}
class IncrementPressed extends CounterEvent {
const IncrementPressed();
@override
List<Object?> get props => [];
}
class DecrementPressed extends CounterEvent {
const DecrementPressed();
@override
List<Object?> get props => [];
}
class ResetPressed extends CounterEvent {
const ResetPressed();
@override
List<Object?> get props => [];
}
// Step 2: Define States (State Snapshots)
abstract class CounterState extends Equatable {
const CounterState();
}
class CounterInitial extends CounterState {
@override
List<Object?> get props => [];
}
class CounterUpdated extends CounterState {
final int count;
const CounterUpdated(this.count);
@override
List<Object?> get props => [count];
}
// Step 3: Implement BLoC (Business Logic)
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<IncrementPressed>(_onIncrementPressed);
on<DecrementPressed>(_onDecrementPressed);
on<ResetPressed>(_onResetPressed);
}
Future<void> _onIncrementPressed(
IncrementPressed event,
Emitter<CounterState> emit,
) async {
if (state is CounterUpdated) {
final current = (state as CounterUpdated).count;
emit(CounterUpdated(current + 1));
} else {
emit(const CounterUpdated(1));
}
}
Future<void> _onDecrementPressed(
DecrementPressed event,
Emitter<CounterState> emit,
) async {
if (state is CounterUpdated) {
final current = (state as CounterUpdated).count;
emit(CounterUpdated(current - 1));
} else {
emit(const CounterUpdated(-1));
}
}
Future<void> _onResetPressed(
ResetPressed event,
Emitter<CounterState> emit,
) async {
emit(const CounterUpdated(0));
}
}
// Step 4: Use in UI (Presentation Layer)
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterBloc(),
child: const CounterView(),
);
}
}
class CounterView extends StatelessWidget {
const CounterView();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLoC Counter')),
body: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
if (state is CounterUpdated) {
return Center(child: Text('Count: ${state.count}'));
}
return const Center(child: Text('Count: 0'));
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'increment',
onPressed: () {
context.read<CounterBloc>().add(const IncrementPressed());
},
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'decrement',
onPressed: () {
context.read<CounterBloc>().add(const DecrementPressed());
},
child: const Icon(Icons.remove),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'reset',
onPressed: () {
context.read<CounterBloc>().add(const ResetPressed());
},
child: const Icon(Icons.refresh),
),
],
),
);
}
}
Advanced BLoC Pattern: Repository + Usecase
// Repository Abstraction (Data Layer)
abstract class CounterRepository {
Future<int> getInitialCount();
Future<void> saveCount(int value);
}
// Usecase (Domain Layer)
class GetInitialCountUseCase {
final CounterRepository repository;
GetInitialCountUseCase({required this.repository});
Future<int> call() => repository.getInitialCount();
}
class SaveCountUseCase {
final CounterRepository repository;
SaveCountUseCase({required this.repository});
Future<void> call(int count) => repository.saveCount(count);
}
// Enhanced BLoC with Usecases
class CounterBloc extends Bloc<CounterEvent, CounterState> {
final GetInitialCountUseCase getInitialCountUseCase;
final SaveCountUseCase saveCountUseCase;
CounterBloc({
required this.getInitialCountUseCase,
required this.saveCountUseCase,
}) : super(CounterInitial()) {
on<InitializeEvent>(_onInitialize);
on<IncrementPressed>(_onIncrementPressed);
}
Future<void> _onInitialize(
InitializeEvent event,
Emitter<CounterState> emit,
) async {
try {
emit(CounterLoading());
final initialCount = await getInitialCountUseCase();
emit(CounterUpdated(initialCount));
} catch (e) {
emit(CounterError(e.toString()));
}
}
Future<void> _onIncrementPressed(
IncrementPressed event,
Emitter<CounterState> emit,
) async {
if (state is CounterUpdated) {
final currentCount = (state as CounterUpdated).count;
final newCount = currentCount + 1;
try {
await saveCountUseCase(newCount);
emit(CounterUpdated(newCount));
} catch (e) {
emit(CounterError(e.toString()));
}
}
}
}
Key Characteristics
- Testable: Pure functions, observable patterns
- Scalable: Clear separation of concerns (domain/data/presentation)
- Predictable: Event-driven, unidirectional data flow
- Enterprise: Industry standard in large teams
- Flexible: Works with any architecture style
Advantages
โ Most testable pattern (business logic isolated)
โ Excellent for large teams
โ Clear separation of concerns
โ Proven in enterprise applications
โ No BuildContext needed
โ Industry standard (companies know how to work with it)
Disadvantages
โ Steepest learning curve
โ Most boilerplate code (Events, States, Bloc)
โ Largest package footprint
โ Verbose for simple features
โ Overhead for small applications
Best For
- Large enterprise applications
- Teams with BLoC experience
- High-complexity state management
- Applications requiring extensive testing
- Multi-team projects needing clear standards
Decision Matrix: Choosing Your Pattern
Comprehensive Comparison Table
| Feature | setState | Provider | Riverpod | GetX | BLoC |
|---|---|---|---|---|---|
| Learning Curve | Trivial | Low | Medium | Low | Steep |
| Boilerplate | Minimal | Moderate | Moderate | Minimal | Extensive |
| Scalability | Poor | High | High | High | Very High |
| Type Safety | Limited | Partial | Excellent | Good | Excellent |
| Testability | Moderate | Good | Excellent | Fair | Excellent |
| Bundle Size | Negligible | ~20KB | ~30KB | ~100KB | ~50KB |
| Performance | Good | Good | Excellent | Good | Good |
| Community | Excellent | Excellent | Growing | Large | Large |
| Async Support | Limited | Good | Native | Good | Good |
| Built-in Routing | No | No | No | Yes | No |
| Time to Production | Minutes | Hours | Hours | Minutes | Days |
| Enterprise Ready | No | Yes | Yes | Maybe | Yes |
Decision Flow Chart
Start: Does your state live in one widget only?
โโ YES โ Use setState (end)
โโ NO โ Next question
Is your application simple (< 5 screens)?
โโ YES โ Use Provider or GetX
โ โโ Prefer GetX if you want routing included
โ โโ Prefer Provider for learning/flexibility
โโ NO โ Next question
Do you need type safety and compile-time checks?
โโ YES โ Use Riverpod (best for async data)
โโ NO โ Next question
Do you need all-in-one solution (state + routing + DI)?
โโ YES โ Use GetX
โโ NO โ Use Provider or BLoC
Large team + extensive testing required?
โโ YES โ Use BLoC
โโ NO โ Use Provider
Final Decision:
- Startups/MVPs: GetX
- Growing Companies: Provider โ Riverpod
- Enterprises: BLoC
- Type-Safe Focus: Riverpod
Context & Dependency Injection Deep Dive
Understanding BuildContext and InheritedWidget
Flutter's BuildContext is the glue that holds the widget tree together. Understanding how it works is critical for effective state management.
// Manual InheritedWidget (low-level, rarely used directly)
class ThemeProvider extends InheritedWidget {
final ThemeData theme;
final Function(ThemeData) onThemeChange;
const ThemeProvider({
required this.theme,
required this.onThemeChange,
required super.child,
});
// Access from descendant widgets
static ThemeProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
}
// Whether to notify listeners
@override
bool updateShouldNotify(ThemeProvider oldWidget) {
return oldWidget.theme != theme;
}
}
// Usage
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late ThemeData _theme;
void _changeTheme(ThemeData newTheme) {
setState(() {
_theme = newTheme;
});
}
@override
Widget build(BuildContext context) {
return ThemeProvider(
theme: _theme,
onThemeChange: _changeTheme,
child: const MaterialApp(home: HomeScreen()),
);
}
}
// Access in descendant
class CustomButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = ThemeProvider.of(context);
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: themeProvider?.theme.primaryColor,
),
onPressed: () {},
child: const Text('Press me'),
);
}
}
Service Locator Pattern with GetIt
Service Locator is essential for cross-cutting concerns and shared services.
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
// Setup during app initialization
void setupServiceLocator() {
// Singletons (one instance for app lifetime)
getIt.registerSingleton<AppConfig>(AppConfig());
getIt.registerSingleton<AnalyticsService>(AnalyticsService());
// Lazy Singletons (instantiated on first access)
getIt.registerLazySingleton<AuthService>(() => AuthServiceImpl());
getIt.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(apiClient: getIt()),
);
// Factories (new instance every time)
getIt.registerFactory<UserBloc>(
() => UserBloc(repository: getIt<UserRepository>()),
);
// Factories with parameters
getIt.registerFactoryParam<DetailBloc, String, void>(
(id, _) => DetailBloc(id: id, repository: getIt()),
);
}
// Usage
void main() async {
setupServiceLocator();
runApp(const MyApp());
}
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userBloc = getIt<UserBloc>();
return BlocProvider.value(
value: userBloc,
child: const UserView(),
);
}
}
Dependency Injection Best Practices
- Use Implicit Injection When Possible: Let Provider/Riverpod manage the dependency graph
- Service Locator for Cross-Cutting Concerns: Analytics, logging, shared preferences
- Avoid Circular Dependencies: Use FutureProvider or AsyncValue to break cycles
- Keep Providers Focused: Single responsibility principle
- Document Your Dependencies: Comment complex provider relationships
- Test with Dependency Overrides: Mock dependencies in tests easily
State Persistence & Data Serialization
JSON Serialization with Freezed + JsonSerializable
The most powerful approach for immutable, serializable data models.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
@Default(false) bool isActive,
@Default([]) List<String> roles,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}
// Usage
void main() {
// Create
final user = const User(
id: '123',
name: 'John Doe',
email: 'john@example.com',
);
// Serialize to JSON
final json = user.toJson();
print(json); // {id: 123, name: John Doe, ...}
// Deserialize from JSON
final userFromJson = User.fromJson(json);
print(userFromJson.name); // John Doe
// Copy with modifications
final updatedUser = user.copyWith(name: 'Jane Doe');
print(updatedUser.name); // Jane Doe
}
Local Persistence with Hive (NoSQL)
Perfect for complex object graphs and offline-first apps.
import 'package:hive/hive.dart';
// Define Hive model
@HiveType(typeId: 0)
class CachedUser extends HiveObject {
@HiveField(0)
late String id;
@HiveField(1)
late String name;
@HiveField(2)
late String email;
@HiveField(3)
late DateTime cachedAt;
}
// Initialize in main
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(CachedUserAdapter());
await Hive.openBox<CachedUser>('users');
runApp(const MyApp());
}
// Use in BLoC with caching layer
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository repository;
UserBloc({required this.repository}) : super(UserInitial()) {
on<FetchUserEvent>(_onFetchUser);
}
Future<void> _onFetchUser(
FetchUserEvent event,
Emitter<UserState> emit,
) async {
try {
emit(UserLoading());
// Try cache first
final box = Hive.box<CachedUser>('users');
final cached = box.get(event.userId);
if (cached != null && DateTime.now().difference(cached.cachedAt).inMinutes < 60) {
// Cache is fresh
emit(UserLoaded(
User(id: cached.id, name: cached.name, email: cached.email),
));
return;
}
// Fetch from remote
final user = await repository.getUser(event.userId);
// Save to cache
await box.put(event.userId, CachedUser()
..id = user.id
..name = user.name
..email = user.email
..cachedAt = DateTime.now());
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
SharedPreferences for Simple Key-Value Storage
Lightweight solution for user preferences and simple data.
class PreferencesService {
static const String _userKey = 'user_data';
static const String _themeKey = 'app_theme';
static final _prefs = SharedPreferences.getInstance();
static Future<void> saveUser(User user) async {
final prefs = await _prefs;
final json = jsonEncode(user.toJson());
await prefs.setString(_userKey, json);
}
static Future<User?> getUser() async {
final prefs = await _prefs;
final json = prefs.getString(_userKey);
if (json == null) return null;
return User.fromJson(jsonDecode(json));
}
static Future<void> saveTheme(String theme) async {
final prefs = await _prefs;
await prefs.setString(_themeKey, theme);
}
static Future<String> getTheme() async {
final prefs = await _prefs;
return prefs.getString(_themeKey) ?? 'light';
}
static Future<void> clear() async {
final prefs = await _prefs;
await prefs.clear();
}
}
// Integration with Riverpod
final userPreferencesProvider = FutureProvider<User?>((ref) async {
return PreferencesService.getUser();
});
final themeProvider = FutureProvider<String>((ref) async {
return PreferencesService.getTheme();
});
Database Integration with Drift/isar
For complex schemas and relational data.
import 'package:drift/drift.dart' as drift;
// Drift table definition
@drift.DataClassName('CachedArticle')
class Articles extends drift.Table {
drift.IntColumn get id => integer().autoIncrement()();
drift.TextColumn get title => text()();
drift.TextColumn get content => text()();
drift.DateTimeColumn get publishedAt => dateTime()();
drift.TextColumn get authorId => text()();
}
@DriftDatabase(tables: [Articles])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
Future<void> insertArticle(CachedArticle article) {
return into(articles).insert(article);
}
Future<List<CachedArticle>> getAllArticles() {
return select(articles).get();
}
Future<void> deleteArticle(int id) {
return delete(articles).delete(articles.id.equals(id) as dynamic);
}
Stream<List<CachedArticle>> watchArticles() {
return select(articles).watch();
}
}
// Using with Riverpod
final databaseProvider = Provider((ref) => AppDatabase());
final articlesProvider = StreamProvider<List<CachedArticle>>((ref) {
final db = ref.watch(databaseProvider);
return db.watchArticles();
});
final articleByIdProvider =
FutureProvider.family<CachedArticle?, int>((ref, id) async {
final db = ref.watch(databaseProvider);
final articles = await db.select(articles).get();
return articles.firstWhereOrNull((a) => a.id == id);
});
Cache Invalidation Strategies
// TTL-based cache with Riverpod
class CacheEntry<T> {
final T value;
final DateTime createdAt;
final Duration ttl;
CacheEntry({
required this.value,
required this.ttl,
}) : createdAt = DateTime.now();
bool get isExpired => DateTime.now().difference(createdAt) > ttl;
}
final cachedUserProvider = FutureProvider.autoDispose<User>((ref) async {
final cache = ref.watch(cacheManagerProvider);
final repository = ref.watch(userRepositoryProvider);
final cached = cache.get<User>('user_data');
if (cached != null && !cached.isExpired) {
return cached.value;
}
final user = await repository.getUser();
cache.set('user_data', CacheEntry(
value: user,
ttl: const Duration(hours: 1),
));
return user;
});
// Manual invalidation
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(cachedUserProvider);
return user.when(
data: (user) => Column(
children: [
Text(user.name),
ElevatedButton(
onPressed: () {
// Force refresh
ref.refresh(cachedUserProvider);
},
child: const Text('Refresh'),
),
],
),
loading: () => const CircularProgressIndicator(),
error: (error, _) => Text('Error: $error'),
);
}
}
Testing Stateful Widgets at Scale
Unit Testing Business Logic (BLoC Pattern)
import 'package:bloc_test/bloc_test.dart';
import 'package:mockito/mockito.dart';
// Mock the repository
class MockUserRepository extends Mock implements UserRepository {}
void main() {
group('UserBloc', () {
late UserBloc userBloc;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
userBloc = UserBloc(repository: mockRepository);
});
tearDown(() {
userBloc.close();
});
const testUser = User(
id: '1',
name: 'John Doe',
email: 'john@example.com',
);
// Test initial state
test('Initial state is UserInitial', () {
expect(userBloc.state, isA<UserInitial>());
});
// Test successful load
blocTest<UserBloc, UserState>(
'emits [UserLoading, UserLoaded] when FetchUserEvent is added',
setUp: () {
when(mockRepository.getUser('1'))
.thenAnswer((_) async => testUser);
},
build: () => userBloc,
act: (bloc) => bloc.add(const FetchUserEvent('1')),
expect: () => [
isA<UserLoading>(),
isA<UserLoaded>()
.having((state) => state.user, 'user', testUser),
],
);
// Test error handling
blocTest<UserBloc, UserState>(
'emits [UserLoading, UserError] when fetch fails',
setUp: () {
when(mockRepository.getUser('1'))
.thenThrow(Exception('Network error'));
},
build: () => userBloc,
act: (bloc) => bloc.add(const FetchUserEvent('1')),
expect: () => [
isA<UserLoading>(),
isA<UserError>()
.having((state) => state.message, 'message', contains('Network')),
],
);
// Test interaction
test('calls repository.getUser with correct id', () async {
when(mockRepository.getUser('1'))
.thenAnswer((_) async => testUser);
userBloc.add(const FetchUserEvent('1'));
await Future.delayed(const Duration(milliseconds: 100));
verify(mockRepository.getUser('1')).called(1);
});
});
}
Widget Testing
void main() {
group('UserScreen', () {
testWidgets('displays loading indicator while fetching',
(WidgetTester tester) async {
final mockBloc = MockUserBloc();
whenListen(
mockBloc,
Stream.fromIterable([UserLoading()]),
initialState: UserInitial(),
);
await tester.pumpWidget(
BlocProvider<UserBloc>.value(
value: mockBloc,
child: const MaterialApp(home: UserScreen()),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('displays user data when loaded',
(WidgetTester tester) async {
const testUser = User(
id: '1',
name: 'John Doe',
email: 'john@example.com',
);
final mockBloc = MockUserBloc();
whenListen(
mockBloc,
Stream.fromIterable([
UserLoading(),
UserLoaded(testUser),
]),
initialState: UserInitial(),
);
await tester.pumpWidget(
BlocProvider<UserBloc>.value(
value: mockBloc,
child: const MaterialApp(home: UserScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text('John Doe'), findsOneWidget);
expect(find.text('john@example.com'), findsOneWidget);
});
testWidgets('shows error message on failure',
(WidgetTester tester) async {
final mockBloc = MockUserBloc();
whenListen(
mockBloc,
Stream.fromIterable([
UserLoading(),
UserError('Failed to load user'),
]),
initialState: UserInitial(),
);
await tester.pumpWidget(
BlocProvider<UserBloc>.value(
value: mockBloc,
child: const MaterialApp(home: UserScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text(contains('Failed to load')), findsOneWidget);
});
});
}
Testing Riverpod Providers
void main() {
group('User Provider', () {
test('returns user from repository', () async {
final container = ProviderContainer(
overrides: [
repositoryProvider.overrideWithValue(MockUserRepository()),
],
);
when(container.read(repositoryProvider).getUser('1'))
.thenAnswer((_) async => testUser);
final user = await container.read(userProvider('1').future);
expect(user.name, 'John Doe');
});
testWidgets('displays loading then user data', (WidgetTester tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
repositoryProvider.overrideWithValue(MockUserRepository()),
],
child: const MaterialApp(home: UserScreen()),
),
);
// Initially loading
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Wait for async operation
await tester.pumpAndSettle();
// Now shows data
expect(find.text('John Doe'), findsOneWidget);
});
});
}
Best Testing Practices
- Test Business Logic Separately: Keep BLoC tests independent of UI
- Mock External Dependencies: Never make real API calls in tests
- Test State Transitions: Verify all state paths
- Use blocTest for BLoC: Simplifies BLoC testing
- Golden Tests for UI: Use golden files for UI regression detection
- Test Error Scenarios: Don't skip error handling
- Test Interactions: Verify methods called with correct arguments
Performance Optimization Strategies
Identifying Bottlenecks
// Enable Flutter DevTools performance profiling
void main() {
// Log key milestones
Timeline.instantSync('app_launch');
runApp(const MyApp());
// Track frame building time
addPostFrameCallback((_) {
Timeline.instantSync('first_frame_complete');
});
}
// Monitor rebuild counts in debug
class DebugRebuildCounter extends StatefulWidget {
final Widget child;
final String name;
const DebugRebuildCounter({
required this.child,
required this.name,
});
@override
State<DebugRebuildCounter> createState() => _DebugRebuildCounterState();
}
class _DebugRebuildCounterState extends State<DebugRebuildCounter> {
int rebuildCount = 0;
@override
Widget build(BuildContext context) {
rebuildCount++;
if (kDebugMode) {
debugPrint('${widget.name} rebuilt $rebuildCount times');
}
return widget.child;
}
}
1. Selective Rebuilds with Selector
// BEFORE: Entire widget rebuilds on any provider change
class UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
final posts = ref.watch(userPostsProvider);
final following = ref.watch(followingProvider);
return Column(
children: [
Text(user.name),
ListView(children: posts),
Text('Following: ${following.length}'),
],
);
}
}
// AFTER: Split into focused widgets
class UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
const UserNameWidget(),
const UserPostsWidget(),
const FollowingCountWidget(),
],
);
}
}
class UserNameWidget extends ConsumerWidget {
const UserNameWidget();
@override
Widget build(BuildContext context, WidgetRef ref) {
// Only rebuilds when user.name changes
final userName = ref.watch(
userProvider.select((user) => user.name),
);
return Text(userName);
}
}
class UserPostsWidget extends ConsumerWidget {
const UserPostsWidget();
@override
Widget build(BuildContext context, WidgetRef ref) {
// Only rebuilds when posts change
final posts = ref.watch(userPostsProvider);
return ListView(children: posts);
}
}
class FollowingCountWidget extends ConsumerWidget {
const FollowingCountWidget();
@override
Widget build(BuildContext context, WidgetRef ref) {
// Only rebuilds when following list changes
final followingCount = ref.watch(
followingProvider.select((list) => list.length),
);
return Text('Following: $followingCount');
}
}
2. Smart Caching with TTL
class CacheManager<T> {
final Map<String, (T, DateTime)> _cache = {};
final Duration _ttl;
CacheManager({Duration ttl = const Duration(minutes: 5)}) : _ttl = ttl;
T? get(String key) {
final entry = _cache[key];
if (entry == null) return null;
final isExpired = DateTime.now().difference(entry.$2) > _ttl;
if (isExpired) {
_cache.remove(key);
return null;
}
return entry.$1;
}
void set(String key, T value) {
_cache[key] = (value, DateTime.now());
}
void clear() => _cache.clear();
}
// Usage with Riverpod
final cacheManagerProvider = Provider((ref) => CacheManager<User>());
final cachedUserProvider = FutureProvider.family<User, String>(
(ref, userId) async {
final cache = ref.watch(cacheManagerProvider);
// Check cache
final cached = cache.get(userId);
if (cached != null) return cached;
// Fetch and cache
final repository = ref.watch(repositoryProvider);
final user = await repository.getUser(userId);
cache.set(userId, user);
return user;
},
);
3. Lazy Loading with Pagination
class PaginatedNotifier extends StateNotifier<AsyncValue<List<Item>>> {
final Repository repository;
int _currentPage = 0;
PaginatedNotifier(this.repository) : super(const AsyncValue.loading());
Future<void> loadNextPage() async {
state = await AsyncValue.guard(() async {
final currentItems = state.maybeWhen(
data: (items) => items,
orElse: () => [],
);
final newItems = await repository.getItems(page: _currentPage++);
return [...currentItems, ...newItems];
});
}
Future<void> refresh() async {
_currentPage = 0;
state = const AsyncValue.loading();
await loadNextPage();
}
}
final paginatedProvider =
StateNotifierProvider<PaginatedNotifier, AsyncValue<List<Item>>>(
(ref) => PaginatedNotifier(ref.watch(repositoryProvider)),
);
// UI with lazy loading
class ItemsList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final itemsAsync = ref.watch(paginatedProvider);
return itemsAsync.when(
data: (items) => ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
// Trigger load when reaching end
Future.microtask(
() => ref.read(paginatedProvider.notifier).loadNextPage(),
);
return const CircularProgressIndicator();
}
return ItemTile(item: items[index]);
},
),
loading: () => const CircularProgressIndicator(),
error: (error, _) => Text('Error: $error'),
);
}
}
4. Debouncing for Search
class SearchNotifier extends StateNotifier<AsyncValue<List<Result>>> {
final SearchRepository repository;
Timer? _debounceTimer;
SearchNotifier(this.repository) : super(const AsyncValue.data([]));
void search(String query) {
_debounceTimer?.cancel();
if (query.isEmpty) {
state = const AsyncValue.data([]);
return;
}
state = const AsyncValue.loading();
_debounceTimer = Timer(const Duration(milliseconds: 500), () async {
state = await AsyncValue.guard(
() => repository.search(query),
);
});
}
@override
String toString() {
_debounceTimer?.cancel();
return super.toString();
}
}
// UI
class SearchWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final results = ref.watch(searchProvider);
return Column(
children: [
TextField(
onChanged: (query) {
ref.read(searchProvider.notifier).search(query);
},
decoration: InputDecoration(
hintText: 'Search...',
prefixIcon: const Icon(Icons.search),
),
),
Expanded(
child: results.when(
data: (items) => ListView(
children: items
.map((item) => SearchResultTile(item: item))
.toList(),
),
loading: () => const CircularProgressIndicator(),
error: (error, _) => Text('Error: $error'),
),
),
],
);
}
}
5. Memory Management
// Proper cleanup in BLoC
class DataBloc extends Bloc<DataEvent, DataState> {
StreamSubscription? _streamSubscription;
DataBloc() : super(DataInitial()) {
on<FetchDataEvent>(_onFetchData);
}
Future<void> _onFetchData(
FetchDataEvent event,
Emitter<DataState> emit,
) async {
// Cancel previous subscription to prevent memory leak
_streamSubscription?.cancel();
_streamSubscription = repository.dataStream.listen((data) {
add(DataReceivedEvent(data));
});
}
@override
Future<void> close() {
_streamSubscription?.cancel();
return super.close();
}
}
// Proper cleanup in GetX
class DataController extends GetxController {
late StreamSubscription _subscription;
@override
void onInit() {
super.onInit();
// Initialize subscription
_subscription = repository.stream.listen((data) {
// Handle data
});
}
@override
void onClose() {
_subscription.cancel(); // Always clean up!
super.onClose();
}
}
Enterprise-Grade Best Practices
Clean Architecture Implementation
lib/
โโโ core/
โ โโโ constants/
โ โโโ errors/
โ โ โโโ exceptions.dart
โ โ โโโ failures.dart
โ โโโ usecases/
โ โ โโโ usecase.dart
โ โโโ utils/
โโโ features/
โ โโโ auth/
โ โ โโโ data/
โ โ โ โโโ datasources/
โ โ โ โโโ models/
โ โ โ โโโ repositories/
โ โ โโโ domain/
โ โ โ โโโ entities/
โ โ โ โโโ repositories/
โ โ โ โโโ usecases/
โ โ โโโ presentation/
โ โ โโโ bloc/
โ โ โโโ pages/
โ โ โโโ widgets/
โ โโโ posts/
โ โโโ data/
โ โโโ domain/
โ โโโ presentation/
โโโ config/
โโโ routes/
โโโ theme/
Structured Dependency Injection
class ServiceLocator {
static final _getIt = GetIt.instance;
static Future<void> init() async {
// Core
_setupCore();
// Features
_setupAuth();
_setupPosts();
}
static void _setupCore() {
_getIt.registerSingleton<AppConfig>(AppConfig());
_getIt.registerLazySingleton<AnalyticsService>(AnalyticsService());
}
static void _setupAuth() {
// Data layer
_getIt.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(httpClient: _getIt()),
);
// Repository
_getIt.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: _getIt(),
localDataSource: _getIt(),
),
);
// Usecases
_getIt.registerLazySingleton<LoginUseCase>(
() => LoginUseCase(repository: _getIt()),
);
// BLoC
_getIt.registerFactory<AuthBloc>(
() => AuthBloc(loginUseCase: _getIt()),
);
}
static void _setupPosts() {
// Similar structure
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ServiceLocator.init();
runApp(const MyApp());
}
Comprehensive Error Handling
abstract class Failure {
final String message;
final StackTrace? stackTrace;
Failure(this.message, [this.stackTrace]);
}
class NetworkFailure extends Failure {
NetworkFailure(super.message, [super.stackTrace]);
}
class ServerFailure extends Failure {
final int statusCode;
ServerFailure(super.message, this.statusCode, [super.stackTrace]);
}
class CacheFailure extends Failure {
CacheFailure(super.message, [super.stackTrace]);
}
// Use Either for functional error handling
typedef ResultFuture<T> = Future<Either<Failure, T>>;
// Repository with error handling
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
final NetworkInfo networkInfo;
UserRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
ResultFuture<User> getUser(String id) async {
try {
if (await networkInfo.isConnected) {
final user = await remoteDataSource.getUser(id);
await localDataSource.cacheUser(user);
return Right(user);
} else {
final cachedUser = await localDataSource.getUser(id);
if (cachedUser != null) {
return Right(cachedUser);
}
return Left(NetworkFailure('No internet connection'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message, e.statusCode));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
}
}
}
// BLoC handles Either
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserUseCase getUserUseCase;
UserBloc({required this.getUserUseCase}) : super(UserInitial()) {
on<FetchUserEvent>(_onFetchUser);
}
Future<void> _onFetchUser(
FetchUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
final result = await getUserUseCase(event.userId);
result.fold(
(failure) => emit(UserError(_mapFailureToMessage(failure))),
(user) => emit(UserLoaded(user)),
);
}
String _mapFailureToMessage(Failure failure) {
if (failure is NetworkFailure) {
return 'Network error. Please check your connection.';
} else if (failure is ServerFailure) {
return 'Server error (${failure.statusCode}). Please try again.';
} else if (failure is CacheFailure) {
return 'Cache error. Please refresh.';
} else {
return 'An unknown error occurred';
}
}
}
Configuration Management
enum Environment { development, staging, production }
class AppConfig {
static const Environment environment = String.fromEnvironment('ENVIRONMENT')
.contains('prod')
? Environment.production
: Environment.development;
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://api.example.com',
);
static const String apiKey = String.fromEnvironment('API_KEY');
static const Duration apiTimeout = Duration(seconds: 30);
static bool get isProduction => environment == Environment.production;
static bool get isDevelopment => environment == Environment.development;
}
// Usage
Dio _setupDio() {
final dio = Dio(BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: AppConfig.apiTimeout,
receiveTimeout: AppConfig.apiTimeout,
));
if (AppConfig.isDevelopment) {
dio.interceptors.add(LoggingInterceptor());
}
return dio;
}
Analytics & Monitoring
abstract class AnalyticsService {
Future<void> logEvent(String name, {Map<String, dynamic>? params});
Future<void> logPageView(String pageName);
Future<void> logError(String code, String message);
}
class FirebaseAnalyticsService implements AnalyticsService {
final FirebaseAnalytics _analytics;
FirebaseAnalyticsService(this._analytics);
@override
Future<void> logEvent(String name, {Map<String, dynamic>? params}) {
return _analytics.logEvent(name: name, parameters: params);
}
@override
Future<void> logPageView(String pageName) {
return _analytics.logEvent(
name: 'page_view',
parameters: {'page_name': pageName},
);
}
@override
Future<void> logError(String code, String message) {
return _analytics.logEvent(
name: 'error',
parameters: {
'error_code': code,
'error_message': message,
},
);
}
}
// Integration with state management
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserUseCase getUserUseCase;
final AnalyticsService analytics;
UserBloc({
required this.getUserUseCase,
required this.analytics,
}) : super(UserInitial()) {
on<FetchUserEvent>(_onFetchUser);
}
Future<void> _onFetchUser(
FetchUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
analytics.logEvent('user_fetch_started', params: {'user_id': event.userId});
try {
final user = await getUserUseCase(event.userId);
analytics.logEvent('user_fetch_success', params: {
'user_id': user.id,
'timestamp': DateTime.now().toIso8601String(),
});
emit(UserLoaded(user));
} catch (e) {
analytics.logError('USER_FETCH_FAILED', e.toString());
emit(UserError(e.toString()));
}
}
}
Conclusion & Recommendations
The State Management Evolution
As your application grows:
- MVP:
setStateorGetXfor speed - Growth Phase:
Providerfor maintainability - Scaling:
Riverpodfor type safety - Enterprise:
BLoCfor complexity
Key Takeaways
Master these principles for any pattern:
- Separate Concerns: Business logic โ UI
- Testability First: Design for easy testing
- Performance Matters: Minimize rebuilds
- Error Handling: Plan for failure
- Documentation: Future you will thank you
- Monitoring: Track real-world performance
- Scalability: Think ahead
Decision Flowchart Summary
Is this a throwaway prototype?
โโ Yes โ setState or GetX
โโ No โ Next question
Single widget state?
โโ Yes โ setState
โโ No โ Is app < 5 screens?
โโ Yes โ Provider or GetX
โโ No โ Type safety critical?
โโ Yes โ Riverpod
โโ No โ Multi-team project?
โโ Yes โ BLoC
โโ No โ Provider
Resources & Learning
- Flutter Official State Management Docs
- Provider Documentation
- Riverpod Official Guide
- BLoC Library
- GetX Documentation
Document Version: 2.0
Last Updated: November 2024
Target Audience: Flutter developers (Beginner to Advanced)
Difficulty: Comprehensive (All Levels)
Repository
