📑 Table of Contents
Chapter 1 – What is State Management and Why It Matters
1.1 Understanding State in Flutter
1.2 Types of State: Ephemeral vs. App State
1.3 Real-Life Importance: Case Study of a Finance Tracker App
1.4 Common Challenges and Best Practices
Chapter 2 – Basic State Management with setState()
2.1 How setState
Works: Step-by-Step
2.2 Simple Example: A Basic Counter App
2.3 Real-Life Example: Tracking Daily Expenses in a Finance App
2.4 Pros, Cons, and Alternatives
2.5 Exception Handling and Best Practices
2.6 Advanced Scenarios: Nested Widgets and Performance Optimization
Chapter 3 – Introduction to Provider
3.1 What is Provider and When to Use It
3.2 Setting Up Provider: Step-by-Step Installation and Configuration
3.3 Basic Example: Counter App with Provider
3.4 Real-Life Example: Managing User Portfolio in a Finance Tracker
3.5 Pros, Cons, and Alternatives
3.6 Exception Handling: Error Boundaries and Fallbacks
3.7 Advanced Topics: ChangeNotifier, MultiProvider, and Selectors
Chapter 4 – Introduction to Riverpod
4.1 Riverpod vs. Provider: Key Differences
4.2 Getting Started with Riverpod: Installation and Basics
4.3 Simple Example: StateNotifier for a Todo List
4.4 Real-Life Example: Dynamic Budget Alerts in a Finance App
4.5 Pros, Cons, and Alternatives
4.6 Exception Handling: Async Providers and Error States
4.7 Advanced Features: Family Providers, AutoDispose, and Scoping
Chapter 5 – Introduction to Bloc Patterns
5.1 Bloc Architecture: Events, States, and Blocs Explained
5.2 Setting Up Bloc: Dependencies and Initial Setup
5.3 Basic Example: Authentication Flow with Bloc
5.4 Real-Life Example: Transaction History in a Finance Tracker
5.5 Pros, Cons, and Alternatives
5.6 Exception Handling: Error Events and Retry Logic
5.7 Advanced Bloc: HydratedBloc for Persistence and Cubit Simplification
Chapter 6 – Managing App-Wide State vs. Local State
6.1 Distinguishing Local and Global State
6.2 Strategies for Local State: InheritedWidget and Keys
6.3 App-Wide State: Using Providers and Blocs Effectively
6.4 Real-Life Scenario: User Settings vs. Session Data in Finance App
6.5 Best Practices: Scoping State to Avoid Over-Engineering
6.6 Pros, Cons, and Hybrid Approaches
Chapter 7 – Persisting State with Shared Preferences
7.1 Introduction to Shared Preferences: When and Why
7.2 Installation and Basic Usage: Step-by-Step
7.3 Simple Example: Saving User Preferences
7.4 Real-Life Example: Storing Last Viewed Transactions in Finance App
7.5 Integrating with State Management: Provider + Shared Prefs
7.6 Pros, Cons, Alternatives (Hive, SQLite)
7.7 Exception Handling: Data Corruption and Migration
7.8 Advanced Persistence: Encryption and Cloud Sync
Chapter 8 – Learning Outcomes and Practical Exercise
8.1 Recap of Key Outcomes
8.2 Practical Exercise: Build a Counter App with Provider Across Screens
8.3 Extending the Exercise: Add Real-Life Features like Persistence
Chapter 9 – Conclusion and Next Steps
-
Summary of Covered Approaches
-
How to Choose the Right State Management Approach for Your App
-
Suggested Roadmap: From Beginner to Advanced Flutter Architectures
What is State Management and Why It Matters
Understanding State in Flutter
In Flutter, "state" refers to any data that can change over time and affects your app's UI or behavior. Unlike static widgets, stateful elements allow your app to respond to user inputs, network responses, or internal logic.
Flutter classifies widgets into two types:
- StatelessWidget: For unchanging UI (e.g., a static text label).
- StatefulWidget: For dynamic UI, where state is managed via a separate State class.
State management is the process of handling these changes efficiently. Without proper management, your app might rebuild unnecessarily, leading to performance lags or inconsistent data.
Step-by-Step Breakdown:
- User interacts (e.g., taps a button).
- State updates (e.g., increment a counter).
- UI rebuilds to reflect the new state.
In real terms, think of state as the "memory" of your app—remembering login status, shopping cart items, or form inputs.
Types of State: Ephemeral vs. App State
Flutter distinguishes between:
- Ephemeral (Local) State: Short-lived, scoped to a single widget (e.g., animation progress in a slider). Managed with setState().
- App (Shared) State: Persists across screens or widgets (e.g., user theme preference). Requires advanced tools like Provider or Bloc.
Choosing the right type prevents bloated code. For instance, local state for a form field; app state for global user data.
Table: Ephemeral vs. App State Comparison
Aspect | Ephemeral State | App State |
---|---|---|
Scope | Single widget/tree | Entire app/multiple screens |
Lifetime | Temporary (e.g., session) | Persistent (e.g., across restarts) |
Tools | setState(), ValueNotifier | Provider, Riverpod, Bloc |
Example | Toggle switch animation | User authentication token |
Performance Impact | Low (localized rebuilds) | Higher (if not optimized) |
Real-Life Importance: Case Study of a Finance Tracker App
Imagine building "FinTrack," a personal finance app where users log expenses, view budgets, and track investments. State management is critical here:
- Without Proper State: If a user adds an expense on one screen, it might not update the budget overview on another, leading to inaccurate totals and frustrated users.
- With Effective State: Changes propagate instantly—add $50 for coffee, and the monthly budget decreases in real-time across all views.
In a 2023 survey by Stack Overflow, 68% of Flutter developers cited state management as a top challenge, emphasizing its role in scalable apps. Poor state can cause issues like data loss during navigation or redundant API calls.
For FinTrack, we'll use this as a running example: managing expense lists (local state) vs. user balance (app state).
Common Challenges and Best Practices
Challenges:
- Widget Tree Rebuilds: Excessive rebuilds slow down the app.
- Data Inconsistency: State not syncing between widgets.
- Scalability: Basic methods fail in large apps.
Best Practices:
- Start simple with setState for prototypes.
- Use dependency injection for shared state.
- Profile with DevTools to monitor rebuilds.
- Handle nulls and errors proactively.
- Test state changes with unit/widget tests.
Pros of good state management: Faster apps, easier debugging. Cons: Learning curve for advanced patterns.
Alternatives: For small apps, stick to built-in; for enterprise, combine with Redux or MobX.
Basic State Management with setState()
How setState Works: Step-by-Step
setState() is Flutter's built-in method for updating ephemeral state in StatefulWidgets. It marks the widget as "dirty," triggering a rebuild.
Step-by-Step:
- Extend StatefulWidget and create a State class.
- Define variables in State (e.g., int _counter = 0;).
- Call setState(() { _counter++; }); on interaction.
- In build(), use the updated state (e.g., Text('$_counter')).
This is ideal for beginners— no external packages needed.
Simple Example: A Basic Counter App
Let's build a simple counter to demonstrate.
Code Snippet:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterScreen(),
);
}
}
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Basic Counter')),
body: Center(
child: Text('Counter: $_counter', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
Explanation: Tapping the button calls _incrementCounter(), updating _counter and rebuilding the UI. Simple and effective for local changes.
Real-Life Example: Tracking Daily Expenses in a Finance App
In FinTrack, use setState for a local expense entry form.
Scenario: User enters daily expenses on one screen. State is local—no need to share yet.
Detailed Explanation:
- The form has fields for amount, category, note.
- On submit, add to a list and update total.
- Real-life realism: Categories like "Food", "Transport"; validate inputs (e.g., amount > 0).
Code Snippet (Expanded for Realism):
import 'package:flutter/material.dart';
class ExpenseEntryScreen extends StatefulWidget {
@override
_ExpenseEntryScreenState createState() => _ExpenseEntryScreenState();
}
class _ExpenseEntryScreenState extends State<ExpenseEntryScreen> {
double _amount = 0.0;
String _category = 'Food';
String _note = '';
List<Map<String, dynamic>> _expenses = [];
double _total = 0.0;
final _formKey = GlobalKey<FormState>();
void _addExpense() {
if (_formKey.currentState!.validate()) {
setState(() {
_expenses.add({'amount': _amount, 'category': _category, 'note': _note});
_total += _amount;
_amount = 0.0; // Reset form
_note = '';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Daily Expense')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: 'Amount'),
validator: (value) => value!.isEmpty || double.parse(value) <= 0 ? 'Enter positive amount' : null,
onChanged: (value) => _amount = double.tryParse(value) ?? 0.0,
),
DropdownButtonFormField<String>(
value: _category,
items: ['Food', 'Transport', 'Entertainment'].map((cat) => DropdownMenuItem(value: cat, child: Text(cat))).toList(),
onChanged: (value) => setState(() { _category = value!; }),
),
TextFormField(
decoration: InputDecoration(labelText: 'Note'),
onChanged: (value) => _note = value,
),
ElevatedButton(onPressed: _addExpense, child: Text('Add Expense')),
SizedBox(height: 20),
Text('Total Expenses: \$$_total', style: TextStyle(fontSize: 20)),
Expanded(
child: ListView.builder(
itemCount: _expenses.length,
itemBuilder: (context, index) {
var exp = _expenses[index];
return ListTile(
title: Text('${exp['category']}: \$${exp['amount']}'),
subtitle: Text(exp['note']),
);
},
),
),
],
),
),
),
);
}
}
Step-by-Step Explanation:
- Initialize state variables for form data and list.
- Use Form for validation—realistic for user input.
- On button press, validate and update state with setState.
- Rebuild shows updated list and total.
- Real-life touch: Dropdown for categories, mimicking actual finance apps like Mint or YNAB.
This example is data-oriented: Handles lists of maps, common for transaction data.
Pros, Cons, and Alternatives for setState
Pros:
- Built-in, no dependencies.
- Simple for beginners.
- Fast for local updates.
Cons:
- Doesn't scale for app-wide state (leads to "prop drilling").
- Causes full widget rebuilds, impacting performance in large trees.
- No easy persistence.
Alternatives:
- For slightly complex local state: Use ValueNotifier with ValueListenableBuilder.
- Transition to Provider for shared state.
In advanced scenarios, combine with Keys to optimize rebuilds.
Exception Handling and Best Practices for setState
Exceptions: setState can throw if called during build or on disposed widgets.
Best Practices:
- Call setState only when necessary—use conditional checks.
- Avoid heavy computations inside setState.
- Use const constructors for unchanged widgets.
Exception Handling Code: Add try-catch in methods:
void _incrementCounter() {
try {
setState(() {
if (_counter > 100) throw Exception('Counter overflow');
_counter++;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
}
}
This handles overflow realistically, showing user feedback.
Advanced Scenarios: Nested Widgets and Performance Optimization
For nested widgets, setState in parent rebuilds children—inefficient.
Optimization:
- Use Builder widgets to localize rebuilds.
- Profile with Flutter DevTools: Inspect frame times.
Example: In FinTrack, nest expense list in a Builder to avoid rebuilding the entire form.
Code Extension: Wrap ListView in Builder:
Builder(
builder: (context) => Expanded(
child: ListView.builder(...),
),
)
This advances from basic to optimized usage.
Introduction to Provider
What is Provider and When to Use It
Provider is a popular package for dependency injection and state management. It's wrapper around InheritedWidget, making state accessible without prop drilling.
Use Provider when:
- State needs sharing across widgets/screens.
- You want reactive updates without full rebuilds.
It's beginner-friendly yet powerful for medium apps.
Setting Up Provider: Step-by-Step Installation and Configuration
- Add to pubspec.yaml: provider: ^6.1.2 (check latest version on pub.dev).
- Run flutter pub get.
- Wrap app with ChangeNotifierProvider or MultiProvider.
Basic Config Code:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterNotifier extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterNotifier(),
child: MyApp(),
),
);
}
Basic Example: Counter App with Provider
Build upon the setState counter, but share across screens.
Full Code:
// CounterNotifier as above
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Consumer<CounterNotifier>(
builder: (context, notifier, child) => Text('Counter: ${notifier.counter}'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterNotifier>().increment(),
child: Icon(Icons.add),
),
);
}
}
Explanation: Consumer listens for changes, read accesses without listening.
Real-Life Example: Managing User Portfolio in a Finance Tracker
In FinTrack, use Provider for portfolio state: stocks, values, updates.
Scenario: User adds stocks; total value updates app-wide. Real-life data: API-fetched prices (simulate for demo).
Detailed Explanation:
- Notifier holds list of stocks (maps with name, quantity, price).
- Update on add/remove, calculate total.
- Realistic: Handle market fluctuations (timer for updates).
Code Snippet (Notifier):
class PortfolioNotifier extends ChangeNotifier {
List<Map<String, dynamic>> _stocks = [];
double _totalValue = 0.0;
List<Map<String, dynamic>> get stocks => _stocks;
double get totalValue => _totalValue;
void addStock(String name, double quantity, double price) {
_stocks.add({'name': name, 'quantity': quantity, 'price': price});
_updateTotal();
notifyListeners();
}
void removeStock(int index) {
_stocks.removeAt(index);
_updateTotal();
notifyListeners();
}
void _updateTotal() {
_totalValue = _stocks.fold(0.0, (sum, stock) => sum + (stock['quantity'] * stock['price']));
}
// Simulate market update
void simulateMarketChange() {
for (var stock in _stocks) {
stock['price'] *= (1 + (DateTime.now().millisecond % 10 / 100)); // Random fluctuation
}
_updateTotal();
notifyListeners();
}
}
UI Screen:
class PortfolioScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Portfolio')),
body: Consumer<PortfolioNotifier>(
builder: (context, notifier, child) {
return Column(
children: [
Text('Total Value: \$${notifier.totalValue.toStringAsFixed(2)}'),
Expanded(
child: ListView.builder(
itemCount: notifier.stocks.length,
itemBuilder: (context, index) {
var stock = notifier.stocks[index];
return ListTile(
title: Text('${stock['name']}: ${stock['quantity']} shares @ \$${stock['price']}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => notifier.removeStock(index),
),
);
},
),
),
],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Dialog to add stock (omitted for brevity)
notifier.addStock('AAPL', 10, 150.0);
},
child: Icon(Icons.add),
),
);
}
}
Wrap in MultiProvider for multiple notifiers.
Step-by-Step: Add stock via dialog, notifier updates, UI reacts. Realistic exception: If price fetch fails, fallback to last known.
Pros, Cons, and Alternatives for Provider
Pros:
- Easy to learn, integrates with Flutter.
- Selective rebuilds via Consumer.
- Supports multiple providers.
Cons:
- Boilerplate for complex logic.
- Not great for async-heavy apps (use FutureProvider).
- Dependency on package.
Alternatives:
- Riverpod for no context dependency.
- GetX for simpler syntax.
Exception Handling: Error Boundaries and Fallbacks
Use ProxyProvider for dependencies, handle errors in notifiers.
Code: In notifier:
void addStock(...) {
try {
// Add logic
} catch (e) {
// Set error state
_error = e.toString();
notifyListeners();
}
}
UI: Check for _error and show AlertDialog.
Best practice: Use Selector for fine-grained listening.
Advanced Topics: ChangeNotifier, MultiProvider, and Selectors
- MultiProvider: Stack multiple (e.g., UserProvider + PortfolioProvider).
- Selector: Listen to specific parts:
Selector<PortfolioNotifier, double>(
selector: (_, notifier) => notifier.totalValue,
builder: (context, value, child) => Text('$value'),
)
For advanced: Combine with streams for real-time data (e.g., Firebase).
This covers from basic setup to pro-level usage.
Introduction to Riverpod
Riverpod vs. Provider: Key Differences
Riverpod is an evolution of Provider—context-independent, compile-safe, and more testable. No magic strings or context reads.
Key Diffs:
- Provider uses InheritedWidgets; Riverpod uses global providers.
- Riverpod supports auto-dispose, families.
Use Riverpod for new projects or when needing advanced scoping.
Getting Started with Riverpod: Installation and Basics
- Add flutter_riverpod: ^2.5.1 to pubspec.yaml.
- Use ProviderScope at app root.
Basic Code:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: HomeScreen());
}
}
class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$counter')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
);
}
}
Simple Example: StateNotifier for a Todo List
Use StateNotifier for mutable state.
Notifier:
class TodoNotifier extends StateNotifier<List<String>> {
TodoNotifier() : super([]);
void addTodo(String todo) {
state = [...state, todo];
}
}
final todoProvider = StateNotifierProvider<TodoNotifier, List<String>>((ref) => TodoNotifier());
UI: Watch and add.
Real-Life Example: Dynamic Budget Alerts in a Finance App
In FinTrack, manage budget categories with alerts if exceeded.
Scenario: User sets budgets (e.g., Food: $200); expenses update progress. Alert on overspend.
Detailed Notifier:
class BudgetNotifier extends StateNotifier<Map<String, Map<String, double>>> {
BudgetNotifier() : super({
'Food': {'budget': 200.0, 'spent': 0.0},
'Transport': {'budget': 100.0, 'spent': 0.0},
});
void addExpense(String category, double amount) {
if (state.containsKey(category)) {
state[category]!['spent']! += amount;
state = {...state}; // Trigger rebuild
if (state[category]!['spent']! > state[category]!['budget']!) {
// Trigger alert logic (e.g., notification)
}
}
}
void setBudget(String category, double budget) {
if (state.containsKey(category)) {
state[category]!['budget'] = budget;
state = {...state};
}
}
}
final budgetProvider = StateNotifierProvider<BudgetNotifier, Map<String, Map<String, double>>>((ref) => BudgetNotifier());
UI:
class BudgetScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final budgets = ref.watch(budgetProvider);
return ListView(
children: budgets.entries.map((entry) {
var cat = entry.key;
var data = entry.value;
var progress = data['spent']! / data['budget']!;
return ListTile(
title: Text(cat),
subtitle: LinearProgressIndicator(value: progress),
trailing: Text('\$${data['spent']}/\$${data['budget']}'),
);
}).toList(),
);
}
}
Explanation: Add expense updates spent, progress bar shows real-time. Realistic: ProgressIndicator for visual feedback, like in budgeting apps.
Step-by-Step: Set budget, add expenses via another screen, watch updates.
Pros, Cons, and Alternatives for Riverpod
Pros:
- Testable, no context leaks.
- Auto-dispose reduces memory usage.
- Family providers for dynamic instances.
Cons:
- Steeper learning for beginners.
- More verbose than Provider.
Alternatives:
- Bloc for event-driven.
- MobX for reactive programming.
Exception Handling: Async Providers and Error States
Use FutureProvider for async.
Code:
final asyncDataProvider = FutureProvider<String>((ref) async {
try {
await Future.delayed(Duration(seconds: 1));
return 'Data Loaded';
} catch (e) {
throw Exception('Load Failed');
}
});
UI: Use .when(data: ..., loading: ..., error: ...).
Advanced Features: Family Providers, AutoDispose, and Scoping
- Family: final userProvider = Provider.family<User, int>((ref, id) => User(id));
- AutoDispose: StateProvider.autoDispose for cleanup.
- Scoping: Override providers in subtrees.
For FinTrack, family for per-category budgets.
Introduction to Bloc Patterns
Bloc Architecture: Events, States, and Blocs Explained
Bloc (Business Logic Component) separates UI from logic using events and states.
Components:
- Event: User action (e.g., AddExpense).
- Bloc: Processes events, emits states.
- State: UI data (e.g., ExpenseLoaded).
Ideal for complex, event-driven apps.
Setting Up Bloc: Dependencies and Initial Setup
- Add flutter_bloc: ^8.1.5, bloc: ^8.1.4.
- Use BlocProvider.
Basic Example: Authentication Flow with Bloc
Event:
abstract class AuthEvent {}
class LoginEvent extends AuthEvent {
final String username, password;
LoginEvent(this.username, this.password);
}
class LogoutEvent extends AuthEvent {}
State:
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState { final String token; AuthAuthenticated(this.token); }
class AuthError extends AuthState { final String message; AuthError(this.message); }
Bloc:
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(AuthInitial());
@override
Stream<AuthState> mapEventToState(AuthEvent event) async* {
if (event is LoginEvent) {
yield AuthLoading();
try {
// Simulate API
await Future.delayed(Duration(seconds: 1));
if (event.username == 'user' && event.password == 'pass') {
yield AuthAuthenticated('token');
} else {
yield AuthError('Invalid credentials');
}
} catch (e) {
yield AuthError(e.toString());
}
} else if (event is LogoutEvent) {
yield AuthInitial();
}
}
}
UI: BlocBuilder to react.
Real-Life Example: Transaction History in a Finance Tracker
For FinTrack, manage transaction list with Bloc.
Scenario: Fetch, add, delete transactions; handle offline.
Events:
abstract class TransactionEvent {}
class FetchTransactions extends TransactionEvent {}
class AddTransaction extends TransactionEvent { final Map<String, dynamic> tx; AddTransaction(this.tx); }
class DeleteTransaction extends TransactionEvent { final int id; DeleteTransaction(this.id); }
States:
abstract class TransactionState {}
class TransactionInitial extends TransactionState {}
class TransactionLoading extends TransactionState {}
class TransactionLoaded extends TransactionState { final List<Map<String, dynamic>> transactions; TransactionLoaded(this.transactions); }
class TransactionError extends TransactionState { final String message; TransactionError(this.message); }
Bloc:
class TransactionBloc extends Bloc<TransactionEvent, TransactionState> {
List<Map<String, dynamic>> _transactions = [];
TransactionBloc() : super(TransactionInitial());
@override
Stream<TransactionState> mapEventToState(TransactionEvent event) async* {
if (event is FetchTransactions) {
yield TransactionLoading();
try {
// Simulate fetch
await Future.delayed(Duration(seconds: 1));
yield TransactionLoaded(_transactions);
} catch (e) {
yield TransactionError(e.toString());
}
} else if (event is AddTransaction) {
_transactions.add(event.tx);
yield TransactionLoaded(_transactions);
} else if (event is DeleteTransaction) {
_transactions.removeAt(event.id);
yield TransactionLoaded(_transactions);
}
}
}
UI:
class TransactionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TransactionBloc()..add(FetchTransactions()),
child: BlocBuilder<TransactionBloc, TransactionState>(
builder: (context, state) {
if (state is TransactionLoading) return Center(child: CircularProgressIndicator());
if (state is TransactionError) return Text(state.message);
if (state is TransactionLoaded) {
return ListView.builder(
itemCount: state.transactions.length,
itemBuilder: (context, index) {
var tx = state.transactions[index];
return ListTile(title: Text('Amount: \$${tx['amount']}'));
},
);
}
return Container();
},
),
);
}
}
Explanation: Add event on button, Bloc processes, emits loaded state. Realistic: ID for deletes, error for failed fetches.
Pros, Cons, and Alternatives for Bloc
Pros:
- Clear separation of concerns.
- Great for testable code.
- Handles async naturally.
Cons:
- Boilerplate heavy.
- Overkill for simple apps.
Alternatives:
- Cubit (simplified Bloc without events).
- Redux for global store.
Exception Handling: Error Events and Retry Logic
Add RetryEvent, yield error state with retry button.
Code: In mapEventToState, catch and yield TransactionError.
UI: On error, show button to add RetryEvent.
Advanced Bloc: HydratedBloc for Persistence and Cubit Simplification
Use hydrated_bloc for storage.
Add package, extend HydratedBloc.
For Cubit: Simpler, emit states directly without events.
Example: Cubit for quick counters.
Managing App-Wide State vs. Local State
Distinguishing Local and Global State
Local: Widget-specific, like form validation. Global: Shared, like user profile.
Rule: If state is used in >1 screen, make it global.
Strategies for Local State: InheritedWidget and Keys
Use InheritedWidget for subtree sharing without packages.
Example: Custom Inherited for theme in subtree.
Keys: ValueKey to preserve state on reorder.
App-Wide State: Using Providers and Blocs Effectively
Use MultiProvider/BlocProvider at root.
For FinTrack, global for user balance, local for screen forms.
Real-Life Scenario: User Settings vs. Session Data in Finance App
Settings (theme): Global Provider. Session (current expense entry): Local setState.
Hybrid: Provider for settings, Bloc for sessions.
Best Practices: Scoping State to Avoid Over-Engineering
- Use const where possible.
- Avoid global for everything—scope with ProviderScope.
- Monitor with DevTools.
Pros, Cons, and Hybrid Approaches
Pros of global: Easy sharing. Cons: Potential leaks.
Hybrid: setState local + Provider global.
Persisting State with Shared Preferences
Introduction to Shared Preferences: When and Why
Shared_preferences stores key-value pairs persistently (e.g., user prefs).
Use for small data: booleans, strings, not large lists (use JSON).
Installation and Basic Usage: Step-by-Step
- Add shared_preferences: ^2.3.2.
- Get instance: SharedPreferences prefs = await SharedPreferences.getInstance();
- Set: prefs.setInt('counter', 5);
- Get: int counter = prefs.getInt('counter') ?? 0;
Simple Example: Saving User Preferences
Save theme mode.
Code:
Future<void> saveTheme(bool isDark) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool('isDark', isDark);
}
Future<bool> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isDark') ?? false;
}
Real-Life Example: Storing Last Viewed Transactions in Finance App
In FinTrack, persist last 5 transactions.
Code:
import 'dart:convert';
// ...
class TransactionPersistence {
static const String key = 'last_transactions';
Future<void> save(List<Map<String, dynamic>> txs) async {
final prefs = await SharedPreferences.getInstance();
prefs.setString(key, jsonEncode(txs.take(5).toList())); // Limit to 5
}
Future<List<Map<String, dynamic>>> load() async {
final prefs = await SharedPreferences.getInstance();
final String? data = prefs.getString(key);
if (data != null) {
return (jsonDecode(data) as List).cast<Map<String, dynamic>>();
}
return [];
}
}
Integrate with Bloc: On load, add to state.
Realistic: JSON for complex data, limit size.
Integrating with State Management: Provider + Shared Prefs
In Provider, load on init:
class CounterNotifier extends ChangeNotifier {
CounterNotifier() {
_load();
}
void _load() async {
// Load from prefs, notify
}
}
Pros, Cons, Alternatives (Hive, SQLite)
Pros:
- Simple, platform-native.
- Async, non-blocking.
Cons:
- Not secure (plain text).
- Limited to primitives.
Alternatives:
- Hive: Fast, NoSQL for larger data.
- SQLite: For queries on big datasets.
Use Hive for offline-first apps.
Exception Handling: Data Corruption and Migration
Handle JSON decode errors:
try {
jsonDecode(data);
} catch (e) {
// Clear corrupt data
prefs.remove(key);
}
Migration: Check version key, update format.
Advanced Persistence: Encryption and Cloud Sync
Use encrypted_shared_preferences for security.
For cloud: Sync with Firebase on change.
Example: Listener in Provider to upload.
Learning Outcomes and Practical Exercise
Recap of Key Outcomes
By now, you can:
- Implement setState and Provider.
- Choose approaches based on app size.
- Persist state and handle exceptions.
Practical Exercise: Build a Counter App with Provider Across Screens
Objective: Counter shared between Home and Settings screens.
Step-by-Step:
- Set up Provider with CounterNotifier.
- Home: Display and increment.
- Navigate to Settings: Display and reset.
- Add persistence with shared prefs.
Full Code Outline (Expand as above):
- Notifier with increment/reset.
- Multi-screen navigation.
- Load/save on init/dispose.
Test: Increment on Home, see on Settings.
Extending the Exercise: Add Real-Life Features like Persistence
Add: Category dropdown, list view, budget alert using Riverpod or Bloc.
Make it advanced: Integrate API simulation for "fetch balance".
Conclusion and Next Steps
You've mastered Flutter state management—from basic setState to advanced Bloc, with real-life FinTrack examples. Apply these in your projects for robust apps.
Next: Chapter 5: Networking and API Integration.
Share your FinTrack variations on socials! For questions, comment below.
Happy coding! 🚀
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam