【Flutter】Flutter Hooksを用いた状態管理

こんにちは、株式会社Pentagonでアプリ開発をしている山崎です!

この記事では、はじめてFlutter Hooksを使う人向けに導入から、使い方までを説明します。Hooksを使ってみたいけど何から始めていいか分からない人にとって役立つ内容になっています。Fluter Hooksにはさまざまな種類がありますが、今回は一番よく使用されるであろうuseState,useEffect,useTextcontrollerの3つについて説明します。

【こんな人に読んで欲しい】

  • はじめてFlutter Hooksを使う人
  • Flutter Hooksで何ができるのか知りたい人

【この記事を読むメリット】

  • Hooksを導入できるようになる
  • HooksのuseState,useEffect,useTextcontrollerの使い方がわかる

【結論】

Flutter Hooksは簡単に導入、使用することができ、StatefulWidgetを使って状態を管理する場合と比較して記述量を大幅に減らすことができます。

目次

Flutter Hooksの導入

最初に、pubspec.yamlにflutter_hooksを入力し、保存し、ターミナルに「flutter pub get」を入力し、flutter_hooksをインストールします。

dependencies:
    flutter_hooks:
flutter pub get

次に、flutter_hooksを使用するファイルに下記のインポート文を記述します。

import 'package:flutter_hooks/flutter_hooks.dart';

最後にStatelessWidgetを作成し、extendsの後のStatelessWidgetをHookWidgetに書き換えます。

class FirstPage extends HookWidget {

以上でFlutter Hooksを使う準備が整いました。

useStateとは

useStateを使うことによって、値を保持することができ、その値が変更されるたびにwidgetを自動的に更新することができます。
例えば

final result = useState(0);

と書くと、0というintの値を保持することができます。
(保持する値はint以外も可能です。また、1つのファイルに複数のuseStateを記述することも可能です。)

この保持した値にアクセスするには、

result.value

と書きます。

この値を変更するには、

result.value = result.value + 1;

と書くことで、0から1に変更することができます。
また、この時、同時にwidgetが更新されます。

下記にサンプルアプリ画面1のソースコードを載せます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hello/SecondPage.dart';

class FirstPage extends HookWidget {
 const FirstPage({super.key});

 @override
 Widget build(BuildContext context) {
   //どういう時にbuildメソッドが呼ばれるのか知るために書きました。
   print('build');
   //0を初期値に設定する。
   final result = useState(0);
   return Scaffold(
     floatingActionButton: Column(
       mainAxisAlignment: MainAxisAlignment.end,
       children: [
         FloatingActionButton(
           heroTag: "hero1-1",
           child: const Icon(
             Icons.add,
           ),
           onPressed: () {
             //値を+1する
             result.value = result.value + 1;
           },
         ),
         const SizedBox(height: 20),
         FloatingActionButton(
           heroTag: "hero1-2",
           child: const Icon(
             Icons.remove,
           ),
           onPressed: () {
             //値を-1する
             result.value = result.value - 1;
           },
         ),
       ],
     ),
     appBar: AppBar(
       title: Text("サンプルアプリ画面1"),
     ),
     body: Center(
         child: Column(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
         Text(
           '${result.value}',
           style: TextStyle(fontSize: 100),
         ),
         TextButton(
           child: Text(
             "サンプルアプリ画面2へ遷移する",
           ),
           onPressed: () {
             Navigator.push(context,
                 MaterialPageRoute(builder: (context) => SecondPage()));
           },
         ),
       ],
     )),
   );
 }
}

このサンプルアプリ画面1では、右下の+ボタンをタップすることによって、useStateを使って保持した値を+1します。
また、右下のーボタンをタップすることによって、useStateを使って保持した値を-1します。
さらに、ボタンをタップするたびにコンソールにbuildが表示されます。

flutter: build

このことからuseStateに保持された値が変更されるたびにbuildメソッドが呼ばれ、widgetが更新されたのが分かります。

useEffectとは

useEffectメソッドは第2引数の値が変化する場合と変化しない場合で動きが違うので、2つに分けて考えます。

useEffectメソッドの第2引数が変化しない場合

この場合に、useEffectを使用すれば、widgetのbuildメソッドが一番最初に呼ばれた時とwidgetが破棄された時にだけ実行したい処理を書くことができます。

つまり、StatefullWidgetのinitメソッドとdisposeメソッド内で処理を記述するのと同じことができます。

使い方は下記のようになります。

//useEffectの使い方
   useEffect(() {
//ここにbuildメソッドが一番最初に呼ばれたときにだけ実行したい処理を書く
     print('サンプルアプリ2-2');
     return () {
//ここにwidgetが破棄された時に実行したい処理を書く
       print('サンプルアプリ2-3');
     };
//第2引数にここでは不変のconst[]を設定しています
//const[]は変更されないため、 print('サンプルアプリ2-2');は最初の一度しか実行されない
   }, const []);

下記にサンプルアプリ画面2のソースコードを載せます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hello/ThirdPage.dart';

class SecondPage extends HookWidget {
 const SecondPage({super.key});

 @override
 Widget build(BuildContext context) {
   print('サンプルアプリ2-1');
   final result = useState(0);

   //useEffectの使い方
   useEffect(() {
     //ここにbuildメソッドが一番最初に呼ばれたときだけ実行した処理を書く
     print('サンプルアプリ2-2');

     //このreturn文は省略可能です
     return () {
       //ここにwidgetが破棄された時に実行したい処理を書く
       print('サンプルアプリ2-3');
     };
     //第2引数にここでは不変のconst[]を設定しています。
     //const[]は変更されないため、 print('サンプルアプリ2-2');は最初の一度しか実行されない
   }, const []);
   return Scaffold(
     floatingActionButton: Column(
       mainAxisAlignment: MainAxisAlignment.end,
       children: [
         FloatingActionButton(
           heroTag: "hero2-1",
           child: const Icon(
             Icons.add,
           ),
           onPressed: () {
             result.value = result.value + 1;
           },
         ),
         const SizedBox(height: 20),
         FloatingActionButton(
           heroTag: "hero2-2",
           child: const Icon(
             Icons.remove,
           ),
           onPressed: () {
             result.value = result.value - 1;
           },
         ),
       ],
     ),
     appBar: AppBar(
       title: Text("サンプルアプリ画面2"),
     ),
     body: Center(
         child: Column(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
         Text(
           '${result.value}',
           style: TextStyle(fontSize: 100),
         ),
         TextButton(
           child: Text("サンプルアプリ画面3へ遷移する"),
           onPressed: () {
             Navigator.push(context,
                 MaterialPageRoute(builder: (context) => ThirdPage()));
           },
         ),
       ],
     )),
   );
 }
}

サンプルアプリ画面1からサンプルアプリ画面2に遷移し、一番最初にbuildメソッドが呼ばれるとコンソール画面に

flutter: サンプルアプリ2-1
flutter: サンプルアプリ2-2

と表示されます。

その後、右下のボタンを複数回タップしbuildメソッドが何回呼ばれてもコンソール画面には

flutter: サンプルアプリ2-1

としか表示されません。

次に左上のバックボタンをタップしてwidgetを破棄すると、コンソール画面に下記のように表示されます。

flutter: サンプルアプリ2-3

次に、再びサンプルアプリ画面2に戻り、サンプルアプリ画面3に遷移すると、widgetは破棄されていないため、コンソールにはサンプルアプリ画面2のprint文は何も表示されません。

useEffectメソッドの第2引数が変化する場合

useEffectの第2引数が変更されるたびに、useEffectの第1引数に記述された処理全てが実行されます。

実際にサンプルアプリ画面3でどのような動きをするのか確認しましょう。

下記にサンプルアプリ画面3のソースコードを載せます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hello/FourthPage.dart';

class ThirdPage extends HookWidget {
 const ThirdPage({super.key});

 @override
 Widget build(BuildContext context) {
   print('ページ3-1');
   final result = useState(0);

   useEffect(() {
     print('ページ3-2');
     return () {
       print('ページ3-3');
     };
     //第2引数に右下のボタンをタップしたら変更される値を設定している
     //result.valueが変更されるたびに、コンソールにページ3-1,ページ3-2,ページ3-3が表示される
   }, [result.value]);
   return Scaffold(
     floatingActionButton: Column(
       mainAxisAlignment: MainAxisAlignment.end,
       children: [
         FloatingActionButton(
           heroTag: "hero3-1",
           child: const Icon(
             Icons.add,
           ),
           onPressed: () {
             result.value = result.value + 1;
           },
         ),
         const SizedBox(height: 20),
         FloatingActionButton(
           heroTag: "hero3-2",
           child: const Icon(
             Icons.remove,
           ),
           onPressed: () {
             result.value = result.value - 1;
           },
         ),
       ],
     ),
     appBar: AppBar(
       title: Text("サンプルアプリ画面3"),
     ),
     body: Center(
         child: Column(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
         Text(
           '${result.value}',
           style: TextStyle(fontSize: 100),
         ),
         TextButton(
           child: Text(
             "サンプルアプリ画面4へ遷移する",
           ),
           onPressed: () {
             Navigator.push(context,
                 MaterialPageRoute(builder: (context) => FourthPage()));
           },
         ),
       ],
     )),
   );
 }
}

サンプルアプリ画面3では、useEffectの第2引数にuseStateに保持した値を入れています。
右下のボタンをタップするたびに、その保持した値が+1または−1され、値が変更されます。
右下のボタンをタップするたびに、コンソール画面には、下記のように表示されます。

flutter: ページ3-1
flutter: ページ3-2
flutter: ページ3-3

このことから、useEffectの第2引数が変更されるたびに、第1引数に記述された処理全てが実行されているのが分かります。

useTextEditingControllerとは

最後はUseTextEditingControllerです。
UseTextEditingControllerとは、TextEditingControllerのHooksバージョンです。

下記のように作成します。

final textController = useTextEditingController();

これだけです。
これをTextEditingControllerと同じように、TextFieldのcontrollerなどに設定して使用するだけです。
TextEditingControllerとの大きな違いは、コードの記述量を大幅に減らせることです。
TextEditingControllerは、Statefulwidgetに記述する必要があり、dispose処理などが必要ですが、useTextEditingControllerはHookWidgetに記述することができ、dispose処理を記述する必要がありません。
これにより大幅に記述量を減らすことができます。

下記にサンプルアプリ画面4のソースコードを載せます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class FourthPage extends HookWidget {
 const FourthPage({super.key});

 @override
 Widget build(BuildContext context) {
   //ここでUseTextEditingControllerを作成
   final textController = useTextEditingController();

   return Scaffold(
     appBar: AppBar(
       title: Text("サンプルアプリ画面4"),
     ),
     body: Center(
       child:
           //UseTextEditingControllerをTextFieldに設定
           TextField(
         controller: textController,
         style: TextStyle(fontSize: 25),
       ),
     ),
   );
 }
}

まとめ

Flutter Hooksは簡単に導入し、使用することができます。
それによって、StatefulWidgetを使用する必要がなくなり、大幅に記述量を減らすことができます。
Flutter Hooksを敬遠していた人もこれを機会にぜひ積極的に取り入れてみてはいかがでしょうか?

おまけ

Flutterに関する知識を深めたい方には、『Flutterの特徴・メリット・デメリットを徹底解説』という記事がおすすめです。

この記事では、Flutter アプリ開発の基本から、flutter とは何か、そして実際のflutter アプリ 事例を通じて、その将来性やメリット、デメリットまで詳しく解説しています。
Flutterを使ったアプリ開発に興味がある方、またはその潜在的な可能性を理解したい方にとって、必見の内容となっています。

ぜひ一度ご覧ください。

採用情報はこちら
目次