Sneak peak on what's coming to Riverpod 3.0 (@FlutterNinjas Tokyo 2024)

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() => [];
}
  • Not Only 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 in riverpod_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.listen

  • If weak: true, changes can be detected later even if they are not initialized at the time of listen

  • Useful 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 Notifier

  • Writing 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 🙌🥳.

4
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

Shohei Ogawa(@heyhey1028)
Shohei Ogawa(@heyhey1028)