flutter_hooksでカスタムhook作ってみる
reactのエンジニアがflutter_hooksを使用してカスタムhookを作成するまでの備忘録
flutter_hooksの説明
flutter_hooksはReact hooksにインスパイアされ、useStateやuseEffectなどの関数ベースの機能を
flutterで使用するためのライブラリです。
flutter_hooksを使用すると、Stateを持つ必要があった際に、StatefulWidgetなどを使用せずに済むため
コードを簡潔に記述することができます。
flutter_hooksを使用すると以下のように変換できます。いつものカウンターアプリです。
before
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
after
class MyHomePage extends HookWidget {
final String title;
const MyHomePage({super.key, required this.title});
@override
Widget build(BuildContext context) {
final count = useState(0);
void incrementCounter() {
count.value++;
}
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${count.value}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
かなり簡潔になったことがわかってもらえると思います。
カスタムhookの作成
Reactでhookを使用している方ならcustom Hookをよく使用しているのかなと思います。
主な用途としては、UIとロジックの分離をおこなったり、ロジックの共通化を行うために使用したりすると思います。
flutterではこんな感じに使用します。
先ほどのCounterのカスタムhookを作成します。
import 'package:flutter_hooks/flutter_hooks.dart';
UseCounter useCounter() {
final counter = useState(0);
void increment() {
counter.value++;
}
return UseCounter(count: counter.value, increment: increment);
}
class UseCounter {
final int count;
final void Function() increment;
UseCounter({required this.count, required this.increment});
}
useCounterを使います。
class HomePage extends HookWidget {
final String title;
const HomePage({super.key, required this.title});
@override
Widget build(BuildContext context) {
final state = useCounter();
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${state.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: state.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
ついでにuseEffectも使用してみる
useEffectの機能も追加して以下の10カウントずつ通知してくれるcounterを作成します。
useEffectを使用して、countを監視し、countが10になった時にダイアログを出します。
class HomePage extends HookWidget {
final String title;
const HomePage({super.key, required this.title});
@override
Widget build(BuildContext context) {
final state = useCounter();
useEffect(() {
if (state.count % 10 == 0) {
Future.microtask(() => showDialog(
barrierDismissible: false,
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text("${state.count}になりました"),
actions: [
CupertinoDialogAction(
child: const Text('close'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
)));
}
}, [state.count]);
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${state.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: state.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
useEffectもロジックの部分なので、これもカスタムhookに入れたいなーと思ったので
以下のような感じでuseHomePageというカスタムhookを作成しました。
useCounterは先ほど上で作成したものです。
また、dialogを出すために、contextを使用するので、hookで用意されているuseContextを使用します。
UseHomePage useHomePage() {
final counter = useCounter();
final context = useContext();
useEffect(() {
if (counter.count % 10 == 0) {
Future.microtask(() => showDialog(
barrierDismissible: false,
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text("${counter.count}になりました"),
actions: [
CupertinoDialogAction(
child: const Text('close'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
)));
}
}, [counter.count]);
return UseHomePage(counter: counter);
}
class UseHomePage {
final UseCounter counter;
UseHomePage({required this.counter});
}
useHomePageを使用します。
useEffectがなくなって、ロジックの部分が1行に圧縮されたことがわかると思います。
class HomePage extends HookWidget {
final String title;
const HomePage({super.key, required this.title});
@override
Widget build(BuildContext context) {
final state = useHomePage();
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${state.counter.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: state.counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}