State Management in Flutter with Provider & Flutter Hooks

I have been using Flutter since 2020.
I tried many state management solutions: Provider, Riverpod, BLoC, MobX, etc.
I finally returned to the traditional Provider.

Recently the Flutter community seems to talk a lot about state management and the verbosity of Flutter.
Let me share my opinion about it.

For me, I prefer to use Provider and Hooks.
Why?
Less frequent breaking changes and more stability.
You don’t need to worry about breaking changes — just focus on building your app.
With flutter_hooks, you can write less code and make it more readable.

Here’s how I use Provider and Hooks.

For state, I consider two types:

  1. App state: state that needs to be shared across the app.
  2. Page state: state that is used only within a single page.

All state is represented by a simple class extending ChangeNotifier.

class HomeState with ChangeNotifier {
  String? errorMessage; // You can create getter/setter if needed. For sample, I keep it simple.
  String? query;
  List<String> items = [];

  void search(String text) {
    query = text;
    notifyListeners();

    // Simulate a search operation
    items = List.generate(5, (index) => '$text result $index');
    notifyListeners();
  }
} 

I also have some custom extensions to make usage easier.

T get<T>() {
  final context = useContext();
  return useMemoized(() => context.read<T>());
}

extension ListenableX<Notifier extends Listenable> on Notifier {
  T watch<T>(
    T Function(Notifier $) selector,
  ) {
    final notifier = this;
    final result = useState(selector(notifier));
    listen(
      selector,
      (previous, next) {
        Future.microtask(() => result.value = next);
      },
    );
    return result.value;
  }

  void listen<T>(
    T Function(Notifier $) selector,
    void Function(T? previous, T next) listener,
  ) {
    final notifier = this;
    final previousValue = useRef<T?>(null);
    final isFirst = useRef(true);

    useOnListenableChange(
      notifier,
      () {
        final previous = previousValue.value;
        final next = selector(notifier);
        if (isFirst.value || previous != next) {
          isFirst.value = false;
          previousValue.value = next;
          listener(previous, next);
        }
      },
    );
  }
}

Key points:

  • get — retrieve the ViewModel from the widget’s build method.
  • watch — observe a value from the ViewModel and rebuild when it changes.
  • listen — listen to a value from the ViewModel and trigger side effects when it changes.

Watch and listen accept a selector so you only observe the value(s) you care about.

Now, in a page you can use it like this.

class HomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => HomeState()),
      ],
      child: HomeView(),
    );
  }
}

class HomeView extends HookWidget {
  @override
  Widget build(BuildContext context) {
    // Get the HomeState instance
    final homeState = get<HomeState>();
    
    // Watch the values you care about.
    // You should put the watch inside the widget that actually needs it
    final errorMessage = homeState.watch((s) => s.errorMessage);
    final items = homeState.watch((s) => s.items);
    
    // Listen for side effects
    homeState.listen<String?>(
      (s) => s.errorMessage,
      (previous, next) {
        if (next != null) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(next)),
          );
        }
      },
    );
    
    return View();
  }
}
2 Likes

I do prefer something simpler, like kinora_flow | Flutter package (the concept of dividing the app into features is a must for me, especially when dealing with dozens of apps using the same code base).

But if you are into ChangeNotifier, https://flutter-it.dev/ is a far better option because:

a) get_it has scopes (meaning: you can push a new set of features into the stack, use them and then dispose when you don’t need them anymore, including all dependencies and memory).

b) watch_it is capable of selecting one property to watch in a ChangeNotifier with N properties that can change.

2 Likes

flutter_hooks also plays well with signals via signals_hooks. And signals are more closely integrated with the full dart/flutter ecosystem than Provider package is.

2 Likes