Back to Posts

Flutter State Management

8 min read

This guide covers different state management solutions and best practices for Flutter applications.

1. Provider

1.1. Basic Provider

class CounterProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

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

  void decrement() {
    _count--;
    notifyListeners();
  }
}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => CounterProvider(),
      child: Consumer<CounterProvider>(
        builder: (context, provider, child) {
          return Column(
            children: [
              Text('Count: ${provider.count}'),
              ElevatedButton(
                onPressed: () => provider.increment(),
                child: const Text('Increment'),
              ),
            ],
          );
        },
      ),
    );
  }
}

1.2. MultiProvider

class AppProviders extends StatelessWidget {
  final Widget child;

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AuthProvider()),
        ChangeNotifierProvider(create: (_) => ThemeProvider()),
        ChangeNotifierProvider(create: (_) => SettingsProvider()),
      ],
      child: child,
    );
  }
}

2. BLoC Pattern

2.1. Basic BLoC

class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<IncrementEvent>((event, emit) => emit(state + 1));
    on<DecrementEvent>((event, emit) => emit(state - 1));
  }
}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Column(
            children: [
              Text('Count: $count'),
              ElevatedButton(
                onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
                child: const Text('Increment'),
              ),
            ],
          );
        },
      ),
    );
  }
}

2.2. Complex BLoC

class UserEvent {}

class LoadUserEvent extends UserEvent {
  final String userId;
  LoadUserEvent(this.userId);
}

class UserState {}

class UserLoadingState extends UserState {}

class UserLoadedState extends UserState {
  final User user;
  UserLoadedState(this.user);
}

class UserErrorState extends UserState {
  final String message;
  UserErrorState(this.message);
}

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;

  UserBloc(this.repository) : super(UserLoadingState()) {
    on<LoadUserEvent>(_onLoadUser);
  }

  Future<void> _onLoadUser(LoadUserEvent event, Emitter<UserState> emit) async {
    try {
      emit(UserLoadingState());
      final user = await repository.getUser(event.userId);
      emit(UserLoadedState(user));
    } catch (e) {
      emit(UserErrorState(e.toString()));
    }
  }
}

3. Riverpod

3.1. Basic Provider

final counterProvider = StateProvider<int>((ref) => 0);

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).state++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

3.2. Async Provider

final userProvider = FutureProvider<User>((ref) async {
  final userId = ref.watch(userIdProvider);
  return await UserRepository().getUser(userId);
});

class UserWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);

    return userAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
      data: (user) => Text('User: ${user.name}'),
    );
  }
}

4. GetX

4.1. Reactive State

class CounterController extends GetxController {
  final count = 0.obs;

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

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(CounterController());

    return Column(
      children: [
        Obx(() => Text('Count: ${controller.count.value}')),
        ElevatedButton(
          onPressed: () => controller.increment(),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

4.2. Dependency Injection

class UserController extends GetxController {
  final UserRepository repository;
  final user = Rxn<User>();
  final isLoading = false.obs;

  UserController(this.repository);

  Future<void> loadUser(String id) async {
    isLoading.value = true;
    try {
      user.value = await repository.getUser(id);
    } catch (e) {
      Get.snackbar('Error', e.toString());
    } finally {
      isLoading.value = false;
    }
  }
}

class UserWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.find<UserController>();

    return Obx(() => controller.isLoading.value
        ? const CircularProgressIndicator()
        : Text('User: ${controller.user.value?.name}'));
  }
}

5. Redux

5.1. Basic Redux

class AppState {
  final int count;
  AppState({this.count = 0});
}

class IncrementAction {}

AppState reducer(AppState state, dynamic action) {
  if (action is IncrementAction) {
    return AppState(count: state.count + 1);
  }
  return state;
}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreProvider(
      store: Store<AppState>(
        reducer,
        initialState: AppState(),
      ),
      child: StoreConnector<AppState, int>(
        converter: (store) => store.state.count,
        builder: (context, count) {
          return Column(
            children: [
              Text('Count: $count'),
              ElevatedButton(
                onPressed: () => StoreProvider.of<AppState>(context)
                    .dispatch(IncrementAction()),
                child: const Text('Increment'),
              ),
            ],
          );
        },
      ),
    );
  }
}

6. Best Practices

  1. State Management Selection

    • Choose based on app complexity
    • Consider team experience
    • Evaluate performance needs
  2. State Organization

    • Keep state as local as possible
    • Use immutable state
    • Separate business logic
  3. Performance

    • Minimize rebuilds
    • Use const constructors
    • Implement proper disposal
  4. Testing

    • Test state changes
    • Mock dependencies
    • Verify UI updates
  5. Maintenance

    • Document state flow
    • Follow consistent patterns
    • Keep code organized

Conclusion

Remember these key points:

  1. Choose appropriate state management
  2. Keep state minimal and local
  3. Follow best practices
  4. Test thoroughly
  5. Document well

By following these practices, you can:

  • Build scalable apps
  • Improve maintainability
  • Enhance performance
  • Simplify testing

Keep improving your state management in Flutter applications!