[GetX] 状態管理

2022-01-24 hit count image

FlutterでGetXを使って状態管理をする方法について説明します。

概要

Flutterで状態を管理するためにはStatefulWidgetInheritedWidgetを使う必要があります。しかし、複雑な状態を管理するためにはBlocパタンやProviderようなパッケージを使います。

今回のブログポストではFlutterで状態管理に一番使われてるパッケージであるGetXについて紹介します。このブログで紹介するソースコードは下記のリンクで確認できます。

GetX

GetXは状態管理パッケージで有名ですが、実際はこれ以外の機能も持っています。FlutterでGetXを使うと状態管理だけではなく、Route、多言語、画面のサイズ、APIコールなど色んな機能を使えます。

今回のブログポストではこの中で状態管理について説明する予定です。

GetXのインストール

FlutterでGetXの使い方を確認するため次のコマンドを実行してFlutterの新いプロジェクトを生成します。

flutter create state_management

その後次のコマンドを実行してGetXパッケージをインストールします。

flutter pub add get

今回のブログポストではFlutterコマンドで生成したサンプルプロジェクトをGetXを使ってリファクタリングをしてGetxを使った状態管理を説明します。

GetXの設定

FltterでGetXを使うためにはMaterialAppの代わりにGetMaterialAppを使う必要があります。これを確認するためにはlib/main.dartファイルを開いて下記のように修正します。

import 'package:get/get.dart';
...
class MyApp extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      ...
    );
  }
}

状態管理

GetXは次のように2つ状態管理方法を提供してます。

  • 単純状態管理(Simple state management)
  • レスポンシブ状態管理(Responsive state management)

単純状態管理

GetXを使って単純状態管理をするため、lib/controller/count_controller.dartファイルを生成して次のように修正します。

import 'package:get/get.dart';

class CountController extends GetxController {
  int count = 0;

  void increment() {
    count++;
    update();
  }
}

GetXで状態管理をするためクラスを生成する時にはGetxControllerを相続します。

class CountController extends GetxController {
  ...
}

そして管理する状態変数を宣言します。

class CountController extends GetxController {
  int count = 0;
  ...
}

最後に状態変数をアップデートする関数を宣言します。

class CountController extends GetxController {
  ...
  void increment() {
    count++;
    update();
  }
}

単純状態管理を使う場合、状態値を変更した後、update()関数を使って状態が変更されたことをお知らせする必要があります。update()を使わないと、状態値は変更できるが、画面の更新が行われなくなり、状態が更新されない画面が確認できます。

このように生成したGetXコントローラーを使ってみましょう。lib/main.dartファイルを開いて次のように修正します。

import 'package:get/get.dart';

import 'controller/count_controller.dart';

...

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final controller = Get.put(CountController());

    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:',
            ),
            GetBuilder<CountController>(builder: (controller) {
              return Text(
                '${controller.count}',
                style: Theme.of(context).textTheme.headline4,
              );
            }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

今から私たちはGetXで状態管理をする予定なので、StatefulWidgetウィジェットを使う必要はないです。したがって、次のようにMyHomePageウィジェットをStatelessWidgetウィジェットに変更します。

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);
  ...
}

GetXで状態管理をするためにはGetXで作ったコントローラーを次のようにGet.putを使って登録(Register)する必要があります。このようにコントローラーを登録したら、登録した後のコードではコントローラーを使って状態管理ができるようになります。

class MyHomePage extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(CountController());
    ...
  }
}

単純状態管理で状態の変化を検出して、変更された値を取得するためには次のようにGetBuilderを使う必要があります。GetBuilderを使わないと、状態が変更されたことが検出できなくて、変更された値を画面に反映することができません。

GetBuilder<CountController>(builder: (controller) {
  return Text(
    '${controller.count}',
    style: Theme.of(context).textTheme.headline4,
  );
}),

GetXで生成した状態値を変更するためには私たちが作ったincrement関数をコールする必要があります。increment関数をコールするためには、次のようにFloatingActionButtononPressedイベントに連結します。

floatingActionButton: FloatingActionButton(
  onPressed: controller.increment,
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),

今度はプロジェクトを実行して、アクションボタンを押したら、画面が上手く更新されることが確認できます。このようにGetXの単純状態管理はupdate()関数を使って、状態を画面に反映するタイミングを決めることができます。

レスポンシブ状態管理

レスポンシブ状態管理はupdate関数を使って状態を直接通知する単純状態管理とは違って、内部ロジックで値の状態変化を検出して画面に反映します。

これを確認するためlib/controller/count_controller.dartファイルを次のように修正します。

import 'package:get/get.dart';

class CountController extends GetxController {
  final count = 0.obs;

  void increment() {
    count.value++;
    // count(count.value + 1);
  }
}

単純状態管理とは違って状態変数を生成する時、.obsを使って生成します。このように生成した変数は単純なタイプではなくRxIntようにレスポンシブ状態変数になります。

レスポンシブ状態変数は次のように2つの方法で値を変更することができます。

count.value++;
// or
count(count.value + 1);

次はこのように生成したレスポンシブ状態管理を使うため、lib/main.dartファイルを次のように修正します。

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final controller = Get.put(CountController());

    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:',
            ),
            Obx(
              () => Text(
                "${controller.count.value}",
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

レスポンシブ状態管理ではGetBuilderの代わりにObxを使って状態の変更を検出します。

Obx(
  () => Text(
    "${controller.count.value}",
    style: Theme.of(context).textTheme.headline4,
  ),
),

このようにプログラムを実行してアクションのボタンを押したら、以前と同じようにカウントが上手く上がってることが確認できます。

LifeCycle

StatefulWidgetを使うとウィジェットのライプサイクル関数を使うことができます。これと同じようにGetxControllerを使うと次のようなライプサイクル関数を使うことができます。

class CountController extends GetxController {
  @override
  void onInit() {
    super.onInit();
  }
  @override
  void onClose() {
    super.onClose();
  }
}
  • onInit: コントローラーが生成される時、コールされます。
  • onClose: コントローラーが要らなくなってメモリから消される時コールされます。

Worker

Workerはレスポンシブ状態値の変化が発生した時、これを検出して特定なコルバック関数をコールさせることができます。

ever(count, (_) => print("called every update"));
once(count, (_) => print("called once"));
debounce(count, (_) => print("called after 1 second after last change"), time: Duration(seconds: 1));
interval(count, (_) => print("called every second during the value is changed."), time: Duration(seconds: 1));
  • ever: レスポンシブ状態値が変更されるたびにコールされます。
  • once レスポンシブ状態値が最初変更される時1回コールされます。
  • debounce: debounceと同じように動作します。最後変更の後、特定した時間の間変更がない場合コールされます。
  • interval: intervalと同じように動作します。レスポンシブ状態値が変更される間、一定の間隔でコールされます。

Workerはコントローラーまたはクラスが生成される時だけ使えます。つまり、コントローラーのonInit、クラスのコンストラクタ、StatefulWidgetのinitState中で定義する必要があります。 안에서 호출해야 합니다.

Get.find

今までの例を見るとGetXの状態値を使うためGet.putを使ってコントローラーを生成して使いました。

final controller = Get.put(CountController());

もし、チャイルドウィジェットでこのコントローラーを使って状態値を変更したり、状態値を使う場合はどうすれば良いでしょうか?もちろん次のように生成したコントローラーをパラメータで渡して使うこともできます。

CustomWidget(controller: controller)

しかし、GetXではGet.findを提供して次のようにもっと簡単に生成したコントローラーにアクセスすることができます。

Get.find<CountController>()

これを確認するためlib/main.dartファイルを次のように修正します。

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Get.put(CountController());

    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:',
            ),
            Obx(
              () => Text(
                "${Get.find<CountController>().count.value}",
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: Get.find<CountController>().increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

伊是はGet.put(CountController());を使って生成したcontrollerの変数を使って状態値にアクセスしましたが、現在の例題では次のようにGet.find<CountController>()を使って状態値にアクセスしてることが分かります。

Obx(
  () => Text(
    "${Get.find<CountController>().count.value}",
    style: Theme.of(context).textTheme.headline4,
  ),
),

また、関数を実行する時にも次のようにGet.findを使ってることが分かります。

floatingActionButton: FloatingActionButton(
  onPressed: Get.find<CountController>().increment,
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),

Get.findを使うとGet.putで登録したコントローラーをどこでもアクセスすることができます。今回の例題では同じファイル中で使いましたが、チャイルドウィジェットでも使うことができます。重要なことはGet.putを使って先にコントローラーを登録した後、使うことです。もし、登録されてないコントローラーにアクセスするとエラーが発生します。

Get.isRegistered

Get.findGet.putで登録されたコントローラーだけ使うことができて、登録されてないコントローラーを使うとエラーが発生します。このような問題を解決するためには次のようにGet.isRegisteredを使って使いたいコントローラーが登録されたか確認することができます。

Get.isRegistered<CountController>()

もし、コントローラーが登録されたらtrueが返されて、登録されたない場合はfalseが返されます。

static get to

GetXで状態管理をすると、Get.findを使ってよく状態値にアクセスします。それで、GetXでは次のようにstaticを使うパタンをよく使います。

static CountController get to => Get.find<CountController>();

これを確認するためlib/controller/count_controller.dartファイルを次のように修正します。

import 'package:get/get.dart';

class CountController extends GetxController {
  static CountController get to => Get.find<CountController>();

  final count = 0.obs;

  void increment() {
    count.value++;
    // count(count.value + 1);
  }
}

次はlib/main.dartファイルを次のように修正します。

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Get.put(CountController());

    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:',
            ),
            Obx(
              () => Text(
                "${CountController.to.count.value}",
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: CountController.to.increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

以前はcontroller変数を使って状態値にアクセスする方法を次のようにstaticを使う方法で変更しました。

Obx(
  () => Text(
    "${CountController.to.count.value}",
    style: Theme.of(context).textTheme.headline4,
  ),
),
...
floatingActionButton: FloatingActionButton(
  onPressed: CountController.to.increment,
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),

GetXではよく使うパタンなので、よく覚えておきましょう。

完了

これでFlutterでGetXを使って状態管理をする方法についてみてみました。今度からは皆さんも、StatefulWidgetで状態管理をすることをやめて、GetXを使って状態管理をしてみてください。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。

Posts