FlutterのデフォルトアプリをRiverpodを使った形にしたサンプルコード
概要
Flutterのプロジェクト生成時の状態のアプリ(カウンターアプリ)を、Riverpodを使った形にしたサンプルコードです。
全サンプルコード
以下のPRで全サンプルコードの全体像を参照できます。
作成した各ファイルの概要
- counter_page.dart
- providerを使ってカウンターの状態を扱い、templateに渡す値を取得したりイベントの実装をして、templateを呼び出す
- counter_template.dart
- カウンターページに表示させるWidgetを定義したテンプレート
- counter_notifier.dart
- StateNotifierを継承してカウンタの状態を管理するCounterNotifierクラスと、そのproviderを定義
- counter_page_test.dart
- カウンターページに表示されているものとボタンイベントのテストコード
各ファイルのコードとメモ
main.dart
void main() {
// Riverpodのproviderを利用できるようにするため、ProviderScopeでルートのwidgetをラップ
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CounterPage(),
);
}
}
counter_page.dart
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watchでカウンターの状態を取得
final counter = ref.watch(counterNotifierProvider);
return CounterTemplate(
title: 'Counter with Riverpod',
message: 'You have pushed the button this many times:',
counter: counter,
onPressedIncrementButton: (() => _incrementCounter(ref)),
);
}
void _incrementCounter(WidgetRef ref) {
// IncrementButtonが押された時に、ref.readでcounterNotifierを取得し、カウンターの状態を更新
final counterNotifier = ref.read(counterNotifierProvider.notifier);
counterNotifier.increment();
}
}
counter_template.dart
class CounterTemplate extends StatelessWidget {
// ページに表示させる値やイベントはテンプレート内では定義せずパラメータとして受け取る
final String title;
final String message;
final int counter;
final void Function() onPressedIncrementButton;
const CounterTemplate({
super.key,
required this.title,
required this.message,
required this.counter,
required this.onPressedIncrementButton,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(message),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: onPressedIncrementButton,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
counter_notifier
class CounterNotifier extends StateNotifier<int> {
// `super(0)`でカウンターの初期値を0に指定
CounterNotifier() : super(0);
// 他のクラスから直接stateを更新することもできるが、メソッドを用意して何をしているかより明確にしている
void increment() => state++;
}
// notifierのproviderは同じファイル内で定義したほうが分かりやすそうに思えたので、ここで定義
final counterNotifierProvider =
StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
counter_page_test.dart
void main() {
// 表示されているwidgetのテスト
testWidgets('''
Counter page has a title, message, and increment button
''', (tester) async {
await tester.pumpWidget(_buildTestWidget());
const expectTitle = 'Counter with Riverpod';
const expectMessage = 'You have pushed the button this many times:';
expect(find.text(expectTitle), findsOneWidget);
expect(find.text(expectMessage), findsOneWidget);
expect(find.byType(FloatingActionButton), findsOneWidget);
});
// カウンタの初期値のテスト
testWidgets('''
Counter initial value is 0
''', (tester) async {
await tester.pumpWidget(_buildTestWidget());
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
// ボタンが押された時にカウントアップされることのテスト
testWidgets('''
When the increment button is pressed,
Then the counter value is incremented
''', (tester) async {
await tester.pumpWidget(_buildTestWidget());
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsOneWidget);
});
}
Widget _buildTestWidget() {
// テストでもriverpodを使うので、ProviderScopeでラップ
return const ProviderScope(
// CounterPageはScaffoldを使っているので、MaterialAppでラップ (詳細は下記)
child: MaterialApp(
home: CounterPage(),
),
);
}
Scaffoldのwidgetをテストする際の注意点
Scaffoldのwidgetをtester.pumpWidget
に指定する際はMaterialAppでラップする必要があります。指定しないと以下のようなエラーが発生します。
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building Scaffold(dirty, state: ScaffoldState#e0693(tickers:
tracking 2 tickers)):
No MediaQuery widget ancestor found.
Scaffold widgets require a MediaQuery widget ancestor.
...
This can happen because you have not added a WidgetsApp, CupertinoApp, or MaterialApp widget (those
widgets introduce a MediaQuery), or it can happen if the context you use comes from a widget above those widgets.
要約すると、Scaffold widgets のような MediaQuery widget には先祖が必要で、WidgetsApp, CupertinoApp, or MaterialApp widget がないと発生するエラー
Discussion