Pull to refresh
Riverpod は宣言的な性質のおかげで自然に pull-to-refresh をサポートしています。
一般的に pull-to-refresh は複数の問題を解決する必要があるため複雑になります:
- ページに初めて入った際はスピナーを表示したい。
リフレッシュ中はリフレッシュインジケーターを表示したい。
リフレッシュインジケーターとスピナーの両方を表示してはいけません。 - リフレッシュが保留中の間、以前のデータ/エラーを表示したい。
- リフレッシュが行われている間はリフレッシュインジケーターを表示し続ける必要があります。
Riverpod を使用してこの問題を解決する方法を見ていきましょう。
ここでは、ユーザーにランダムなアクティビティを提案する簡単な例を作成します。
そして pull-to-refresh をトリガーすると新しい提案が表示されます:
アプリケーションのベースを作成する
pull-to-refresh を実装する前、まずリフレッシュする何かが必要です。
Bored APIを使ってユーザーにランダムなアクティビティを提案するシンプルなアプリケーションを作成します。
まず、Activity
クラスを定義します:
class Activity with _$Activity {
factory Activity({
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}
このクラスは堅安全な方法で提案されたアクティビティを表現し、JSON エンコード/デコードを処理します。 Freezed/json_serializable を使用することは必須ではありませんが、推奨されます。
今、単一のアクティビティを取得するための HTTP GET リクエストを行うプロバイダを定義します:
Future<Activity> activity(Ref ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(Map.from(json));
}
この provider を使うことでランダムなアクティビティを表示できます。 今、ローディング/エラー状態を処理ぜず、アクティビティが利用可能な場合にのみ表示します:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: Center(
// アクティビティがあればそれを表示し、なければ待ちます。
child: Text(activity.valueOrNull?.activity ?? ''),
),
);
}
}
RefreshIndicator
の追加
シンプルなアプリケーションができたので、ここに RefreshIndicator
を追加します。
このウィジェットはユーザーが画面を下に引くとリフレッシュインジケーターを表示する公式のマテリアルウィジェットです。
RefreshIndicator
を使用するにはスクロール可能な表面が必要です。
しかし、これまでそういったものはありませんでした。
ListView
/GridView
/SingleChildScrollView
/などを使用することでこの問題を解決できます:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () async => print('refresh'),
child: ListView(
children: [
Text(activity.valueOrNull?.activity ?? ''),
],
),
),
);
}
}
ユーザーは画面を引き下げることができますが、データはまだリフレッシュされていません。
リフレッシュロジックの追加
ユーザーが画面を引き下げると、RefreshIndicator
は onRefresh
コールバックを呼び出します。
このコールバックを使用してデータをリフレッシュできます。
ここでは ref.refresh
を使用して選択した provider をリフレッシュできます。
注意: onRefresh
は Future
を返すことが期待されており、
リフレッシュが完了したときにその future が完了することが重要です。
そのような future を取得するには、provider の.future
プロパティを読み取ることができます。
これにより、provider が解決されたときに完了する future が返されます。
RefreshIndicator
を次のように更新できます:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
// "activityProvider.future"をし、その結果を返すことで、
// 新しいアクティビティがフェッチされるまでリフレッシュインジケーターを表示し続けます。
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
Text(activity.valueOrNull?.activity ?? ''),
],
),
),
);
}
}
初期読み込み中にスピナーを表示し、エラーを処理する。
現時点では、UI はエラー/ローディング状態を処理していません。
代わりにデータがロード/リフレッシュされると、データが魔法のように表示されます。
これらを優雅に処理するため変更してみましょう。
二つのケースがあります:
- 初期ロード中に、フルスクリーンのスピナーを表示したい。
- リフレッシュ中にリフレッシュインジケーターと前のデータ/エラーを表示したい。
幸いなことに、Riverpod で非同期 provider をリッスンするとき、Riverpod は必要なすべてを提供する AsyncValue
を提供します。
この AsyncValue
は次のように Dart 3.0 のパターンマッチングと組み合わせて使用できます:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
// いくつかのデータが利用可能な場合、それを表示します。
// refresh中もデータは利用可能であることに注意してください。
AsyncValue<Activity>(:final valueOrNull?) => Text(valueOrNull.activity),
// エラーがある場合、エラーメッセージを表示します。
AsyncValue(:final error?) => Text('Error: $error'),
// データ/エラーがない場合、ローディング状態になります。
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
valueOrNull
を使用して、現在のようにエラー/ローディング状態でvalue
を使用します。
Riverpod 3.0 では、value
がvalueOrNull
のように動作するように変更される予定です。
今はvalueOrNull
を使用し続けましょう。
パターンマッチングで:final valueOrNull?
構文を使用していることに注意してください。
この構文は、activityProvider が null 不許容の Activity
を返すためにのみ使用できます。
データがnull
である可能性がある場合、代わりにAsyncValue(hasData: true, :final valueOrNull)
を使用できます。
これにより、データがnull
であるケースを正しく処理できますが、少し余分な文字が必要になります。
まとめ: フルアプリケーション
これまでの内容をすべて統合したソースはこちらです:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'codegen.g.dart';
part 'codegen.freezed.dart';
void main() => runApp(ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(home: ActivityView());
}
}
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
AsyncValue<Activity>(:final valueOrNull?) =>
Text(valueOrNull.activity),
AsyncValue(:final error?) => Text('Error: $error'),
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
Future<Activity> activity(Ref ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(Map.from(json));
}
class Activity with _$Activity {
factory Activity({
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}