🥚

【Flutter】GetXの世界1 ~カウンターアプリで基本を学ぼう編~

2021/10/11に公開

GetXの世界① ~カウンターアプリで基本を学ぼう編~

GetXとは

GetX(パッケージ名: get)とはFlutterの公式ドキュメントでも紹介されている状態管理ソリューションの一つです。

状態管理以外にも依存オブジェクトやRoute管理のソリューションなど、さまざまな機能を提供する統合パッケージであり、海外では bloc(BLoC) / Provider / Riverpod と並び人気があります。

シンプルなシンタックス、そしてメモリ管理の容易さなどが人気の理由ですが、一方で同じパッケージ内で多くの機能を提供するがゆえ敬遠する開発者も多い印象です。また日本語での情報がまだ少ないため、このシリーズでは項目を絞って、かつなるべく実例付きで機能を紹介していけたらと思います。

ちなみに私が翻訳したGetXの日本語ドキュメントが先日公式にマージされたので、導入の検討をしている方はこのシリーズと併せて読んでいただけるとうれしいです。

今後、GetXが日本でも状態管理ソリューションの選択肢の一つとして挙げられるようになることを願います。

とりあえずこれだけ覚えれば大丈夫!

GetXには様々な機能・APIがありますが、ひとまず以下の4つを抑えておけばGetXの特徴の片鱗をつかむことができるのではないかと思います。

1️⃣ GetxController

Controllerクラスを作成する際に継承する抽象クラスです。GetxController を継承することでControllerにライフサイクルを持たせることができます。

class CounterController extends GetxController { }

主なライフサイクルは以下の3つです。

  • onInit() … Controllerを配置したWidgetにメモリが割り当てられた直後に呼び出されます。Controller内で使用するオブジェクトの初期化に利用します。
  • onReady() … onInit()の1フレーム後、Widgetがレンダリングされると呼び出されます。Snackbarなどナビゲーションの処理や非同期の処理を行うときに利用できます。
  • onClose() … … Controllerが破棄される直前に呼び出されます。Controller内で使用したオブジェクトを破棄するときに利用します。

StatefulWidgetと同様のライフサイクルがあることで、initState() や dispose() に書くような処理をControllerにまとめることができます。

class CounterController extends GetxController {
  
  void onInit() {
    super.onInit();
    // イベントlistenerの追加、オブジェクトの初期化など
  }

  
  void onReady() {
    super.onReady();
    // SnackarやDialogの表示など
  }

  
  void onClose() {
    // TextEditingControllerのdisposeなど
    super.onClose();
  }
}

Controller自身の破棄については紐づくWidgetやRouteの破棄とともにGetXが自動で行ってくれますが、それには一定の条件を満たす必要があります(参考)。これについては次回の記事で触れたいと思います。

2️⃣ .obs

Stringやintなどのプリミティブな型、ListやMap型、その他オブジェクトの拡張プロパティです。どんなオブジェクトも .obs を付ける足すことで特殊なStream(Rx)に変換することができます。これによりUI側で値の変化を監視することができるようになります。

class CounterController extends GetxController {
  final count = 0.obs; // varでも可
}

.obsを付けた際の型は以下の通りです。

  • int → RxInt(Rx<int>)
  • Stinrg → RxString(Rx<String>)
  • List → RxList(Rx<List>) など

.obsはカスタムクラスに付けることもできます。

class CounterModel {
  final int count;
  CounterModel(this.count);
}

class CounterController extends GetxController {
  final count = CounterModel(0).obs;
}

ちなみにobsは Observable(監視可能なオブジェクト) の略です。

3️⃣ Get.put() / Get.find()

Get.put() により、指定のControllerインスタンスをメモリに注入することができます。

class CounterPage extends StatelessWidget {
  final controller = Get.put(CounterController());
}

Get.put()で立ち上げたControllerインスタンスはシングルトンです。つまり、Get.put()で同じControllerを何度立ち上げてもインスタンスは一つということになります。

ただし、tagプロパティに一意のStringを指定することで同じControllerの異なるインスタンスを作成することもできます。

また立ち上げたControllerはウィジェットツリーのどこからでもアクセスできます。元のRouteが生きている限り、異なるRouteからでもアクセスすることができます。

別のクラスから同じControllerインスタンスにアクセスするには Get.find() を使います。(Get.put()の際にtagを使用した場合は同じtagを指定)

class AnotherPage extends StatelessWidget {
  final controller = Get.find<CounterController>();
  // もしくは final CounterController controller = Get.find();
}

4️⃣ Obx()

Controllerで設定したRxオブジェクトのプロパティ「value」を監視して、値の変化に応じて指定ウィジェットを更新するウィジェットです。Obx() のかっこの中にはWidgetを生成するビルダーが入ります。

  
  Widget build(BuildContext context) {
    return Obx(() => Text('${controller.count}'));
  }

当然、更新をトリガーするにはウィジェットにControllerのRxプロパティを使用する必要があります。ちなみに、Obxは "Observe Rx"(ReactiveXを監視)の略だと思われます(多分🙄 )。

ということで、早速これらを使って基本のカウンターアプリを作ってみましょう!

基本のカウンターアプリを作ってみよう

GetxControllerの作成とRxの設定

import 'package:get/get.dart';

class CounterController extends GetxController {
  final count = 0.obs;
  // final count = RxInt(0); でも同じです。varでも可。
}

Viewクラスの作成

class CounterPage extends StatelessWidget {
  GetPutPage({Key? key}) : super(key: key);
  final controller = Get.put(CounterController());

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Obx(() => Text('${controller.count}')), // ①
            ElevatedButton(
              onPressed: () => controller.count.value++, // ②
              child: const Text('increase'),
            ),
          ],
        ),
      ),
    );
  }
}

カウンターアプリ
カウンターアプリ

簡単ですね。

コメント①: ここは「controller.count.value」としても同じです。RxオブジェクトのtoString()メソッドはvalue.toString()となるようにoverrideされています。

コメント②: Controllerでcount変数の宣言をvarもしくはRxIntとしていた場合はここは「controller.count++」とすることもできます。

そのほかにもRx型は元となる「型」と同じオペレーターやメソッドを引き継いでいるため、Rxであることを意識することなく使用することが可能です。

次に、もう一つRxプロパティをControllerに設定して「countが偶数のときだけその偶数が表示されるウィジェット」を作ってみましょう。

RxをWorkerで監視する

前述した通り、Rxの実態はStreamです。なので普通のStreamと同様にlistenすることができます。この仕組みを利用して「countが偶数のときに更新されるRxプロパティ」が作れそうですね。まずは受け皿を準備します。

class CounterController extends GetxController {
  final count = 0.obs;
+ final countEven = 0.obs;
}

「countが偶数のときに」なので、count変数を監視する必要がありますが、どこにどのような処理を書けばいいでしょうか?

どこに、についてはinitState()のような役割を持つライフサイクルイベントがGetxControllerにもありましたね。onInit() です。ここに処理を書きましょう。

onInit..と入力することでIDEが自動的に候補を表示してくれるかと思います。

class CounterController extends GetxController {
  final count = 0.obs;
  final countEven = 0.obs;

  
  void onInit() {
    super.onInit(); // この下に処理を書く
  }

次にever..と入力して候補のトップを選択。次のような処理を書きます。

  
  void onInit() {
    super.onInit();
    ever<int>(count, (value) {
      // count.valueが偶数なら、countEvenにその値を入れる
      if (value.isEven) {
        countEven.value = value;
      }
    });
  }

ever()Workerクラスのメソッドの一つです。Rxの値を監視して、値に変化がある度に指定したコールバックを実行してくれます。

everメソッドのパラメータは以下の通りです。

  • ever< Rx.valueの型 >( 監視するRx , コールバック )
    ※ コールバックが受け取る値はRx.value

これで更新するウィジェットの元となるRx変数の準備ができました。それではこれをUIに組み込みます。

(中略)
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Obx(() => Text('${controller.count}')),
          ElevatedButton(
            onPressed: () => controller.count.value++,
            child: const Text('increase'),
          ),
+         Obx(() => Text('${controller.countEven}')),
        ],
      ),

偶数のときだけ更新されるWidget
偶数のときだけ更新されるWidget

できましたね!
ちなみにWorkerはStreamSubscriptionのようなものなので、忘れずにdispose()しておきましょう。

class CounterController extends GetxController {
  final count = 0.obs;
  final countEven = 0.obs;
+ late final Worker _worker;

  
  void onInit() {
    super.onInit();
+   _worker = ever<int>(count, (value) {
      if (value.isEven) {
        countEven.value = value;
      }
    });
  }

+ 
+ void onClose() {
+   _worker.dispose();
+   super.onClose();
+ }
}

Workerの種類

Workerにはeverの他に以下の種類があります。

  • everAll() … 指定したRxのリストを全て監視して、それぞれにコールバックを実行する。
  • once() … 指定したRxに、指定した条件に応じて、コールバックを一度だけ実行する。
  • debounce() … 指定したRxが更新された後、指定秒数の間変化がなかった場合にのみ、最後の更新に対してコールバックを実行する。検索機能などに有効。
  • interval() … 指定したRxが更新され続ける間、指定秒数の間隔でしかコールバックを実行しない。DDoS対策に有効。

これらのWorkerについては、また別の記事で触れたいと思います。

Get.find() を使ってControllerインスタンスを探す

次に、別ページに遷移してそこで現在の数字が「偶数」なのか「奇数」なのかを表示する機能を付けてみたいと思います。

これについてはcount変数をlistenする必要もなく、値に応じたStringを返すgetterを作れば良さそうですね。

class CounterController extends GetxController {
  final count = 0.obs;
  final countEven = 0.obs;
  late final Worker _worker;
+ String get isEvenText => count.isEven ? 'Even' : 'Odd';
}

それではこのStringを表示する別ページを作成しましょう。一度立ち上げたControllerインスタンスを探すには Get.find() でしたね。

class AnotherPage extends StatelessWidget {
  AnotherPage({Key? key}) : super(key: key);

  final controller = Get.find<CounterController>();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('${controller.count}'),
            Text(controller.isEvenText),
          ],
        ),
      ),
    );
  }
}

現在の値が偶数か奇数か
現在の値が偶数か奇数か

このページをNavigator.push()してみてもらえると分かると思いますが、別Routeにも関わらず、きちんと前のページで使用したControllerインスタンスを拾ってきてくれていますね。

Get.find()はControllerを立ち上げたWidgetやRouteが破棄されていない限り、アプリのどこにいてもControllerを探すことができます。ツリー構造を意識する必要がないので便利ですね。

次の記事

https://zenn.dev/inari_sushio/articles/d3801e74d22c2d

Discussion