Introduction
In the previous article, I described state management from statefulWidget + setState to Provider. This time, we will look at everything from Provider to Riverpod.
Provider challenges
- Context-dependent:
Provideris highly dependent on contextBuildContext, which makes the code difficult to test and reuse. This dependency is especially problematic when trying to access the state from outside the widget tree or from non-UI code. - When using immutability
Provider, you can have mutable state, which can lead to bugs. Ideally, the state should be immutable, butProvideryou cannot force this alone. - Initialization complexity: If there are other states that a particular state depends on,
Providerit is relatively complex to handle this.
Let’s look at an example of a counter app.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class CounterProvider with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('Provider Counter Example'),
),
body: Center(
child: Text('Counter: ${counter.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
context dependent
MyHomePageBuildContextis CounterProviderbeing accessed using . This context dependency causes problems, especially when you want to access the state but its position in the widget tree is unclear.
Immutability problem = object overwriting
CounterProvider_countstate that can be changed within . Flutter is a reactive framework, so it typically notifyListeners()calls to notify the widget tree of changes. However, if for some reason notifyListeners()it is not called, the UI will be out of sync with the actual state.
Initialization complexity
Although not clearly shown in this simple counter app, in more complex scenarios where multiple counters depend on each other, Providermanaging these dependencies can become cumbersome. For example, if one counter is to be initialized dependent on the value of another counter, this dependency needs to be precisely coded, but Providermanaging it alone may not be intuitive.
How to write Riverpod
Riverpodis a library designed to try to solve these problems. ProviderDeveloped by the authors of and Providerimproved upon the ideas of.
A similar counter app implemented using Riverpod would look like this:
Add ProviderScope to route
ProviderScope If you enclose a Widget in the upper tree with , you will be able to call the Provider in the Widget in the lower tree. In the code below, by MyApp() enclosing ProviderScope , Provider can be called by Widgets after MyApp.
void main() {
runApp(
ProviderScope(child: MyApp()),
);
}Define Provider as a global variable
Provider as a global variable
counterProvideris a global variable that can be accessed from anywhere in the app. ThisCounterNotifierallows references to manage state from anywhereStateNotifierProviderWhen defining, < subclass name of managed, type of data to be managed> is requiredStateNotifierProviderafter .StateNotifierStateNotifiercounterProviderisStateNotifierProvider<CounterNotifier, int>an object of type, and its entity isCounterNotifieran instance of the class.CounterNotifierinheritsStateNotifier<int>from and manages the state of integer types.
callback function
StateNotifierProviderThe callback function passed to the constructorCounterNotifiercreates and returns an instance of . This function is executed the first time the provider is called and initializes the state.
ProviderRef
- The callback function argument is an object used to refer to state or services from other providers .
refProviderRef
StateNotifier
CounterNotifieris a class that inherits fromStateNotifier, which manages the state of the counter (in this case, the integer value).super(0)In the constructor ,0it means that the initial value of the counter is set to .
Update status
incrementThe method increments the value of the counter.staterepresents the current counter value, andstate++the state is updated by. This is immutable state management, where new state objects are created to update state and old state objects are destroyed .StateNotifiernotifies subscribed widgets when the state is updated and triggers a redraw of the UI.
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
state++;
}
}Example of using ref
For example, CounterNotifierif gets an initial value from a configuration provider on startup, refyou can access that value using:
In this example, CounterNotifierthe constructor of receives an initial value, which settingsProvideris obtained from . ref.readmethod settingsProviderto access the current value and CounterNotifieruse that value to initialize the . refis the key object for accessing other providers within a provider’s callback.
Dart
final settingsProvider = Provider<Settings>((ref) => Settings());
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
// Get settings from settingsProvider
final settings = ref.read(settingsProvider);
// Initialize CounterNotifier by getting initial counter value from configuration
return CounterNotifier(settings.initialCounterValue);
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier(int initialCounter) : super(initialCounter);
void increment() {
state++;
}
}Get data from Provider
ConsumerWidget
ConsumerWidgetis a special widget provided by Riverpod that youProvidercan inherit from to configure the widget to read data from and rebuild based on changes to that data.
WidgetRef
buildWidgetRefThe arguments passed to the methodProviderare used to read data from. is used in place of and provides access to data and additional functionality regarding the widget lifecycle.WidgetRefBuildContextProvider
watch() and read()
ref.watch(),ref.read()The argument specified to the method is a globally defined provider object. In this case,ref.watch(counterProvider)by calling ,counterProviderwe can access the state managed by , in this case the value of a counter, and watch that value change.ref.watch():When the UI needs to monitor data changesref.read(): When the UI does not need to monitor data changes. For example, when triggering an action (e.g. processing when a button is tapped).
Calling increment()
FloatingActionButtononPressedBy calling Soderef.read(counterProvider.notifier).increment(), the value of the counter increases.StateNotifierA subclass of can be obtained by specifying an instanceWidgetRefof the classwatch()orread()as an argument .StateNotifierProvider.notifierStateNotifierprovides two different objects, and it is not necessary to directly manage and access the state ( Is necessary.state.notifier.notifier
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: Text('Riverpod Counter Example'),
),
body: Center(
child: Text('Counter: $count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}What changed and what stayed the same from Provider to Riverpod?
ConsumerWidget role: Same
- In both cases,
ConsumerWidgetis responsible for updating the UI based on data changes.ConsumerWidgetallows the widget to be rebuilt when the data it depends on changes.
Introduction of WidgetRef
BuildContextProvider requires when accessing state . It locates and references the state based on the widget’s position in the Flutter widget tree. That is, get the state using , like orProvider.of<T>(context).context.watch<T>()BuildContext- With Riverpod
WidgetRef‘s introduction, developers can use widgets to read state and monitor state changes using widgetsBuildContextwithout worrying about their location. At this time , Riverpod internally handles the location of the widget that needs to change its state (a role traditionally played by BuildContext).ref.read(provider)ref.watch(provider)
Benefits of Riverpod
Context independent (due to the introduction of WidgefRef)
- Riverpod
BuildContextallows you to access state without relying on.ProviderScopeProvide state throughout your app using , andWidgetRefaccess state using . This allows you to easily view the status from anywhere and improves the ease of testing.
Enhanced immutability = discard objects and create new ones instead of overwriting them
StateNotifierBy using , you can guarantee state immutability.stateis kept private andstatecan only be modified through properties. This keeps the UI and state consistent and reduces the risk of bugs.
Clear dependency management
- With Riverpod, dependencies between providers can be clearly defined. To reference a provider, use
ref.watchorref.read, which allows state dependencies to be managed clearly and concisely.
Difference and usage of CosumerWidget and ConsumerStatefulWidget
ConsumerWidget
ConsumerWidgetis immutable and has no internal state. Any state change causes the entire widget to be rebuilt. In a class that inherits, the method receives as an argument.ConsumerWidgetbuildWidgetRef- In cases where state management is not required (displaying static content, simple display of external data, etc.), it
ConsumerWidgetis simpler and easier to maintain. StatelessWidgetWidgetUse when you Note thatConsumerif you use it without wrapping it with ,Widgetthe entire file will be rebuilt.
class MyConsumerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}ConsumerStatefulWidget
ConsumerStatefulWidgetRiverpod compatible version ofConsumerStateTohatosono . The main difference is that objects are included as properties. This allows you to access Riverpod’s features at any time within the app .StatefulWidgetStateConsumerStaterefConsumerStaterefConsumerWidgetis passed as a method argument , but no such argument is passed in . Since is a property of you can use it directly within the class .buildrefConsumerStatefulWidgetrefConsumerStateConsumerStaterefref.watchIf there are multiple , and the state is updated frequently ,ConsumerWidgetit will be rebuilt each time, which may be inefficient in terms of performance (Consumerif you use it without wrapping it, the state change part will be rebuilt) (This can be prevented, as it is subject to In such scenarios,ConsumerStatefulWidgetit is better to use .
class MyConsumerStatefulWidget extends ConsumerStatefulWidget {
@override
_MyConsumerStatefulWidgetState createState() => _MyConsumerStatefulWidgetState();
}
class _MyConsumerStatefulWidgetState extends ConsumerState<MyConsumerStatefulWidget> {
@override
Widget build(BuildContext context) {
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}6 types of providers
| Provider name | Type of data to manage | the purpose |
| Provider | Any | Provide data that cannot be modified externally and is accessible throughout the app |
| StateNotifierProvider | Subclass of StateNotifier | Provide state management that encapsulates state changes and business logic |
| FutureProvider | any Future | Handle the results of asynchronous operations and provide data that will be available at some point in the future |
| StreamProvider | any Stream | Subscribe to data from streams and provide data that changes over time |
| StateProvider | Any | Provides simple state reading and writing and easy state management within widgets |
| ChangeNotifierProvider | Subclasses of ChangeNotifier | Use ChangeNotifier to listen for state changes and notify multiple listeners about state changes |
Provider
- Purpose: Provide data that cannot be changed externally. Used when you want the provider’s users to only read the value without manipulating it.
- Use case: Provide configuration information that is shared across applications.
final configProvider = Provider((ref) => AppConfig(apiBaseUrl: '<https://api.example.com>'));
final config = ref.watch(configProvider);StateProvider
- Purpose: Provide simple state reading and writing and easy state management within widgets. Used when you want the provider’s users to manipulate and reference the value.
- Example use: Manage UI toggle state.
final toggleProvider = StateProvider<bool>((ref) => false);
ref.read(toggleProvider.notifier).state = !ref.read(toggleProvider);StateNotifierProvider
- Purpose: Provide state management that encapsulates state changes and business logic.
- Usage example: State management of a counter app.
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
ref.read(counterProvider.notifier).increment();FutureProvider
- Purpose: Handle the results of asynchronous operations and provide data that will be available at some point in the future.
- Example usage: Fetching data from a network request.
final fooProvicer = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 3));
return "foo";
});
class FooWidget extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer(
builder: (context, ref, child) => ref.watch(fooProvicer).when(
data: (foo) => Text(foo),
error: (err, st) => const Text("Error"),
loading: () => const Text("Loading"),
),
);
}StreamProvider
- Purpose: Subscribe to data from streams and provide data that changes over time. Stream version of FutureProvider
- Use case: Provide real-time stock price information.
final fooProvicer = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 3));
return "foo";
});
class FooWidget extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer(
builder: (context, ref, child) => ref.watch(fooProvicer).when(
data: (foo) => Text(foo),
error: (err, st) => const Text("Error"),
loading: () => const Text("Loading"),
),
);
}ChangeNotifierProvider
- Purpose:
ChangeNotifierListen for state changes using and notify multiple listeners of state changes.
final formStateProvider = ChangeNotifierProvider<FormStateNotifier>((ref) => FormStateNotifier());



コメント