riverpodのhooksってなにか?
ReackHooksを真似て作ったらしい?
開発環境
- flutter2.8.0
- hooks_riverpod: ^1.0.3
hooks_riverpod と flutter_riverpod の書き方の違い
どちらのRiverpodパッケージをインストールするかによって、Widgetからの利用時に書き方の違いがあります。
グローバル変数としてのProvider定義の書き方は共通です。
flutter_riverpodと同じだよ!
final appNameProvider = Provider((ref) => 'Special App!');
hooks_riverpodの書き方の例
pub.devのリンク
https://pub.dev/packages/hooks_riverpod
main.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
runApp(
/// [MyApp] is wrapped in a [ProviderScope].
/// This widget is where the state of most of our providers will be stored.
/// This replaces `MultiProvider` if you've used `provider` before.
const ProviderScope(
child: MyApp(),
),
);
}
/// A provider that creates and listen to a [StateNotifier].
///
/// Providers are declared as global variables.
/// This does not hinder testability, as the state of a provider is instead
/// stored inside a [ProviderScope].
final counterProvider = StateNotifierProvider<Counter, int>((_) => Counter());
/// A simple [StateNotifier] that implements a counter.
///
/// It doesn't have to be a [StateNotifier], and could be anything else such as:
/// - [ChangeNotifier], with [ChangeNotifierProvider]
/// - [Stream], with [StreamProvider]
/// ...
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
// StatelessWidgetが、HookConsumerWidgetに変更されている。
class MyHomePage extends HookConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod counter example'),
),
body: Center(
child: Text(
'$count',
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
flutter_riverpodの書き方の例
pub.devのリンク
https://pub.dev/packages/flutter_riverpod
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A Counter example implemented with riverpod
void main() {
runApp(
// Adding ProviderScope enables Riverpod for the entire project
const ProviderScope(child: MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
/// Providers are declared globally and specifies how to create a state
final counterProvider = StateProvider((ref) => 0);
// StatelessWidgetが、ConsumerWidgetに変更されている。
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
// Consumer is a widget that allows you reading providers.
// You could also use the hook "ref.watch(" if you uses flutter_hooks
child: Consumer(builder: (context, ref, _) {
final count = ref.watch(counterProvider.state).state;
return Text('$count');
}),
),
floatingActionButton: FloatingActionButton(
// The read method is an utility to read a provider without listening to it
onPressed: () => ref.read(counterProvider.state).state++,
child: const Icon(Icons.add),
),
);
}
}
今回作ってみた学習サンプル
ディレクトリ構造
lib
├── components
│ └── importer.dart
├── main.dart
├── models
│ └── counter_view_model.dart
└── views
└── home_page.dart
今回は、exportという面白いDartの機能を使ってみた。import文を使って各ページで、同じpackageを毎回読み込むのが面倒臭い!!!
で、1つのファイルにexport文を使ってまとめる方法を思い出したので使ってみた。
components/import.dart
export 'package:flutter/material.dart';
export 'package:hooks_riverpod/hooks_riverpod.dart';
export 'package:hook_sample_app/models/counter_view_model.dart';
export 'package:hook_sample_app/views/home_page.dart';
models/counter_view_model.dart
import 'package:hook_sample_app/components/importer.dart';
//①Riverpodのみ設定
final countViewModel = ChangeNotifierProvider((_) => CountViewModel());
//CountViewModelの中身は一緒
class CountViewModel extends ChangeNotifier {
final String title = 'hooks_riverpod';
int _counter = 0;
get counter => _counter;
void incrementCounter() {
_counter++;
notifyListeners();
}
}
views/home_page.dart
import 'package:hook_sample_app/components/importer.dart';
// StatelessWidgetをHookConsumerWidgetに変更
class MyHomePage extends HookConsumerWidget {
MyHomePage({Key? key}) : super(key: key);
@override
// 第2引数にWidgetRef refを追加
Widget build(BuildContext context, WidgetRef ref) {
//④countViewModelを参照することを定義、viewModelでViewModelを参照する
final viewModel = ref.watch(countViewModel);
return Scaffold(
appBar: AppBar(
title: Text('${viewModel.title}'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${viewModel.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: viewModel.incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
main.dart
import 'components/importer.dart';
void main() {
runApp(
// MyAppをProviderScopeで囲む
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hooks riverpod!',
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: MyHomePage(),
);
}
}
ご覧いただくと各ページで、import文が今のところ1つしかない!
今まで作ってきたアプリより見た目がスッキリして、補修もしやすくなった気がした...
もっと速く使うべきだった!!!
おまけ
カウンターばっかりだと面白くないから、ボタンを押したら文字が変わる機能を追加してみた!
models/counter_view_model.dart
import 'package:hook_sample_app/components/importer.dart';
// 定数の中にクラスを格納する
final countViewModel = ChangeNotifierProvider((_) => CountViewModel());
//CountViewModelの中身は一緒
class CountViewModel extends ChangeNotifier {
final String title = 'hooks_riverpod!';
int _counter = 0;
get counter => _counter;
String _privateText1 = 'これは多分ゲッターだよ???';
get privateText => _privateText1;
String _privateText2 = 'これはプライベートなプロパティだよ!';
get privateText2 => _privateText2;
String _greeting = 'あいさつ?';
get greeting => _greeting;
void incrementCounter() {
_counter++;
notifyListeners();
}
void sayMethod() {
// ボタンを押すと変数に文字を代入する
_greeting = 'Hello Riverpos!!!';
notifyListeners();
}
}
views/home_page.dart
import 'package:hook_sample_app/components/importer.dart';
// StatelessWidgetをHookConsumerWidgetに変更
class MyHomePage extends HookConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
// 第2引数にWidgetRef refを追加
Widget build(BuildContext context, WidgetRef ref) {
//④countViewModelを参照することを定義、viewModelでViewModelを参照する
final viewModel = ref.watch(countViewModel);
return Scaffold(
appBar: AppBar(
title: Text('${viewModel.title}'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('${viewModel.privateText}'),
const SizedBox(height: 10),
Text('${viewModel.privateText2}'),
const SizedBox(height: 10),
Text('${viewModel.greeting}'),
const SizedBox(height: 10),
Text(
'${viewModel.counter}',
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
ElevatedButton(
onPressed: viewModel.sayMethod,
child: const Text('あいさつをする')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: viewModel.incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}