Choosing the Best Flutter State Management in 2021: InheritedWidget, Provider, and Riverpod
This article explains when to use a state‑management solution in Flutter, compares Ephemeral and App state, and provides detailed overviews, usage steps, and pros‑cons of InheritedWidget, Provider, and the newer Riverpod library.
Unlike Redux’s dominance in React, Flutter offers many data‑flow solutions; this article introduces the most worthwhile options for 2021, including the familiar InheritedWidget and provider, as well as the eagerly anticipated Riverpod.
1. When does a state require a data‑flow management solution?
For declarative UI, UI = f(state); the build method is f, so a solution should let consumers obtain data, be notified on updates, and avoid unnecessary rebuilds.
Only the part of the state that triggers UI updates matters. State can be divided into Ephemeral State (local to a single widget, e.g., animation progress) and App State (shared across the app, e.g., login status).
Ephemeral State is used by a single widget, such as the progress of a complex animation.
App State is retained globally and shared by many components, like a user’s login status.
Generally, the type of a state can be inferred from which components use it.
Ephemeral State can be managed with a StatefulWidget.
For App State, consider the following approaches:
InheritedWidget – a built‑in component for sharing data with descendants.
Event Bus – a global singleton (not covered here).
Data‑flow frameworks – community solutions such as provider or Riverpod.
2. InheritedWidget
InheritedWidget is a crucial Flutter component that propagates ancestor state to descendants without passing parameters manually . Its properties are final , meaning that updating them triggers a UI rebuild.
2.1 How to use it
Typical steps:
Create a widget that extends InheritedWidget and holds the state and methods to modify it.
Place this widget at the top of the subtree that needs the state.
In descendant widgets, call the static
of(context)method to retrieve the state.
Now see how InheritedWidget works internally.
2.2 Establishing dependency between nodes
When a child calls
of, Flutter invokes
BuildContext.getElementForInheritedWidgetOfExactTypeto create a dependency.
Each
Elementmaintains two maps:
_inheritedWidgets: a
Map<Type, InheritedElement>of all ancestor inherited widgets.
_dependencies: records the specific ancestors this element depends on.
InheritedElement also keeps a list of its dependents. When a matching ancestor is found, it registers the dependent and calls
updateInheritanceon the ancestor.
2.3 Why use BuildContext to obtain data?
Children retrieve data via
XXDataWidget.of(context).data. The
contextrepresents the widget’s element, which provides access to the nearest matching inherited widget.
<code>@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
</code>Because
_inheritedWidgetsis a
Map, it can only locate the nearest ancestor of the same type, which is a limitation.
2.4 Update mechanism
When an update is needed,
InheritedWidget.updateShouldNotify(usually overridden by the user) decides whether to notify dependents; if so, it iterates over
_dependentsand calls
Element.didChangeDependencies.
3. provider
Although InheritedWidget works, it requires a lot of boilerplate. Provider, now at version 4.3.3, is the officially recommended solution and reduces repetitive code.
Provider builds on InheritedWidget but adds many optimizations such as lazy loading and selective rebuilds.
3.1 How to use it
Provider usage is extensive; detailed guides are available online.
Flutter | State Management Guide – Provider https://juejin.cn/post/6844903864852807694
3.2 Simple implementation
Provider is essentially a thin wrapper around InheritedWidget. Readers are encouraged to implement a minimal
mini_providerto deepen understanding.
Cross‑component state sharing (Provider) https://book.flutterchina.club/chapter7/provider.html
3.3 Provider and MVVM
Common pitfalls when first using provider include excessive rebuilds if Selector/Consumer are not used properly, and difficulty handling dependent data across pages.
Even with lazy loading, missing Selector or Consumer can cause unnecessary UI refreshes.
When pages have data dependencies, refreshing becomes cumbersome.
Flutter does not enforce a specific architecture; you can adopt MVC, MVVM, or any pattern you prefer.
Key MVVM concepts:
View – the UI and user interactions.
Model – holds raw data.
ViewModel – processes data for the view and contains helper functions.
Repository – fetches data from databases or APIs.
Bean – data entity definitions.
Separating ViewModel from Model prevents unnecessary rebuilds of the Model when the ViewModel updates.
3.4 Encapsulating a generic page container
Most pages need to handle loading, empty, error, and success states. A reusable container can abstract this logic, favoring composition over inheritance for UI components.
Typical classes involved:
ChangeNotifier – Flutter’s listener‑subscriber base class.
NormalPageState – enumeration of page states.
NormalPageController – extends ChangeNotifier to manage state changes; mixed into a ViewModel.
ContainerStateManager – displays content based on
NormalPageStateand subscribes via provider.
3.5 Drawbacks
Provider’s limitations:
It couples dependency injection with UI code.
Because it relies on InheritedWidget, it can only locate the nearest same‑type ancestor.
State availability is discovered at runtime.
The author of provider, Remi Rousselet, created Riverpod to address these three issues while preserving provider’s ergonomics.
4. Riverpod
Riverpod’s slogan is “provider but different”. It decouples from Flutter, solves provider’s shortcomings, and is a promising 2021 solution.
4.1 Usage
4.1.1 Where is the state stored?
Wrap the root widget with
ProviderScope; multiple scopes can override upper‑level state.
<code>void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
</code>4.1.2 Listening to provider changes and rebuilding UI
Method 1: Use
ConsumerWidget, which provides a
ScopedReaderto read providers and trigger rebuilds.
<code>typedef ScopedReader = T Function<T>(ProviderBase<Object, T> provider);
</code> <code>final helloProvider = Provider((_) => 'Hello World');
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final String value = watch(helloProvider);
return Scaffold(
appBar: AppBar(title: Text('Example')),
body: Center(child: Text(value)),
);
}
}
</code>Method 2: Use a regular
Consumerwidget inside a
StatelessWidget.
<code>class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Example')),
body: Center(
child: Consumer(
builder: (context, watch, child) {
final String value = watch(helloProvider);
return Text(value);
},
),
),
);
}
}
</code>4.1.3 Accessing a provider outside of build
Use
context.readto obtain a provider instance.
<code>class IncrementNotifier extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value += 1;
notifyListeners();
}
}
final incrementProvider = ChangeNotifierProvider((ref) => IncrementNotifier());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, watch, child) {
final incrementNotifier = watch(incrementProvider);
return Text(incrementNotifier.value.toString());
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(incrementProvider).increment();
},
child: Icon(Icons.add),
),
),
);
}
}
</code>4.2 Advantages
Riverpod can retrieve the same type of data from ancestors, not just the nearest one.
<code>final firstStringProvider = Provider((ref) => 'First String Data');
final secondStringProvider = Provider((ref) => 'Second String Data');
// Inside a ConsumerWidget
final first = watch(firstStringProvider);
final second = watch(secondStringProvider);
</code>Dependencies become simpler.
<code>class FakeHttpClient {
Future<String> get(String url) async {
await Future.delayed(const Duration(seconds: 1));
return 'Response from $url';
}
}
final fakeHttpClientProvider = Provider((ref) => FakeHttpClient());
final responseProvider = FutureProvider<String>((ref) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get('https://baidu.com');
});
</code>5. Summary
A quick comparison of the three data‑flow solutions:
Solution
Pros
Cons
InheritedWidget
Built‑in Flutter solution.
Excessive boilerplate; can only find the nearest same‑type state.
Provider
Comprehensive, many optimizations, large community, stable.
Couples DI with UI; limited to nearest same‑type state; runtime discovery only.
Riverpod
Created by provider’s author, fixes provider’s three main drawbacks; not coupled to Flutter; supports flutter_hooks.
Still in beta.
Riverpod is essentially a new version of provider with added benefits and is the most promising state‑management option for new projects in 2021; flutter_hooks is also worth exploring as a replacement for StatefulWidget.
QQ Music Frontend Team
QQ Music Web Frontend Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.