🚀 Flutter State Management
From Local Props to Riverpod: A Complete Guide
Part 1: Local State Management & Prop Passing
When you're just starting with Flutter, local state management is your best friend. You pass data down from parent to child using constructors, and child widgets call functions to notify parents of changes.
📌 Step 1: The Child Component (The Form)
In Dart, we use the ? symbol for "maybe" (nullable) values and the required keyword for required parameters.
class UserForm extends StatelessWidget {
final String name; // Required
final String? email; // Optional (Maybe)
final Function(String) onNameChanged;
final Function(String)? onEmailChanged; // Optional
const UserForm({
super.key,
required this.name,
this.email,
required this.onNameChanged,
this.onEmailChanged,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: onNameChanged,
decoration: InputDecoration(hintText: "Name: $name"),
),
if (onEmailChanged != null)
TextField(
onChanged: onEmailChanged,
decoration: InputDecoration(
hintText: "Email: ${email ?? 'Not set'}"
),
),
],
);
}
}
📌 Step 2: The Parent (State Management)
Here's how you handle local state using StatefulWidget and setState:
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
// 1. Define the local state
String _userName = "John Doe";
String? _userEmail; // Starts as null
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Edit Profile")),
body: UserForm(
name: _userName,
email: _userEmail,
onNameChanged: (newName) {
setState(() {
_userName = newName;
});
},
onEmailChanged: (newEmail) {
setState(() {
_userEmail = newEmail;
});
},
),
);
}
}
Part 2: Riverpod State Management
As your app grows, you need state that's shared across multiple screens. That's where Riverpod comes in. Riverpod gives your app a global state management layer with three simple building blocks.
🧩 The Three Building Blocks
1. Provider (The Data)
Holds your app's state globally. Different types for different needs: StateProvider, NotifierProvider, FutureProvider, etc.
2. Ref (The Remote Control)
Use ref.watch() to listen to changes, or ref.read() to get data once without listening.
3. Consumer (The UI)
Use ConsumerWidget or ConsumerStatefulWidget to access the ref object.
Why Riverpod?
Share state across any widget, automatic rebuilding, dependency injection, and complex logic management.
🎯 Simple Example: Dark Mode Toggle
Step 1: Create the Provider (The Data)
final darkModeProvider = StateProvider((ref) => false);
Step 2: Create the Consumer UI
class SettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch the provider - rebuilds when it changes
final isDarkMode = ref.watch(darkModeProvider);
return Scaffold(
appBar: AppBar(title: Text("Settings")),
body: SwitchListTile(
title: Text("Dark Mode"),
value: isDarkMode,
onChanged: (val) {
// Use read to change the value
ref.read(darkModeProvider.notifier).state = val;
},
),
);
}
}
Part 3: NotifierProvider - Complex State with Logic
When you have complex objects and custom logic (like a todo list with add/remove methods), use NotifierProvider. It's like a class that manages its own state.
🎯 Real-World Example: Todo List
Step 1: Create the Notifier Class
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TodoListNotifier extends Notifier<List<String>> {
@override
List<String> build() {
return ["Buy Milk", "Clean Room"];
}
void addTodo(String todo) {
// In Riverpod, state is immutable
// Replace the old list with a new one
state = [...state, todo];
}
void removeTodo(String todo) {
state = state.where((item) => item != todo).toList();
}
}
Step 2: Create the Provider
final todoListProvider = NotifierProvider<TodoListNotifier, List<String>>(
TodoListNotifier.new,
);
Step 3: Use it in Your Widget
class TodoScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return Scaffold(
appBar: AppBar(title: Text("My Todos")),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index]),
onLongPress: () {
ref.read(todoListProvider.notifier)
.removeTodo(todos[index]);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(todoListProvider.notifier)
.addTodo("New Task");
},
child: Icon(Icons.add),
),
);
}
}
[...state, todo].
When to Use What?
Local State (setState)
Use when: State is only needed on one screen, simple toggles, form inputs, or temporary data.
Pros: Simple, no external packages, fast to implement.
Cons: Can't share state across screens, prone to prop drilling.
Riverpod
Use when: State is shared across multiple screens, complex logic, API calls, or global settings.
Pros: Global access, automatic rebuilding, testable, scales well.
Cons: Slight learning curve, requires package dependency.
setState, while complex features use Riverpod. There's no "one right way"—it depends on your app's needs.

Comments
Post a Comment