Sneak peak on what's coming to Riverpod 3.0 (@FlutterNinjas Tokyo 2024)
Table of contents
- Remi's first visit to Japan to speak at FlutterNinjas Tokyo2024 🇯🇵
- 1. removing redundant types
- 2. Generic providers
- 3. Destructive changes to Scoped Provider
- 4. Support for FamilyProvider arguments
- 5. Addition of if(mounted)
- 6. Simplified description during testing
- 7. ref.listen without initialization
- 8. Enhanced support for side-effects
- 9. Automatic retry
- 10. Offline caching
- About FlutterNinjas Tokyo🥷
Remi's first visit to Japan to speak at FlutterNinjas Tokyo2024 🇯🇵
Remi Rousselet, renowned Riverpod developer, came to Japan to speak at FlutterNinjas Tokyo2024, Japan's first global Flutter conference, which took place on 13-14 June in Tokyo!
He gave a session on the features to be added to Riverpod 3.0, which is due for release by the end of the year. The following is an extract from the session.
1. removing redundant types
No more AutoDispose subclasses for Provider and Notifier.
No more Ref subclasses.
StateNotifier, StateProvider, etc. are transferred to the legacy package.
2. Generic providers
Providers that accept Generic types can now be defined.
Providers can be created that specify the type at definition time as follows.
class MyListNotifier<T> exetends _$MyListNotifier<T>{
List<T> build() => [];
}
NotOnly supported by code generation.Support for more complex generics.
@riverpod
Pair<A,B> pair<A extends num, B extends Object>(
MyListRef<T> ref,
A a,
B b,
){
return Pair(a,b);
}
3. Destructive changes to Scoped Provider
Scoped providers are required to include the
dependencies
option from ver 3.Scoped Providers: Providers that are supposed to be overridden.
ProviderScope`, which could previously be implemented from anywhere in the widget tree and overridden by a Provider, can now only be used at the top level
Providers that want to override outside the top level need to use the
dependencies: []
annotation when defining the Provider to make it clear that it is an overrideable Provider.Performance improvements are expected by not having the option of whether or not the Provider is overridden.
@Riverpod(dependencies: [])
int b(ref) => 0;
void main(){
runApp(
ProviderScope(
overrides:[
bProvider.overrideWithValue(42),
]
),
child: ProviderScope(
overrides:[
bProvider.overrideWithValue(42),
]
),
);
}
These can also be detected by the
provider_dependencies
rule inriverpod_lint
.Scoped Providers can also be described more concisely
// Full
@Riverpod(dependencies: [])
class MyNotifier{
@override
int build(){
throw UnimplementedError();
}
}
// Simpler
@riverpod
class MyNotifier{
@override
int build();
}
4. Support for FamilyProvider arguments
When using FamilyProvider with multiple widgets, it was necessary to prepare the arguments to be passed to multiple widgets, but by applying the above change to ScopedProvider, it is now possible to write more concisely.
Until now, it was necessary to pass arguments for each FamilyProvider
@riverpod
int myFamily(MyFamilyRef ref, {required int id}) => 0;
class Example extends ConsumerWidget{
Example({super.key, required this.id});
final int id;
@override
Widget build(context, ref){
ref.watch(
myFamilyProvider(id: id),
);
}
}
- However, from now on, by writing the FamilyProvider to override in the Dependencies annotation without arguments, it is only necessary to define the arguments on the ProviderScope side.
Widget build(context){
return ProviderScope(
overrides: [
myFamilyProvider.overrideWithDefault(id:id)
],
child: Example(),
)
}
@Dependencies([myFamily])
class Example extends ConsumerWidget{
Example({super.key});
@override
Widget build(context, ref){
ref.watch(myFamilyProvider);
}
}
5. Addition of if(mounted)
- Enables
mounted
checks in Provider.
@riverpod
class Example extends _$Example{
@override
int build() => 0;
Future<void> asyncMethod() async {
await something();
if(!mounted) return;
}
}
6. Simplified description during testing
- Provider tests can be written more concisely with
ProviderContainer.test()
.
BEFORE
ProviderContainer createContainer({
List<ProviderOverride>? overrides,
}) {
final container = ProviderContainer(overrides: []);
addTearDown(container.dispose);
return container;
}
test('example', (){
final container = createContainer();
...
})
AFTER
test('example', (){
final container = ProviderContainer.test();
...
})
7. ref.listen without initialization
Allow ref.listen to be defined without Provider initialization
Turn on/off functionality with a boolean passed to the
weak
parameter in ref.listenIf
weak: true
, changes can be detected later even if they are not initialized at the time of listenUseful for monitoring login status, etc.
@riverpod
int another(AnotherRef ref){
ref.listen(exampleProvider, weak: true, (prev, next){
...
});
}
8. Enhanced support for side-effects
Changing the UI according to the state of processes that involve changes to the server (side-effects), such as POST, has been an issue for some time, and a new mutation method has now been added.
(For more information on side-effect issues here)
Implemented a static method with
@mutation
annotation for NotifierWriting the method to return a new state value as return value.
@riverpod class TodoList extends _$TodoList{ @mutation static Future<List<Todo>> addTodo( MutationRef<TodoList> ref, Todo todo, ){ final response = await http.get( 'your-api/todos/new', todo.toJson(), ); return (response as List) // New state .map(Todo.fromJson) .toList(); }
On the UI side, monitor the
MutationState
object returned by the mutation method and write UI processing according to that state value
class SuperButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final mutation = ref.watch(todoNotifier.addTodo);
return switch (MutationState addTodo) {
EmptyMutationState() => ElevatedButton(
onPressed: () => addTodo(Todo('New todo!')),
child: Text('Add todo'),
),
LoadingMutationState() => ElevatedButton(
onPressed: null,
child: CircularProgressIndicator(),
),
ErrorMutatationState() => ElevatedButton(
onPressed: addTodo.retry,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.red),
foregroundColor: WidgetStateProperty.all(Colors.white),
),
child: Text('Error. Retry?'),
),
SuccessMutationState() => ElevaedButton(
onPressed: null,
child: Icon(Icons.check),
),
};
}
}
- This allows mutation state values to be monitored elsewhere in the UI by watching the Provider as follows, so that multiple UIs can display based on the same state values
class SuperAppBar extends ConsumerWidget{
@override
Widget build(context, ref){
final addTodo = ref.watch(todoListProvider.notifier);
}
}
9. Automatic retry
- Automatic retry when an Exception is thrown in the Provider.
@riverpod
int example() {
print(DateTime.now().seconds);
throw Exception();
}
// Execution result: 1,2,4,8.
- If you want to add custom retry logic, you can pass it to the Provider with
@Riverpod(retry: myRetry)
.
@Riverpod(retry: myRetry)
int example(ExampleRef ref){
print(DateTime.now().seconds);
throw Exception();
}
Duration? myRetry(int retry Count, Object error){
if(retryCount > 10) return null;
return Duration(
seconds: 1 + retryCount * retry Count,
)
}
10. Offline caching
Enables Provider values to be cached directly on the device.
Binding of local DB (ex. SharedPreference, SQLite, etc.) classes defined in a separate package to ProviderScope
ProviderScope(
offlineConnector: const SharedAppPreferenceAsJson(),
)
- Add the required serialization methods to the model class (ex. fromJson, toJson, etc.).
class Product{
factory Product.fromJson(Map<String, Object?> json){
...
}
Map<String, Object?> toJson() => ...
}
- Define the name of the table to be stored offline on the Provider side as an annotation.
@Riverpod(offline: '<name-table>')
Future<List<Product>> products(ProductRef ref){
final response = http.get('my-api/products');
return (response as List).map(Product.fromJson).toList();
}
- If changes occur in the table definitions on the local DB = changes occur in the model definitions, the
destroyKey
option can be added to allow caching the data of the new schema definition
@Riverpod(offline: '<name-table>', destroyKey: 'abc')
Future<List<Product>> products(ProductRef ref){
final response = http.get('my-api/products');
return (response as List).map(Product.fromJson).toList();
}
About FlutterNinjas Tokyo🥷
FlutterNinjas Tokyo is Japan's first global conference dedicated to Flutter. This year was the first time the conference was held, with Code Magic as platinum sponsor and Money Forward Inc. as gold sponsor, and over 130 Flutter developers from Japan and overseas attended, both as speakers and visitors. All sessions and workshops were conducted in English, with the mission to provide unprecedented inspiration to Flutter developers in Japan.
https://twitter.com/FlutterNinjas
https://www.linkedin.com/company/flutterninjas-tokyo/
We want to create a great community where you can get the latest information before anywhere else, like this article, and interact with the developers you admire, Flutter developers in Japan and abroad.
We will also be streaming session videos from the event & will be holding the event again next year. Please follow us 🙌🥳.
Subscribe to my newsletter
Read articles from Shohei Ogawa(@heyhey1028) directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by