🌊

Riverpod 3.0 最新プレビュー (06.17時点)

2024/06/17に公開
1

Remi氏 FlutterNinjas Tokyo2024登壇の為、初来日🇯🇵

6月13-14日に東京で行われた日本初のグローバルFlutterカンファレンス「FlutterNinjas Tokyo2024」にRiverpodの開発者として著名なRemi Rousselet氏が来日。

年内にリリース予定のRiverpod 3.0に追加される機能についてセッションを行いました。以下はそのセッション内容の抜粋となります。

1. 冗長なタイプを削除

  • ProviderやNotifierのAutoDispose subclassesはなくなる
  • Refのsubclassesもなくなる
  • StateNotifier, StateProviderなどはlegacy packageに移管

2. Generic providers

  • Generic typeを受け取るProviderの定義が可能に
  • 以下のように定義時に型を指定するProviderを作成可能
class MyListNotifier<T> exetends _$MyListNotifier<T>{
    List<T> build() => [];
}
  • code generationでは対応していないでのみ対応
  • より複雑なジェネリクスにも対応

Pair<A,B> pair<A extends num, B extends Object>(
    MyListRef<T> ref,
    A a,
    B b,
){
    returns Pair(a,b);
}

3. Scoped Providerへの破壊的変更

  • Scoped providerはver3よりdependenciesオプションの記述が必須となる
  • Scoped Providers: オーバーライドされることが前提のProvider
  • 以前はwidgetツリーどこからでも実装しProviderをオーバーライドすることが出来たProviderScopeは今後はトップレベルでのみ使用可能に
  • トップレベル以外でオーバーライドしたいProviderはProvider定義時にdependencies: []アノテーションを使って、オーバーライド可能なProviderである事を明示する必要がある
  • Providerがオーバーライドされるか否かの選択肢を持たない事でパフォーマンスの改善が見込まれる
(dependencies: [])
int b(ref) => 0;

void main(){
    runApp(
        ProviderScope(
            overrides:[
                bProvider.overrideWithValue(42),
            ]
        ),
        child: ProviderScope(
            overrides:[
                bProvider.overrideWithValue(42),
            ]
        ),
    );
}
  • これらはriverpod_lintprovider_dependenciesルールでの検知も可能になる
  • Scoped Providerはより簡潔に記述することも可能
// Full
(dependencies: [])
class MyNotifier{
    
    int build(){
        throw UnimplementedError();
    }
}

// Simpler

class MyNotifier{
    
    int build();
}

4. FmailyProvider引数へのサポート

  • FamilyProviderを複数のwidgetで使用する場合、渡す引数を複数のwidgetに用意する必要があったが、上記のScopedProviderへの変更を応用する事でより簡潔に記述する事が可能になった
  • 今までは、FamilyProvider1つ1つに引数を渡す必要があった

int myFamily(MyFamilyRef ref, {required int id}) => 0;

class Example extends ConsumerWidget{
    Example({super.key, required this.id});

    final int id;

    
    Widget build(context, ref){
        ref.watch(
            myFamilyProvider(id: id),
        );
    }
}
  • しかし今後はDependenciesアノテーションにてoverrideするFamilyProviderを引数なしで記述する事で、ProviderScope側で引数を定義するだけで良い
Widget build(context){
    return ProviderScope(
        overrides: [
            myFamilyProvider.overrideWithDefault(id:id)
        ],
        child: Example(),
    )
}
([myFamily])
class Example extends ConsumerWidget{
    Example({super.key});

    
    Widget build(context, ref){
        ref.watch(myFamilyProvider);
    }
}

5. if(mounted)の追加

  • Provider内でmountedのチェックが可能に

class Example extends _$Example{
    
    int build() => 0;

    Future<void> asyncMethod() async {
        await something();

        if(!mounted) return;
    }
}

6. テスト時の記述の簡素化

  • Providerのテストが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を初期化不要に

  • ref.listenをProviderの初期化無しに定義可能に
  • ref.listen内のweakパラメータに渡すbooleanで機能をオンオフ
  • weak: trueの場合、listenの時点で初期化されていなくても、後々変更があった際に検知する事が可能
  • ログイン状態の監視などに有効

int another(AnotherRef ref){
    ref.listen(exampleProvider, weak: true, (prev, next){
        ...
    });
}

8. Side-effectのサポート強化

  • POSTなどサーバーへの変更を伴う処理(side-effect)の状態に応じたUIの変更は以前から課題があったが、この度新しいミューテーションメソッドを追加
  • (side-effectの課題についてはこちら)
  • Notifierに@mutationアノテーションを付けたstaticメソッドを実装
  • 返り値として新しい状態値を返す形に記述

class TodoList extends _$TodoList{
    
    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) // 新しい状態値
            .map(Todo.fromJson)
            .toList();
}
  • UI側ではミューテーションメソッドが返すMutationStateオブジェクトを監視し、その状態値に応じたUI処理を記述
class SuperButton extends ConsumerWidget {
    
    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),
                ),
        };
    }
}
  • これにより下記のようにProviderをwatchすることで、UIの別の箇所でもミューテーションの状態値を監視する事ができ、複数のUIで同一の状態値に基づいた表示を行う事が可能
class SuperAppBar extends ConsumerWidget{
    
    Widget build(context, ref){
        final addTodo = ref.watch(todoListProvider.notifier);
    }
}

9. 自動リトライ

  • Provider内でExceptionをthrowされると自動でリトライが実行されるように

int example() {
    print(DateTime.now().seconds);
    throw Exception();
}
// 実行結果:1,2,4,8
  • カスタムのリトライロジックを追加したい場合はProviderに対し@Riverpod(retry: myRetry)で渡す事が可能
(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. オフライン・キャッシング

  • Providerの値をデバイスに直接キャッシュすることが可能になる
  • 別パッケージに定義されるローカルDB(ex. SharedPreference, SQLite, etc.)クラスをProviderScopeにバインディング
ProviderScope(
    offlineConnector: const SharedAppPreferenceAsJson(),
)
  • 必要なシリアライズメソッドをモデルクラスに追加(ex. fromJson, toJson, etc.)
class Product{
    factory Product.fromJson(Map<String, Object?> json){
        ...
    }
    Map<String, Object?> toJson() => ...
}
  • Provider側にオフライン時の保存先となるテーブル名をアノテーションに定義
(offline: '<name-table>')
Future<List<Product>> products(ProductRef ref){
    final response = http.get('my-api/products');

    return (response as List).map(Product.fromJson).toList();
}
  • ローカルDB上のテーブル定義に変更が生じる=モデル定義に変更が発生する場合はdestroyKeyオプションを追加することで、新しいスキーマ定義のデータをキャッシュさせる事が可能になる
(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();
}

FlutterNinjas Tokyoについて🥷


FlutterNinjas Tokyoは日本初のFlutterに特化したグローバルカンファレンスです。今年が初開催となり、プラチナスポンサーにCode Magic、ゴールドスポンサーにMoneyForwardを迎え、登壇者、来場者共に国内外より130名以上のFlutter開発者が参加しました。全セッションおよびワークショップは英語で行われ、今までにない刺激を日本のFlutter開発者に与える事をミッションとしています。

https://twitter.com/FlutterNinjas
https://www.linkedin.com/company/flutterninjas-tokyo/

本記事のようにどこよりも早く最新情報を入手出来たり、憧れの開発者、国内外のFlutter開発者と交流したり、素敵なコミュニティを作り上げていきたいと思っています。

イベントのセッション動画も配信予定&来年も開催予定です。是非ともフォローお願いします🙌🥳

Discussion

heyhey1028heyhey1028

申し訳ありません。自分のミスで「2. Generic Providers」について一点誤っていた箇所があるので修正させて頂きました🙇🙇🙇🙇

code generationでは対応していない❌ → code generationでのみ対応⭕️