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:
Provider
is 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, butProvider
you cannot force this alone. - Initialization complexity: If there are other states that a particular state depends on,
Provider
it 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
MyHomePage
BuildContext
is CounterProvider
being 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
_count
state 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, Provider
managing 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 Provider
managing it alone may not be intuitive.
How to write Riverpod
Riverpod
is a library designed to try to solve these problems. Provider
Developed by the authors of and Provider
improved 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
counterProvider
is a global variable that can be accessed from anywhere in the app. ThisCounterNotifier
allows references to manage state from anywhereStateNotifierProvider
When defining, < subclass name of managed, type of data to be managed> is requiredStateNotifierProvider
after .StateNotifier
StateNotifier
counterProvider
isStateNotifierProvider<CounterNotifier, int>
an object of type, and its entity isCounterNotifier
an instance of the class.CounterNotifier
inheritsStateNotifier<int>
from and manages the state of integer types.
callback function
StateNotifierProvider
The callback function passed to the constructorCounterNotifier
creates 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 .
ref
ProviderRef
StateNotifier
CounterNotifier
is a class that inherits fromStateNotifier
, which manages the state of the counter (in this case, the integer value).super(0)
In the constructor ,0
it means that the initial value of the counter is set to .
Update status
increment
The method increments the value of the counter.state
represents 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 .StateNotifier
notifies 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, CounterNotifier
if gets an initial value from a configuration provider on startup, ref
you can access that value using:
In this example, CounterNotifier
the constructor of receives an initial value, which settingsProvider
is obtained from . ref.read
method settingsProvider
to access the current value and CounterNotifier
use that value to initialize the . ref
is 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
ConsumerWidget
is a special widget provided by Riverpod that youProvider
can inherit from to configure the widget to read data from and rebuild based on changes to that data.
WidgetRef
build
WidgetRef
The arguments passed to the methodProvider
are used to read data from. is used in place of and provides access to data and additional functionality regarding the widget lifecycle.WidgetRef
BuildContext
Provider
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 ,counterProvider
we 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()
FloatingActionButton
onPressed
By calling Soderef.read(counterProvider.notifier).increment()
, the value of the counter increases.StateNotifier
A subclass of can be obtained by specifying an instanceWidgetRef
of the classwatch()
orread()
as an argument .StateNotifierProvider
.notifier
StateNotifier
provides 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,
ConsumerWidget
is responsible for updating the UI based on data changes.ConsumerWidget
allows the widget to be rebuilt when the data it depends on changes.
Introduction of WidgetRef
BuildContext
Provider 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 widgetsBuildContext
without 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
BuildContext
allows you to access state without relying on.ProviderScope
Provide state throughout your app using , andWidgetRef
access 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
StateNotifier
By using , you can guarantee state immutability.state
is kept private andstate
can 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.watch
orref.read
, which allows state dependencies to be managed clearly and concisely.
Difference and usage of CosumerWidget and ConsumerStatefulWidget
ConsumerWidget
ConsumerWidget
is 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.ConsumerWidget
build
WidgetRef
- In cases where state management is not required (displaying static content, simple display of external data, etc.), it
ConsumerWidget
is simpler and easier to maintain. StatelessWidgetWidget
Use when you Note thatConsumer
if you use it without wrapping it with ,Widget
the 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
ConsumerStatefulWidget
Riverpod compatible version ofConsumerState
Tohatosono . The main difference is that objects are included as properties. This allows you to access Riverpod’s features at any time within the app .StatefulWidget
State
ConsumerState
ref
ConsumerState
ref
ConsumerWidget
is 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 .build
ref
ConsumerStatefulWidget
ref
ConsumerState
ConsumerState
ref
ref.watch
If there are multiple , and the state is updated frequently ,ConsumerWidget
it will be rebuilt each time, which may be inefficient in terms of performance (Consumer
if 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,ConsumerStatefulWidget
it 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:
ChangeNotifier
Listen for state changes using and notify multiple listeners of state changes.
final formStateProvider = ChangeNotifierProvider<FormStateNotifier>((ref) => FormStateNotifier());
コメント