riverpod+hooks+HiveでFlutterアプリを作ったときメモ
先日、ポケモンWordleみたいなものをFlutterで作成してワードクイズ アプリとしてオープンソースにしました。
初めてriverpodに触ったため、色々と壁にぶち当たりました。せっかくなのでメモがてら残しておきます。
つまづきながらなので、真の解決策は別にあるかもしれないですが、その点はご了承ください。
riverpod+hooksの良さがわかった
親子関係を気にしたりする必要がないので使いやすかったです。
特にhooksでドロップダウンにuseState()をぱっと使えたりと楽でした。
// ここで定義
final dropdownValue = useState<QuizRange>(defaultQuizRange);
// こんな感じで使う
return DropdownButton<QuizRange>(
value: dropdownValue.value,
items: [
...quizRangeList.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.displayName ?? ''),
),
),
],
onChanged: (value) {
dropdownValue.value = value!;
},
);
useFutureを使ったときに何回もbuildが呼ばれる
useMemoizedと一緒に使いましょう。
final randomPickFuture = useMemoized<Future<Monster?>>(
() => ref.watch(monsterPickerProvider).pick(),
);
final snapshot = useFuture(randomPickFuture);
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
ref.watch(hoge)のテスト
Widgetで、ref.watch(hoge)
されている場合、mockitoで出力したMockクラスを使おうとすると、addListenerのスタブがないよということでエラーが発生しました。まさにこの事象です。
Widget->providerまでテストする場合は問題にはなりませんが、Widget単体でテストしたい場合は困ります。
その場合は、これで解決できました。
実際にはこんな感じ。(Mockでは名前が衝突するのでFakeにしています)
class FakeExampleStateNotifier extends StateNotifier<bool> implements ExampleStateNotifier {
FakeExampleStateNotifier(bool state) : super(state);
}
こんな感じで書く
ProvideScope(
overrides:[
exampleStateNotifierProvider.overrideWithValue(FakeExampleStateNotifier(false)),
],
)
参考:fake_quiz_info_notifier.dart
showDialog内でWidgetRef使うときはProviderScopeが必要
ダイアログ表示時の引数にWidgetRefを渡していましたが、たまにエラーが吐かれていました。
このようにProviderScopeで囲むと大丈夫になるようです。
とはいえ今回は、ダイアログで「はい」か「いいえ」のどちらがタップされたかが分かればいいので、呼び出し元の方で処理するようにしました。
そのため、showDialogはbool値だけを返すようにし、そもそもWidgetRefを渡さないように修正しました。
2.0.0では少し触れられている?
libフォルダ全体のカバレッジが出ない
どうやら flutter test --coverage
では、テストを書いたファイルしかカバレッジ出力の対象にならないみたいで、全体のカバレッジが計測できません。
これを.shにとりこみました。
参考:coverage.sh
一部のLine Coverageが無視できない
Widgetのconstとなっているコンストラクタで super(key:key)
の呼び出し部分がCoverageとしてはマークされないようです。
そのため、このような形で coverage:ignore:line
をつけることにしました。
class StatisticsButton extends ConsumerWidget {
const StatisticsButton({
Key? key,
}) : super(key: key); // coverage:ignore-line
...
}
しかし一部のファイルはこの状態では無視できませんでした。結局原因は不明だったのですが、1行に収めると解決しました。
class WordKeyboard extends ConsumerStatefulWidget {
const WordKeyboard({Key? key}) : super(key: key); // coverage:ignore-line
...
}
Hiveはどうやってテストする?
これを使うといいと思います。
今回はこれを見つける前に同じようなことをしていたので、独自に実装しました。
/// テスト用のパス
final _dirBasePath = '${Directory.current.path}/.dart_tool/test';
/// Hiveのテスト用の初期化を行います。
void setUpHive() {
// タイミングによって衝突が発生するのでランダムな名称のフォルダとする
final dirForTest = Directory('$_dirBasePath/${Random().nextInt(1000000000)}');
// ディレクトリを一度削除して作成
if (dirForTest.existsSync()) {
dirForTest.deleteSync(recursive: true);
}
dirForTest.createSync(recursive: true);
Hive.init(dirForTest.path);
}
/// Hiveのテスト用の削除を行います。
Future<void> tearDownHive() async {
await Hive.deleteFromDisk();
}
Timerで更新するWidgetは分割する
ワードクイズでは、数百ミリ秒ごとに残り時間を更新する画面があります。
最初は画面全体buildメソッドの中に、時間表示Widgetを入れていましたが、時間更新のたびに頻繁にstateが更新されていました。そこでWidgetを分割して、画面全体が更新されないようにしました。
基本的な失敗なのですが、私自身への自戒を込めて残しておきます。
class _ClockText extends ConsumerWidget {
const _ClockText({
Key? key,
}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
// remainingTimeProviderはTimerを使って頻繁に更新しています
final remainingTime = ref.watch(remainingTimeProvider);
return Text(
remainingTime,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
}
}
share_plusを使ってiOSでビルドしようとしたら、失敗する
ワードクイズにはシェア機能があります。
share_plusというパッケージを使っているのですが、iOSでビルドエラーが発生しました。
問題はこれで解決しました。
sudo arch -x86_64 gem install ffi
arch -x86_64 pod install
まとめ
初めてのことで困惑することが多かったですが、粘ればなんとか解決策は見つかりました。
テストに関しては難しいところも多かったですが、なんとか形になりました。
Discussion