Back to Posts

Flutter State Management Patterns: A Practical Guide

10 min read

State management is a crucial aspect of Flutter application development. This guide explores different state management patterns and helps you choose the right approach for your needs.

1. setState Pattern

The simplest form of state management, suitable for small widgets.

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

When to Use setState

  • Small widgets
  • Local state management
  • Simple UI updates
  • No need for state sharing

2. Provider Pattern

A lightweight solution for state management and dependency injection.

// Data model
class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

// Provider setup
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

// Using Provider
class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CounterModel>(
      builder: (context, counter, child) {
        return Column(
          children: [
            Text('Count: ${counter.count}'),
            ElevatedButton(
              onPressed: () => counter.increment(),
              child: Text('Increment'),
            ),
          ],
        );
      },
    );
  }
}

Provider Best Practices

// Multiple providers
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserModel()),
        ChangeNotifierProvider(create: (_) => ThemeModel()),
        ChangeNotifierProvider(create: (_) => SettingsModel()),
      ],
      child: MyApp(),
    ),
  );
}

// Selective rebuilds
class UserProfile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Only rebuilds when name changes
        Selector<UserModel, String>(
          selector: (_, user) => user.name,
          builder: (_, name, __) => Text(name),
        ),
        // Only rebuilds when email changes
        Selector<UserModel, String>(
          selector: (_, user) => user.email,
          builder: (_, email, __) => Text(email),
        ),
      ],
    );
  }
}

3. BLoC Pattern

Business Logic Component pattern for complex state management.

// Events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}

// States
abstract class CounterState {
  final int count;
  CounterState(this.count);
}
class CounterInitial extends CounterState {
  CounterInitial() : super(0);
}
class CounterUpdated extends CounterState {
  CounterUpdated(int count) : super(count);
}

// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitial()) {
    on<IncrementEvent>((event, emit) {
      emit(CounterUpdated(state.count + 1));
    });

    on<DecrementEvent>((event, emit) {
      emit(CounterUpdated(state.count - 1));
    });
  }
}

// Using BLoC
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: CounterView(),
    );
  }
}

class CounterView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CounterBloc, CounterState>(
      builder: (context, state) {
        return Column(
          children: [
            Text('Count: ${state.count}'),
            Row(
              children: [
                ElevatedButton(
                  onPressed: () => context
                      .read<CounterBloc>()
                      .add(IncrementEvent()),
                  child: Text('Increment'),
                ),
                ElevatedButton(
                  onPressed: () => context
                      .read<CounterBloc>()
                      .add(DecrementEvent()),
                  child: Text('Decrement'),
                ),
              ],
            ),
          ],
        );
      },
    );
  }
}

4. Riverpod Pattern

A more type-safe and maintainable alternative to Provider.

// Provider definition
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
  void decrement() => state--;
}

// Using Riverpod
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: Text('Increment'),
        ),
      ],
    );
  }
}

// Complex state management
final userProvider = StateNotifierProvider<UserNotifier, User>((ref) {
  return UserNotifier();
});

class UserNotifier extends StateNotifier<User> {
  UserNotifier() : super(User.empty());

  Future<void> fetchUser(String id) async {
    state = await api.getUser(id);
  }

  void updateName(String name) {
    state = state.copyWith(name: name);
  }
}

5. GetX Pattern

A lightweight yet powerful solution combining state management, navigation, and dependency injection.

// Controller
class CounterController extends GetxController {
  var count = 0.obs;

  void increment() => count++;
  void decrement() => count--;
}

// Dependency injection
void main() {
  Get.put(CounterController());
  runApp(MyApp());
}

// Using GetX
class CounterView extends StatelessWidget {
  final controller = Get.find<CounterController>();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Obx(() => Text('Count: ${controller.count}')),
        ElevatedButton(
          onPressed: controller.increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

// Navigation
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => Get.to(() => DetailPage()),
      child: Text('Go to Detail'),
    );
  }
}

Choosing the Right Pattern

  1. setState

    • Small widgets
    • Local state
    • Simple interactions
  2. Provider

    • Medium-sized apps
    • Shared state
    • Simple state logic
  3. BLoC

    • Large apps
    • Complex business logic
    • Event-driven architecture
  4. Riverpod

    • Type safety critical
    • Complex dependencies
    • Testing focus
  5. GetX

    • Rapid development
    • All-in-one solution
    • Simple syntax

Best Practices

1. State Organization

// Separate state logic
class UserRepository {
  Future<User> fetchUser(String id) async {
    // API call
  }
}

class UserBloc extends Cubit<UserState> {
  final UserRepository repository;
  
  UserBloc(this.repository) : super(UserInitial());
  
  Future<void> loadUser(String id) async {
    emit(UserLoading());
    try {
      final user = await repository.fetchUser(id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

2. State Immutability

@immutable
class User {
  final String id;
  final String name;
  final String email;

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

  User copyWith({
    String? id,
    String? name,
    String? email,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }
}

3. Error Handling

class AsyncValue<T> {
  final T? data;
  final String? error;
  final bool isLoading;

  const AsyncValue({
    this.data,
    this.error,
    this.isLoading = false,
  });

  bool get hasData => data != null;
  bool get hasError => error != null;
}

// Usage
class UserProvider extends StateNotifier<AsyncValue<User>> {
  UserProvider() : super(AsyncValue());

  Future<void> fetchUser(String id) async {
    state = AsyncValue(isLoading: true);
    try {
      final user = await api.getUser(id);
      state = AsyncValue(data: user);
    } catch (e) {
      state = AsyncValue(error: e.toString());
    }
  }
}

Conclusion

Effective state management is crucial for building maintainable Flutter applications. Remember to:

  1. Choose the right pattern for your needs
  2. Keep state logic separate from UI
  3. Maintain immutability
  4. Handle errors gracefully
  5. Test state changes thoroughly

By following these patterns and best practices, you can create Flutter applications with clean, maintainable, and scalable state management.