はじめに
React 界隈は Hooks で盛り上がっています。しかしながら、Hooks は、クラス(オブジェクト)のメンバ変数やモジュールのローカル変数を、あたかも関数のローカル変数のように使う手法のひとつに過ぎない(と僕は考えます)ため、React や JavaScript 固有のものではありません。
そこで別の盛り上がりを見せている Flutter でも Hooks を使えないかと思いリサーチしていたところ、flutter_hooks というものを見つけましたのでご紹介したいと思います。
将来的には Flutter の標準機能として取り込まれそうですし、なにより Hooks や Flutter に関しての勉強にもなると思いますので React ユーザーや Flutter ユーザーなら読んでみて損はないと思います。
Flutter について
React ユーザーのために少しだけ Flutter を紹介しますと、現時点では、 JSX 記法が使えない React Native みたいなものです。(爆
僕の他の Qiita 記事である「ずぼらシリーズ」により、JSX 記法に頼らないよう鍛えあげられているずぼらシリーズ読者の皆様ならすぐに Flutter が好きになるはずです(まあ Flutter のベース言語である Dart が、JavaScript を置き換えるべく誕生したことなどからすれば、JSX 記法的なものが使えるようになるのも時間の問題でしょうから、ずぼらシリーズ読者以外の方も少しの我慢です)。
UI の構築以外の部分についてはそもそもベースとなる言語が違うため、比較はしづらいですが、前述のような誕生経緯からすれば、Dart の方が JavaScript よりも優れたものであって当然である気がします。
Dart から JavaScript にトランスパイルすることもできますので TypeScript を今から始めようと思っているような方は、思い切って Dart/Flutter を始めるのもありだと思います。
Flutter のサンプルコード
VS Code で、コマンドパレットから Flutter: New Project
を実行すれば、サンプルコードが完成します。作成されるソース(main.dart)は次のとおり。ボタンをクリックするとカウントアップされる、サンプルとしてはよくあるタイプのものです。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
親切なコメントをたくさんつけてくれていますので長めですが、オブジェクト指向型の言語による UI 構築は大体こんな感じなので経験豊富な方であればさほど驚かないのではないでしょうか。
これを flutter_hooks を使って書き直すと main.dart は次のようになります。比較しやすいようにコメントはあえて残してみました。
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends HookWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
final counter = useState(0);
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
counter.value.toString(),
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.value++,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
変更手順
- パッケージの追加
pubspec.yaml のdependencies:
以下にflutter_hooks:
を追加する。
例)
・・・
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
flutter_hooks:
dev_dependencies:
flutter_test:
sdk: flutter
・・・
VS Code では、pubspec.yaml を編集すると Get Package コマンドが自動的に実行されますので便利です。
-
import 文の追加
main.dart に(以下はすべて main.dart が変更の対象です)、
import 'package:flutter_hooks/flutter_hooks.dart';
という行を追加します。VS Code では、パッケージの追加が正しくできていれば、import 'package:f
くらいまで入力すれば自動補完機能により簡単に選択できるので便利です。 -
MyApp クラスについて
Hooks を使えば、StatelessWidget クラスと StatefulWidget クラスとを分ける意味がなくなりますが、かといって StatelessWidget の代わりに HookWidget を使う意味もあまりないので MyApp クラスについては変更しませんでした。 -
MyHomePage クラスを変更する
- MyHomePage クラスの親を StatefulWidget から HookWidget に変更します。
- コンストラクタと title メンバ変数はそのままで OK です。
- createState メソッドは削除します。
- build メソッドを新しく追加します。内容は _MyHomePageState の build メソッドをコピーします。
-
_MyHomePageState クラスを削除する
build メソッド以外は用なしです。クラス宣言を丸ごと削除します。 -
MyHomePage クラスの build メソッドを変更する(1)
return Scaffold(
の直前にfinal counter = useState(0);
を追加します。ここが Hooks の肝となる部分です。useState 関数は、引数で与えられた初期値で初期化された値(value プロパティ)を持つ ValueNotifier 型の新しいインスタンスを返します。そして、これに適切に設定されたリスナーにより value プロパティが変更されるたびに Widget が再描画されます。
React ですと、useState 関数が返す setter を自前で呼び出す必要がありますが、Flutter では自動的に呼び出してくれます。 -
MyHomePage クラスの build メソッドを変更する(2)
appBar の title はわざわざ StatefulWidget のメンバ変数を 参照しなくてもよくなりましたのでtitle: Text(widget.title),
からtitle: Text(title),
に変更します。 -
MyHomePage クラスの build メソッドを変更する(3)
カウンターを表示する部分は、'$_counter'
から、counter.value.toString()
に変更します。Dart では、PHP みたいに文字列の中に$ほにゃらら
でほにゃらら式の内容を表示できます。JavaScrit では、ストリングリテラル中に${ほにゃらら}
とすることでほにゃらら式の内容を表示できますが、Dart ではストリングリテラル以外でも使えて便利です。 -
MyHomePage クラスの build メソッドを変更する(4)
floatingActionButton の onPressed イベントを
() => counter.value++
に変更します。
前述のように、value プロパティを変更するだけで Widget が再描画されますので、たったこれだけの記述で済みます。
終わりに
いかがでしたでしょうか。
Hooks がコードの記述量を減らしてくれることを実感できたのではないでしょうか。
仮に Flutter に Hooks が標準で搭載されたとしても、 StatefulWidget クラスもそのまま残るはずです。ですから、このような変換作業を経験しておけば他人のソースで StatefulWidget クラスが使われていてもびっくりしなくて済みそうです。
また、こうして他の言語での Hooks の使い方を知ることで React における Hooks についての理解もより深まったのではないでしょうか。
少しでもお役に立てれば幸いです。