Flutter State Management: Local & Riverpod Guide

🚀 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.

dart
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:

dart
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;
          });
        },
      ),
    );
  }
}
When to use: Simple pages with minimal state. Perfect for forms, toggles, and small features that don't need to share state across the app.

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)

dart
final darkModeProvider = StateProvider((ref) => false);

Step 2: Create the Consumer UI

dart
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;
        },
      ),
    );
  }
}
Key Difference: With Riverpod, the state lives outside the widget. Any widget in your app can access it without passing props down the widget tree!

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

dart
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

dart
final todoListProvider = NotifierProvider<TodoListNotifier, List<String>>(
  TodoListNotifier.new,
);

Step 3: Use it in Your Widget

dart
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),
      ),
    );
  }
}
Important: In Riverpod, state is immutable. You don't modify existing lists; you create new ones. This is why we use the spread operator [...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.

Pro Tip: Many professional Flutter apps use both! Simple widgets use setState, while complex features use Riverpod. There's no "one right way"—it depends on your app's needs.

📚 Flutter State Management Guide | Built with ❤️ for Flutter Developers

Comments