DEV Community

Rost
Rost

Posted on

State Management in Flutter: A Comprehensive Guide

State management is one of the most critical aspects of building Flutter applications. As your app grows in complexity, managing state effectively becomes crucial for maintaining clean, scalable, and testable code. In this article, we'll explore various state management approaches in Flutter, from simple to sophisticated, with practical examples.

What is State in Flutter?

State refers to any data that can change during the lifetime of your app. This includes:

  • User input (form fields, search queries)
  • API responses (user data, products list)
  • UI state (loading indicators, selected items)
  • Application settings (theme, language preferences)

Flutter categorizes state into two types:

  • Ephemeral State (Local State): State that only affects a single widget (e.g., current page in a PageView)
  • App State (Shared State): State that needs to be shared across multiple widgets (e.g., user authentication status)

1. setState() - The Simplest Approach

setState() is Flutter's built-in method for managing local state within a StatefulWidget.

When to Use

  • Simple, widget-specific state
  • Quick prototypes
  • State that doesn't need to be shared across widgets

Example: Counter App

import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({Key? key}) : super(key: key);

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  bool _isLoading = false;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  Future<void> _asyncIncrement() async {
    setState(() => _isLoading = true);

    // Simulate async operation
    await Future.delayed(const Duration(seconds: 1));

    setState(() {
      _counter++;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('setState Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter Value:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            if (_isLoading)
              const CircularProgressIndicator()
            else
              ElevatedButton(
                onPressed: _asyncIncrement,
                child: const Text('Async Increment'),
              ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: const Icon(Icons.add),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

✅ Simple and straightforward

✅ No external dependencies

✅ Good for small, isolated components

❌ Difficult to share state

❌ Can lead to prop drilling

❌ Hard to test business logic


2. InheritedWidget - Flutter's Foundation

InheritedWidget is the low-level mechanism that Flutter uses to propagate information down the widget tree. Most state management solutions are built on top of it.

When to Use

  • When you need to pass data down the widget tree
  • Building your own state management solution
  • Understanding how other solutions work under the hood

Example: Theme Manager

import 'package:flutter/material.dart';

// The InheritedWidget
class ThemeProvider extends InheritedWidget {
  final ThemeMode themeMode;
  final VoidCallback toggleTheme;

  const ThemeProvider({
    Key? key,
    required this.themeMode,
    required this.toggleTheme,
    required Widget child,
  }) : super(key: key, child: child);

  static ThemeProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
  }

  @override
  bool updateShouldNotify(ThemeProvider oldWidget) {
    return themeMode != oldWidget.themeMode;
  }
}

// Stateful wrapper
class ThemeManager extends StatefulWidget {
  final Widget child;

  const ThemeManager({Key? key, required this.child}) : super(key: key);

  @override
  State<ThemeManager> createState() => _ThemeManagerState();
}

class _ThemeManagerState extends State<ThemeManager> {
  ThemeMode _themeMode = ThemeMode.light;

  void _toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.light 
          ? ThemeMode.dark 
          : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ThemeProvider(
      themeMode: _themeMode,
      toggleTheme: _toggleTheme,
      child: widget.child,
    );
  }
}

// Usage in a widget
class ThemedButton extends StatelessWidget {
  const ThemedButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final themeProvider = ThemeProvider.of(context);

    return ElevatedButton(
      onPressed: themeProvider?.toggleTheme,
      child: Text(
        'Current: ${themeProvider?.themeMode == ThemeMode.light ? "Light" : "Dark"}',
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

✅ Built into Flutter

✅ Efficient rebuilds

✅ Foundation for understanding other patterns

❌ Verbose boilerplate

❌ Requires wrapper StatefulWidget

❌ Easy to make mistakes


3. Provider - The Recommended Solution

Provider is the officially recommended state management solution by the Flutter team. It's a wrapper around InheritedWidget that's easier to use and more flexible.

When to Use

  • Most Flutter applications
  • When you need dependency injection
  • When you want a balance between simplicity and power

Example: Shopping Cart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Model
class Product {
  final String id;
  final String name;
  final double price;

  Product({required this.id, required this.name, required this.price});
}

// State Management Class
class CartModel extends ChangeNotifier {
  final List<Product> _items = [];

  List<Product> get items => List.unmodifiable(_items);

  int get itemCount => _items.length;

  double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);

  void addItem(Product product) {
    _items.add(product);
    notifyListeners();
  }

  void removeItem(String productId) {
    _items.removeWhere((item) => item.id == productId);
    notifyListeners();
  }

  void clear() {
    _items.clear();
    notifyListeners();
  }
}

// Main App Setup
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const ProductListPage(),
    );
  }
}

// Product List Page
class ProductListPage extends StatelessWidget {
  const ProductListPage({Key? key}) : super(key: key);

  final List<Product> products = const [
    Product(id: '1', name: 'Laptop', price: 999.99),
    Product(id: '2', name: 'Mouse', price: 29.99),
    Product(id: '3', name: 'Keyboard', price: 79.99),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        actions: [
          // Using Consumer for specific rebuilds
          Consumer<CartModel>(
            builder: (context, cart, child) {
              return Padding(
                padding: const EdgeInsets.all(8.0),
                child: Center(
                  child: Text(
                    'Cart: ${cart.itemCount}',
                    style: const TextStyle(fontSize: 16),
                  ),
                ),
              );
            },
          ),
          IconButton(
            icon: const Icon(Icons.shopping_cart),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => const CartPage()),
              );
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ListTile(
            title: Text(product.name),
            subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
            trailing: IconButton(
              icon: const Icon(Icons.add_shopping_cart),
              onPressed: () {
                // Using Provider.of with listen: false for actions
                Provider.of<CartModel>(context, listen: false)
                    .addItem(product);

                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('${product.name} added to cart'),
                    duration: const Duration(seconds: 1),
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

// Cart Page
class CartPage extends StatelessWidget {
  const CartPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shopping Cart')),
      body: Consumer<CartModel>(
        builder: (context, cart, child) {
          if (cart.itemCount == 0) {
            return const Center(
              child: Text('Your cart is empty'),
            );
          }

          return Column(
            children: [
              Expanded(
                child: ListView.builder(
                  itemCount: cart.items.length,
                  itemBuilder: (context, index) {
                    final item = cart.items[index];
                    return ListTile(
                      title: Text(item.name),
                      subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
                      trailing: IconButton(
                        icon: const Icon(Icons.remove_circle),
                        onPressed: () => cart.removeItem(item.id),
                      ),
                    );
                  },
                ),
              ),
              Container(
                padding: const EdgeInsets.all(16),
                child: Column(
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Total:',
                          style: TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Text(
                          '\$${cart.totalPrice.toStringAsFixed(2)}',
                          style: const TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: () {
                        cart.clear();
                        Navigator.pop(context);
                      },
                      child: const Text('Checkout'),
                    ),
                  ],
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Provider Patterns

  1. ChangeNotifierProvider: For classes that extend ChangeNotifier
  2. Consumer: For widgets that need to rebuild when state changes
  3. Provider.of(): For accessing state (use listen: false for actions)
  4. MultiProvider: For providing multiple state objects
  5. ProxyProvider: For providers that depend on other providers

Pros & Cons

✅ Simple and intuitive API

✅ Good performance with granular rebuilds

✅ Officially recommended

✅ Great for dependency injection

❌ Requires understanding of context

❌ Can be verbose for complex state


4. Riverpod - Provider's Evolution

Riverpod is a complete rewrite of Provider that removes its dependency on BuildContext and adds compile-time safety.

When to Use

  • New projects
  • When you want compile-time safety
  • When you need advanced features like state combinations

Example: User Authentication

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Models
class User {
  final String id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});
}

enum AuthState { initial, loading, authenticated, unauthenticated }

class AuthData {
  final AuthState state;
  final User? user;
  final String? error;

  AuthData({required this.state, this.user, this.error});

  AuthData copyWith({AuthState? state, User? user, String? error}) {
    return AuthData(
      state: state ?? this.state,
      user: user ?? this.user,
      error: error ?? this.error,
    );
  }
}

// Providers
class AuthNotifier extends StateNotifier<AuthData> {
  AuthNotifier() : super(AuthData(state: AuthState.initial));

  Future<void> login(String email, String password) async {
    state = state.copyWith(state: AuthState.loading);

    try {
      // Simulate API call
      await Future.delayed(const Duration(seconds: 2));

      if (email == 'test@example.com' && password == 'password') {
        final user = User(
          id: '1',
          name: 'John Doe',
          email: email,
        );
        state = AuthData(state: AuthState.authenticated, user: user);
      } else {
        state = AuthData(
          state: AuthState.unauthenticated,
          error: 'Invalid credentials',
        );
      }
    } catch (e) {
      state = AuthData(
        state: AuthState.unauthenticated,
        error: e.toString(),
      );
    }
  }

  void logout() {
    state = AuthData(state: AuthState.unauthenticated);
  }
}

final authProvider = StateNotifierProvider<AuthNotifier, AuthData>((ref) {
  return AuthNotifier();
});

// Computed/Derived state
final isAuthenticatedProvider = Provider<bool>((ref) {
  final authData = ref.watch(authProvider);
  return authData.state == AuthState.authenticated;
});

// Main App
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Auth Example',
      home: const AuthWrapper(),
    );
  }
}

// Auth Wrapper
class AuthWrapper extends ConsumerWidget {
  const AuthWrapper({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isAuthenticated = ref.watch(isAuthenticatedProvider);

    return isAuthenticated ? const HomePage() : const LoginPage();
  }
}

// Login Page
class LoginPage extends ConsumerStatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

  @override
  ConsumerState<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends ConsumerState<LoginPage> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final authData = ref.watch(authProvider);
    final isLoading = authData.state == AuthState.loading;

    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'Email',
                hintText: 'test@example.com',
              ),
              enabled: !isLoading,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: 'Password',
                hintText: 'password',
              ),
              obscureText: true,
              enabled: !isLoading,
            ),
            const SizedBox(height: 24),
            if (authData.error != null)
              Padding(
                padding: const EdgeInsets.only(bottom: 16),
                child: Text(
                  authData.error!,
                  style: const TextStyle(color: Colors.red),
                ),
              ),
            ElevatedButton(
              onPressed: isLoading
                  ? null
                  : () {
                      ref.read(authProvider.notifier).login(
                            _emailController.text,
                            _passwordController.text,
                          );
                    },
              child: isLoading
                  ? const CircularProgressIndicator()
                  : const Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

// Home Page
class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(authProvider).user;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () {
              ref.read(authProvider.notifier).logout();
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Welcome, ${user?.name ?? "User"}!',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 8),
            Text(
              user?.email ?? '',
              style: const TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Riverpod Provider Types

  1. Provider: For immutable values
  2. StateProvider: For simple state that can change
  3. StateNotifierProvider: For complex state logic
  4. FutureProvider: For async operations
  5. StreamProvider: For streams

Pros & Cons

✅ Compile-time safety

✅ No BuildContext dependency

✅ Better testability

✅ Powerful state composition

❌ Steeper learning curve

❌ Different from standard Flutter patterns


5. BLoC (Business Logic Component)

BLoC pattern uses streams to manage state and is particularly popular in enterprise applications.

When to Use

  • Large, complex applications
  • When you need strict separation of business logic
  • Team with reactive programming experience

Example: Todo List with BLoC

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// Models
class Todo {
  final String id;
  final String title;
  final bool isCompleted;

  Todo({
    required this.id,
    required this.title,
    this.isCompleted = false,
  });

  Todo copyWith({String? id, String? title, bool? isCompleted}) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}

// Events
abstract class TodoEvent {}

class AddTodo extends TodoEvent {
  final String title;
  AddTodo(this.title);
}

class ToggleTodo extends TodoEvent {
  final String id;
  ToggleTodo(this.id);
}

class DeleteTodo extends TodoEvent {
  final String id;
  DeleteTodo(this.id);
}

class LoadTodos extends TodoEvent {}

// States
abstract class TodoState {}

class TodoInitial extends TodoState {}

class TodoLoading extends TodoState {}

class TodoLoaded extends TodoState {
  final List<Todo> todos;
  TodoLoaded(this.todos);
}

class TodoError extends TodoState {
  final String message;
  TodoError(this.message);
}

// BLoC
class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc() : super(TodoInitial()) {
    on<LoadTodos>(_onLoadTodos);
    on<AddTodo>(_onAddTodo);
    on<ToggleTodo>(_onToggleTodo);
    on<DeleteTodo>(_onDeleteTodo);
  }

  Future<void> _onLoadTodos(LoadTodos event, Emitter<TodoState> emit) async {
    emit(TodoLoading());
    try {
      // Simulate API call
      await Future.delayed(const Duration(seconds: 1));
      emit(TodoLoaded([]));
    } catch (e) {
      emit(TodoError(e.toString()));
    }
  }

  Future<void> _onAddTodo(AddTodo event, Emitter<TodoState> emit) async {
    if (state is TodoLoaded) {
      final currentState = state as TodoLoaded;
      final newTodo = Todo(
        id: DateTime.now().toString(),
        title: event.title,
      );
      emit(TodoLoaded([...currentState.todos, newTodo]));
    }
  }

  Future<void> _onToggleTodo(ToggleTodo event, Emitter<TodoState> emit) async {
    if (state is TodoLoaded) {
      final currentState = state as TodoLoaded;
      final updatedTodos = currentState.todos.map((todo) {
        return todo.id == event.id
            ? todo.copyWith(isCompleted: !todo.isCompleted)
            : todo;
      }).toList();
      emit(TodoLoaded(updatedTodos));
    }
  }

  Future<void> _onDeleteTodo(DeleteTodo event, Emitter<TodoState> emit) async {
    if (state is TodoLoaded) {
      final currentState = state as TodoLoaded;
      final updatedTodos = currentState.todos
          .where((todo) => todo.id != event.id)
          .toList();
      emit(TodoLoaded(updatedTodos));
    }
  }
}

// Main App
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BLoC Todo Example',
      home: BlocProvider(
        create: (context) => TodoBloc()..add(LoadTodos()),
        child: const TodoPage(),
      ),
    );
  }
}

// Todo Page
class TodoPage extends StatelessWidget {
  const TodoPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('BLoC Todo List')),
      body: BlocBuilder<TodoBloc, TodoState>(
        builder: (context, state) {
          if (state is TodoLoading) {
            return const Center(child: CircularProgressIndicator());
          }

          if (state is TodoError) {
            return Center(child: Text('Error: ${state.message}'));
          }

          if (state is TodoLoaded) {
            if (state.todos.isEmpty) {
              return const Center(child: Text('No todos yet!'));
            }

            return ListView.builder(
              itemCount: state.todos.length,
              itemBuilder: (context, index) {
                final todo = state.todos[index];
                return ListTile(
                  leading: Checkbox(
                    value: todo.isCompleted,
                    onChanged: (_) {
                      context.read<TodoBloc>().add(ToggleTodo(todo.id));
                    },
                  ),
                  title: Text(
                    todo.title,
                    style: TextStyle(
                      decoration: todo.isCompleted
                          ? TextDecoration.lineThrough
                          : null,
                    ),
                  ),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () {
                      context.read<TodoBloc>().add(DeleteTodo(todo.id));
                    },
                  ),
                );
              },
            );
          }

          return const SizedBox.shrink();
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddTodoDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddTodoDialog(BuildContext context) {
    final controller = TextEditingController();

    showDialog(
      context: context,
      builder: (dialogContext) {
        return AlertDialog(
          title: const Text('Add Todo'),
          content: TextField(
            controller: controller,
            decoration: const InputDecoration(hintText: 'Enter todo title'),
            autofocus: true,
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(dialogContext),
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                if (controller.text.isNotEmpty) {
                  context.read<TodoBloc>().add(AddTodo(controller.text));
                  Navigator.pop(dialogContext);
                }
              },
              child: const Text('Add'),
            ),
          ],
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

✅ Clear separation of concerns

✅ Highly testable

✅ Great for large teams

✅ Powerful stream-based architecture

❌ Verbose boilerplate

❌ Steep learning curve

❌ Can be overkill for simple apps


6. GetX - The All-in-One Solution

GetX is a lightweight yet powerful state management solution that also includes routing and dependency injection.

When to Use

  • Rapid development
  • When you want minimal boilerplate
  • Simple to medium complexity apps

Example: Theme Switcher with GetX

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// Controller
class ThemeController extends GetxController {
  var isDarkMode = false.obs;
  var counter = 0.obs;

  void toggleTheme() {
    isDarkMode.value = !isDarkMode.value;
    Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
  }

  void increment() {
    counter.value++;
  }

  void reset() {
    counter.value = 0;
  }
}

// Main App
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Example',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      themeMode: ThemeMode.light,
      home: const HomePage(),
    );
  }
}

// Home Page
class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Initialize controller
    final ThemeController controller = Get.put(ThemeController());

    return Scaffold(
      appBar: AppBar(
        title: const Text('GetX Example'),
        actions: [
          Obx(() => IconButton(
                icon: Icon(
                  controller.isDarkMode.value
                      ? Icons.light_mode
                      : Icons.dark_mode,
                ),
                onPressed: controller.toggleTheme,
              )),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Counter Value:',
              style: TextStyle(fontSize: 20),
            ),
            Obx(() => Text(
                  '${controller.counter.value}',
                  style: const TextStyle(
                    fontSize: 48,
                    fontWeight: FontWeight.bold,
                  ),
                )),
            const SizedBox(height: 32),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: controller.increment,
                  child: const Text('Increment'),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: controller.reset,
                  child: const Text('Reset'),
                ),
              ],
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: () {
                Get.to(() => const SecondPage());
              },
              child: const Text('Go to Second Page'),
            ),
          ],
        ),
      ),
    );
  }
}

// Second Page (showing persistent state)
class SecondPage extends StatelessWidget {
  const SecondPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final ThemeController controller = Get.find();

    return Scaffold(
      appBar: AppBar(title: const Text('Second Page')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Counter persists across pages:',
              style: TextStyle(fontSize: 18),
            ),
            Obx(() => Text(
                  '${controller.counter.value}',
                  style: const TextStyle(
                    fontSize: 48,
                    fontWeight: FontWeight.bold,
                  ),
                )),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: () {
                Get.back();
              },
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

✅ Minimal boilerplate

✅ Fast development

✅ Includes routing and DI

✅ Good performance

❌ Less Flutter-like API

❌ Can lead to tight coupling

❌ Less community support than Provider


Comparison Table

Solution Complexity Boilerplate Learning Curve Use Case
setState Low Minimal Easy Simple local state
InheritedWidget Medium High Medium Understanding Flutter internals
Provider Low-Medium Low Easy Most applications
Riverpod Medium Low-Medium Medium Modern, type-safe apps
BLoC High High Steep Enterprise apps
GetX Low Minimal Easy Rapid development

Best Practices

1. Start Simple

Don't over-engineer. Use setState() for truly local state, and graduate to more complex solutions as needed.

2. Separate Business Logic

Keep your business logic separate from your UI code, regardless of which state management solution you choose.

3. Immutable State

Prefer immutable state objects. Use copyWith() methods for updates.

class AppState {
  final bool isLoading;
  final List<String> items;

  AppState({required this.isLoading, required this.items});

  AppState copyWith({bool? isLoading, List<String>? items}) {
    return AppState(
      isLoading: isLoading ?? this.isLoading,
      items: items ?? this.items,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Optimize Rebuilds

Use Consumer, Selector, or Obx to rebuild only the widgets that need updating.

// Good - only rebuilds when counter changes
Consumer<CartModel>(
  builder: (context, cart, child) => Text('${cart.itemCount}'),
)

// Bad - rebuilds entire widget tree
Provider.of<CartModel>(context).itemCount
Enter fullscreen mode Exit fullscreen mode

5. Test Your State Logic

State management solutions should make testing easier, not harder.

void main() {
  test('Counter increments', () {
    final controller = CounterController();
    expect(controller.counter.value, 0);
    controller.increment();
    expect(controller.counter.value, 1);
  });
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

There's no one-size-fits-all solution for state management in Flutter. The best choice depends on:

  • App complexity: Simple apps can use setState, complex apps might need BLoC
  • Team experience: Choose what your team knows or can learn quickly
  • Project requirements: Consider testability, scalability, and maintainability
  • Performance needs: Some solutions are more optimized than others

For most applications, Provider or Riverpod offer the best balance of simplicity and power. They're officially recommended, well-documented, and have strong community support.

Remember: the goal is to write maintainable, testable code that solves real problems. Choose the solution that helps you achieve that goal, not the one that's most popular or complex.


Additional Resources

Top comments (0)