はじめに
Flutter3.10が正式リリースとなりました。
https://docs.flutter.dev/release/release-notes/release-notes-3.10.0
Flutter3.10の中で、Dart3が実行されるようになりました!!
今回Dart3で新しく追加されたRecord型を使用して、FlutterでCustomHookの導入をしていきます。
flutter でなんでCustom Hookを書くの?
Flutterのエンジニアの方はCustom Hookを聞いたことありますでしょうか?
flutter hookは聞いたことがあり、StatefullWidgetをStatelessWidgetに書き直して
簡略化することができるといったイメージしかないのではないでしょうか?
本当はhookを使いこなせば、ChangeNotifierやStateNotifierを置き換えることができ
UIのロジック部分をhookに全て任せることができます。
※現在はChangeNotifierやStateNotifierなどを使用されている方が多い印象です。
hookをよく使うReactなどでは、UIで使用するロジック部分はCustom Hookで記載することが一般的です。
Reactに多少なりとも影響を受けているFlutterなので、今後Custom Hookで記載することが一般的になるかもしれません。
書き方など慣れておいて損はないと思います。ということでflutterでCustom Hookを作っていきましょう。
なんでRecord型を使用してCustom Hookを書くの?
今までFlutterでCustom Hookを書く場合は以下のような書き方をしていました。
import 'package:flutter_hooks/flutter_hooks.dart';
UseCounter useCounter() {
final count = useState(0);
void increment() {
count.value++;
}
return UseCounter(
increment: increment,
count: count.value,
);
}
class UseCounter {
final void Function() increment;
final int count;
UseCounter({
required this.increment,
required this.count,
});
}
こちら、Reactの実装者なら違和感があると思うのですが、
戻り値について、わざわざクラスを作成してあげて、そのクラスを返却するように記載しなければいけませんでした。
このUseCounterクラスは基本的には無意味です。ただuseCounterの戻り値を定義するためだけのものです。
この欠点をDart3のRecord型の導入により克服することができるようになりました。
import 'package:flutter_hooks/flutter_hooks.dart';
final useCounter = () {
final count = useState(0);
void increment() {
count.value++;
}
return (
increment: increment,
count: count.value,
);
};
上記のコードのように、戻り値のクラスを作成することなく、Record型を定義してあげることで
Custom Hookを簡単に作成できるようになりました。
かなり簡潔に書けたことがわかると思います。
Custom Hookのテスト
次にテストを書いていきたいと思います。
テストを書いていくためには、先ほど書いたコードに少し型定義をしてあげないといけません。
(ここを省略する方法あればコメントで教えていただけると嬉しいです。)
import 'package:flutter_hooks/flutter_hooks.dart';
typedef UseCounterRecord = ({
void Function() increment,
int count,
});
typedef UseCounter = UseCounterRecord Function();
final UseCounter useCounter = () {
final count = useState(0);
void increment() {
count.value++;
}
return (
increment: increment,
count: count.value,
);
};
実装には手をつけていないですが、変更した点は、
UseCounterRecord
とUseCounter
の方を定義しました。
UseCounterRecord
についてはテストの際に使用します。
次にテストを書いていきます。
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks_test/flutter_hooks_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:<path>use_counter.dart';
void main() {
testWidgets('use counter ...', (tester) async {
late UseCounterRecord hook;
var buildCount = 0;
await tester.pumpWidget(MaterialApp(
home: HookBuilder(builder: (context) {
buildCount++;
hook = useCounter();
return Container();
}),
));
expect(buildCount, 1);
expect(hook.count, 0);
// incrementの実行
hook.increment();
// 描画が終わるまで待機
await tester.pumpAndSettle();
expect(buildCount, 2);
expect(hook.count, 1);
});
}
上記のテストコードのキモは、HookBuilder内でbuildを行い
その中でuseCounter
のhookを取得します。
hookをlateで宣言してtest全体で使用できるようにしています。この際にdynamicで定義しても良いのですが、UseCounterRecordを定義しておくことで後のテストも書きやすくなります。
(関数から戻り値の推定方法が知れれば、わざわざ戻り値を別定義とせずに使えるはずなのでここをどうにかしたい。)
あとは、hookの関数を実行して、テストを行うことができるという感じです。
mockを使いたい場合は、useCounter
の引数にmockインスタンスを渡すことで使用できたりします。
Custom Hookを使用するメリット
Build Contextを使用することができる
hooksはWidget ツリー内に存在するため、Build Contextを使用することができます。(useContextが使用可能)
ChangeNotifierなどでは、BuildContextを呼び出せずに、alertの表示やtoastの表示など困ったことないですか?
正直なところ私は、toastの表示などはロジックになるかなと思うので、Cusotm Hook内でScaffoldMessenger.of(context).showSnackBar();
などが
実行できることはメリットかなと思います。
Custom Hookのデメリット
- riverpodでCacheできない?
- riverpodを使用するとChangeNotifierなどは一度作成するとCacheされるので次読み込む際に、同じ状態を担保することができる。
- 基本的にChangeNotifierなどはautoDisposeなどで使用しているWidgetが破棄されると一緒に破棄されると思うのでデメリットにはならない?????
- riverpodを使用するとChangeNotifierなどは一度作成するとCacheされるので次読み込む際に、同じ状態を担保することができる。
- 関数型なのでMock化できない
- custom hookに渡す引数をmock化することは可能だが、custom hook自体をmock化し、Stubを用意することなどはできない。
- 私ができないと思っているだけで本当はやり方ある??
- custom hookをmockにする需要はある?? ChangeNotifierをMock化してテストしたい要望がある時ってどんな時だろう。。