Closed19

FlutterのRiverpodを使ってみる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

HelloWorld

準備

flutter pub add flutter_riverpod
flutter pub get

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final helloWorldProvider = Provider((_) => 'Hello world');

void main() {
  runApp(const ProviderScope(
    child: MyApp(),
  ));
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final String value = ref.watch(helloWorldProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(value),
        ),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロバイダとは

https://riverpod.dev/ja/docs/concepts/providers

プロバイダはあるステート(状態)をラップするためのオブジェクトであり、その監視を可能にしてくれます。

プロバイダは完全にイミュータブル(不変)であり、関数をグローバルで宣言することと違いはありません。

refを使って他のプロバイダを利用したり、プロバイダのステートが破棄される際のコールバック関数を登録したりすることができます。

プロバイダを利用するには、Flutter アプリのルート(root)に ProviderScope を置く必要があります。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロバイダの利用方法

https://riverpod.dev/ja/docs/concepts/reading

プロバイダはすべて ref オブジェクトを引数として受け取ります。

final provider = Provider((ref) {
  // `ref` を通じて他のプロバイダを利用する
  final repository = ref.watch(repositoryProvider);

  return SomeValue(repository);
})

StatelessWidget の代わりに ConsumerWidget を継承する

StatefulWidget+State の代わりに ConsumerStatefulWidget+ConsumerState を継承する

ref は ConsumerState のプロパティです。

ref の使い道は主に3通りあります。

  • ref.watch: 値を取得する+変化を監視する(変化したらウィジェットやプロバイダを更新する)
  • ref.listen: 変化を監視する+コールバック関数を登録する(変化したら呼び出す)
  • ref.read: 値を取得する

watch メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。これらの場合は代わりに ref.read を使用してください。

上記はlistenメソッドについても同様

【重要】 ref.read は build メソッドの中で使わない

下記のコードではref.watchの代わりにref.readを使っても効果は同等とのこと

final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
  StateController<int> counter = ref.watch(counterProvider.notifier);
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}

なぜなのかは理解できない、もう少しドキュメントを読み進める必要がある

StreamProviderは下記3つの値の取得方法がある

  • provider: 現在のステートを取得する
  • provider.stream: Streamを取得する
  • provider.future: Futureを取得する

コード例は下記の通り(引用)

Widget build(BuildContext context, WidgetRef ref) {
  AsyncValue<User> user = ref.watch(userProvider);

  return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
  );
}
Widget build(BuildContext context, WidgetRef ref) {
  Stream<User> user = ref.watch(userProvider.stream);
}
Widget build(BuildContext context, WidgetRef ref) {
  Future<User> user = ref.watch(userProvider.future);
}

つまり select を使って監視対象にするプロパティを返す関数を指定することができるのです。

Widget build(BuildContext context, WidgetRef ref) {
  String name = ref.watch(userProvider.select((user) => user.name));
  return Text(name);
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロバイダのステートを組み合わせる

プロバイダのコールバック関数に渡される ref オブジェクトの watch メソッドを使用してください。

final cityProvider = Provider((ref) => 'London');
final weatherProvider = FutureProvider((ref) async {
  // `ref.watch` により他のプロバイダの値を取得・監視します。
  // 利用するプロバイダ(ここでは cityProvider)を引数として渡します。
  final city = ref.watch(cityProvider);

  // 最後に `cityProvider` の値をもとに行った計算結果を返します。
  return fetchWeather(city: city);
});

依存先のプロバイダの値が変わったら、その依存元のプロバイダはどうなりますか?

値の取得に watch を使っていれば、Riverpod は値の変化を検出して 自動的に 依存元のプロバイダの値を再評価してくれます。

依存するプロバイダの値が変わると自動で値が再評価されるプロバイダは Provider だけではありません。 すべてのプロバイダがこの性質を持ちます。

プロバイダ内で別のプロバイダの値を監視せずに、取得だけする方法はないですか?

このような場合は Repository オブジェクトに ref.read を渡してください。 そうすることで Repository オブジェクトは自身のタイミングで、別のプロバイダからトークンを取得できるようになります。

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
  Repository(this.read);

  /// `ref.read` 関数
  final Reader read;

  Future<Catalog> fetchCatalog() async {
    String token = read(userTokenProvider);

    final response = await dio.get('/path', queryParameters: {
      'token': token,
    });

    return Catalog.fromJson(response.data);
  }
}

read をプロパティに持つオブジェクトのテスト方法を教えてください。

これには ProviderContainer クラスを利用してください。

final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog', () async {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  Repository repository = container.read(repositoryProvider);

  await expectLater(
    repository.fetchCatalog(),
    completion(Catalog()),
  );
});

プロバイダが頻繁に更新されます。どうしたらいいですか?

select を利用して必要なプロパティのみを監視

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロバイダ修飾子 .family

.family 修飾子の目的は「外部のパラメータをもとに一意のプロバイダを作成すること」です。

例えば family と FutureProvider を組み合わせることで、メッセージ ID に紐づく Message を取得することが可能です。

final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

次のように messagesFamily に引数を渡してください。

Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}

引数として渡すオブジェクトの等価性

プリミティブ型(bool/int/double/String)か、定数(プロバイダ)もしくは == と hashCode をオーバーライドしたイミュータブル(不変)オブジェクトのいずれかが理想的です。

【重要】 オブジェクトが一定ではない場合は autoDispose 修飾子との併用が望ましい

こうしたメモリリークは .family と .autoDispose 修飾子を併用することで避けることができます。

family プロバイダに複数のパラメータを渡す

例えば以下のオブジェクトを渡すことでプロバイダに複数のパラメータを間接的に渡すことができます。

  • tuple
  • freezed
  • built_value
  • equitable

https://pub.dev/packages/freezed

https://pub.dev/packages/equatable

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロバイダ修飾子 .autoDispose

プロバイダ作成時に次のように .autoDispose 修飾子を付け加えてください。

final userProvider = StreamProvider.autoDispose<User>((ref) {

});

これで userProvider が参照されなくなった際に、ステートが自動的に破棄されるようになります。

プロバイダに .autoDispose 修飾子を付けると、ref オブジェクトに keepAlive というメソッドが追加されます。keepAlive メソッドを実行することで、プロバイダが参照されなくなった際にもステートを維持するよう Riverpod に伝えることができます。

使用例: プロバイダが参照されなくなったタイミングで HTTP リクエストをキャンセルする

final myProvider = FutureProvider.autoDispose((ref) async {
  // http リクエストのキャンセルを実行するための package:dio のオブジェクト
  final cancelToken = CancelToken();
  // プロバイダのステートが破棄されたら http リクエストをキャンセル
  ref.onDispose(() => cancelToken.cancel());

  // データを取得しつつキャンセル用の `cancelToken` を渡す
  final response = await dio.get('path', cancelToken: cancelToken);
  // リクエストが成功したらステートを維持する
  ref.keepAlive();
  return response;
});

https://pub.dev/documentation/dio/latest/dio/CancelToken-class.html

エラー:The argument type 'AutoDisposeProvider' can't be assigned to the parameter type 'AlwaysAliveProviderBase'

原因は .autoDispose 修飾子付きのプロバイダを、そうではないプロバイダ内で利用したためです。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ProviderObserver

ProviderObserver は ProviderContainer 内で起こる変化を監視します。ProviderObserver クラスを継承するクラスを定義し、使用したいメソッドをオーバーライドして使用してください。

didAddProvider はプロバイダが初期化されるたびに呼び出されます。公開される値は value パラメータで利用できます。

didDisposeProvider はプロバイダが破棄されるたびに呼び出されます。

didUpdateProvider はプロバイダが変更通知を送信するたびに呼び出されます。

ProviderObserverを使用するにはProviderScopeのobservers引数にリストとして渡す

ProviderScope(observers: [Logger()], child: const MyApp())
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ConsumerWidget

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class User {
  final int id;
  final String name;

  const User(this.id, this.name);
}

final userProvider = Provider<User>((_) => const User(1, "bob"));

void main() {
  runApp(const ProviderScope(
    child: MyApp(),
  ));
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(userProvider.select((value) => value.name));

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(name),
        ),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

StateProvider

https://riverpod.dev/ja/docs/providers/state_provider

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final countProvider = StateProvider<int>((ref) => 0);

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});
  
  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: Center(
        child: Text("Count: $count"),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          ref.read(countProvider.notifier).state = count + 1;
          // or
          // ref.read(countProvider.notifier).update((count) => count + 1);
        },
        icon: const Icon(Icons.plus_one),
        label: const Text('Increment'),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

FutreProvider

https://riverpod.dev/ja/docs/providers/future_provider

コード例

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final nameProvider = FutureProvider<String>((ref) async {
  await Future.delayed(const Duration(seconds: 3));
  return 'bob';
});

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(nameProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: Center(
        child: name.when(
          data: (name) => Text("Name: $name"),
          error: (err, stack) => Text("Error: $err"),
          loading: () => const CircularProgressIndicator(),
        ),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

StreamProvider

https://riverpod.dev/ja/docs/providers/stream_provider

コード例

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final countProvider = StreamProvider<int>((ref) async* {
  for (var i = 3; i >= 0; i -= 1) {
    yield i;
    await Future.delayed(const Duration(seconds: 1));
  }
});

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: Center(
        child: count.when(
          data: (data) => Text("Count: $data"),
          error: (err, stack) => Text("Error: $err"),
          loading: () => const CircularProgressIndicator(),
        ),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ProviderScope overrides

https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/ProviderScope-class.html

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final nameProvider = Provider((ref) => "Bob");

void main() {
  runApp(
    ProviderScope(
      overrides: [
        nameProvider.overrideWithValue("Alice"),
      ],
      child: const MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(nameProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: Center(
        child: Text("My name is $name."),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Combining Provider States

https://riverpod.dev/ja/docs/concepts/combining_providers

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final nameProvider = StateProvider((ref) => "Bob");

final greetProvider = Provider(((ref) {
  final name = ref.watch(nameProvider);
  return "My name is $name";
}));

final buttonLabelProvider = StateProvider((ref) {
  final name = ref.watch(nameProvider);

  if (name == "Alice") {
    return "Bob";
  } else {
    return "Alice";
  }
});

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final greet = ref.watch(greetProvider);
    final buttonLabel = ref.watch(buttonLabelProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: Center(
        child: Text(greet),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          ref.read(nameProvider.notifier).state = buttonLabel;
        },
        label: Text(buttonLabel),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

StateNotifierProvider

https://riverpod.dev/ja/docs/providers/state_notifier_provider

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Todo {
  final String title;
  const Todo(this.title);
}

class TodosNotifier extends StateNotifier<List<Todo>> {
  TodosNotifier() : super([]);

  void addTodo(Todo todo) {
    state = [...state, todo];
  }

  void removeTodo(Todo removedTodo) {
    state = [
      for (final todo in state)
        if (todo != removedTodo) todo
    ];
  }
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todosProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: ListView(
        children: [
          for (final todo in todos)
            ListTile(
              onTap: () {
                ref.read(todosProvider.notifier).removeTodo(todo);
              },
              title: Text(todo.title),
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // ignore: prefer_const_constructors
          ref.read(todosProvider.notifier).addTodo(Todo("Todo Title"));
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ChangeNotifierProvider

原則として使用は非推奨

https://riverpod.dev/ja/docs/providers/change_notifier_provider

コード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Todo {
  final String title;
  const Todo(this.title);
}

class TodosNotifier extends ChangeNotifier {
  final todos = <Todo>[];

  void addTodo(Todo todo) {
    todos.add(todo);
    notifyListeners();
  }

  void removeTodo(Todo todo) {
    todos.remove(todo);
    notifyListeners();
  }
}

final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
  return TodosNotifier();
});

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: MyApp(),
      ),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todosProvider).todos;

    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: ListView(
        children: [
          for (final todo in todos)
            ListTile(
              onTap: () {
                ref.read(todosProvider.notifier).removeTodo(todo);
              },
              title: Text(todo.title),
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // ignore: prefer_const_constructors
          ref.read(todosProvider.notifier).addTodo(Todo("Todo Title"));
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おまけ:Provider

https://github.com/rrousselGit/provider

準備

flutter create hello_provider
cd hello_provider
flutter pub add provider
flutter pub get

コード

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: const MaterialApp(
        home: MyApp(),
      )
    )
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    final count = context.watch<Counter>().count;
    return Scaffold(
      appBar: AppBar(title: const Text('AppBar Title')),
      body: Center(
        child: Text("Count: $count"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<Counter>().increment();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

実行結果

このスクラップは2023/01/10にクローズされました