💜

【Flutter】「GetX エコシステム」- 状態管理編(翻訳記事)

2021/09/07に公開

本記事はこちらの記事の日本語翻訳です。執筆者であるAachman Gargさんの許諾を得て翻訳しています。

GetXは海外での知名度の割に、日本語情報が少なすぎると感じ翻訳を思い立ちました。状態管理のパッケージとしてProviderやRiverpod以外の選択肢も模索している方に読んでもらえれば幸いです。

後編はこちら
https://zenn.dev/inari_sushio/articles/3ce3494f37166c


表紙

Flutterは本当に素晴らしいですね。数あるフレームワークの中でも、機能やパフォーマンス面で妥協することなくクロスプラットフォームなアプリをスピーディに作れるという意味では、ベストな選択肢だと思います。開発体験(DX)の点でも合格です。

もちろん、パーフェクトと言うつもりはありません。まだ改善の余地がある点も多いと感じます。

たとえば、BLoCパターンで書かなければいけないボイラープレートコード、MobX導入時にコード生成にかかる時間、NavigatorやMediaQueryを活用する際に書く長い構文、などなど。こうしたちょっとした時間の浪費が開発体験を損ねる原因になることもあります。

GetXとは

GetXは簡素なアプローチとわかりやすい構文でボイラープレートコードを最小限にしてくれるマイクロフレームワークです。GetXによる開発はまさにシンプル&パワフルの一言に尽きます。

また状態管理のみならず、依存オブジェクトの注入やRoute管理のソリューションも提供し、さらにはその他の開発を楽にしてくれるユーティリティ(多言語化、テーマ切り替え、バリデーション機能など)も多く提供してくれます。

多機能すぎてパフォーマンス面が不安に感じるかもしれません。でもGetXは統合パッケージであり機能ごとに個別パッケージになっているので、開発者自身がどれを導入する・しないを自由に選択することができるので安心してください。

とはいえ、一度GetXを使い始めたら状態管理以外の機能も使ってみたくなると思います。一緒に使うことでそれぞれの機能が共鳴するような心地良さがあるからです。私はそれを 「GetX エコシステム」 と呼んでいます。

GetXの何がいいのか

  • シンプルな構文
    たとえば、別ページへの遷移。contextもbuilderも不要、Routeを意識する必要なし。Get.to(SomePage()) と書くだけです。

  • パフォーマンス
    controllerの類は開発者がどれをdispose()するか選び、それをコードに落とすのが通常ですが、GetXでは考えが逆です。開発者はどのcontrollerをメモリに残すかを選択します(それ以外は自動でリソース解放)。コード量もメモリも減らすことができますね。

  • Viewとロジックの切り離し
    GetXはcontextに依存しないので切り離しがしやすいです。依存オブジェクトでさえ、Bindingsというクラスを利用することでWidgetから切り離すことができます。

  • 機能の集約管理
    多種多様な機能を使うために多くのパッケージを管理していくのは骨が折れる作業です。パッケージの一つに破壊的変更でもあれば目も当てられません。GetXを導入することである程度を集約管理してしまえば、互換性やメンテナンスの心配を減らすことができます。

状態管理、結局どれがいいの?! 🤯

状態管理はFlutter界ではいまだに熱いトピックです。選べる手法/パッケージが多いので初学者は萎縮しちゃいますよね。それぞれいい点と悪い点があるので迷ってしまいます。

いずれにしても自分が使いやすいと思うものを選ぶのが一番ですが、この記事をきっかけにGetXも選択肢に加えてもらえたらうれしいです。

では、GetXが実際にどんなものなのかみていきましょう。

GetMaterialApp

ステップ0: MaterialAppをGetMaterialAppに差し替える。

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return GetMaterialApp(); // MaterialAppの代わりに
  }
}

GetxController

Controller系クラスにはビジネスロジックに関わる変数やメソッドが集約され、それらをViewからアクセスすることで機能を実現します。

GetXにはこのControllerに使う専用クラス GetxController が存在します。GetxControllerは DisposableInterface(dispose可能なインターフェース)クラスを継承しています。

これはつまり、画面遷移のスタックからWidgetが消えればcontrollerも瞬時にメモリから削除されるということです。GetXでは明示的にdispose()するものは何もありません。

またGetxControllerには onInit()onClose() などのメソッドがあります。StatefulWidgetの initState() / dispose() などを代替するものであり、これによりStatelessWidgetのみでアプリ全体を構成することも可能になります。

class Controller extends GetxController {
  
  void onInit() { // widgetにメモリが割り当てられ次第実行される
    fetchApi();
    super.onInit();
  }

  
  void onReady() { // widgetが描画され次第実行される
    showIntroDialog();
    super.onReady();
  }

  
  void onClose() { // controllerがメモリから削除される直前に実行される
    closeStream();
    super.onClose();
  }

}

3つのアプローチ

GetBuilderによるアプローチ

GetBuilder で他のWidgetをラッピングすることでcontrollerの変数やメソッドにアクセスし、関数を呼び出したり、状態の変化をlistenしたりすることが可能になります。

まずはcontrollerから作成してみましょう。

class Controller extends GetxController {
  int counter = 0;

  void increment() {
    counter++;
    update(); // 注目!
  }
}

GetBuilderをUI側に使う場合、Widgetに変更を通知するため update() メソッドは必須です。ChangeNotifierのnotifyListeners()と同じ要領ですね。

そしてUI側でGetBuilderの設定をします。

view側
GetBuilder<Controller>( // <controllerの型を指定>
  init: Controller(), // controllerを初期化
  builder: (value) => Text(
    '${value.counter}', // valueはControllerのインスタンス
  ),
),
GetBuilder<Controller>( // 一度initすれば再度する必要なし
  builder: (value) => Text(
    '${value.counter}', // increment()を呼び出せば更新される
  ),
),

Controllerの初期化はGetBuilderで初めて使用するときだけすればよく、同じControllerを他のGetBuilderで使用するときは再度初期化する必要はありません。型を指定していれば、他のGetBuilderは自動で最初のGetBuilderの状態を共用します。これはGetBuilderがアプリのどこに配置されていても同じです。

GetBuilderは基本的にStatefulWidgetの代わりと考えてください。GetBuilderをうまく利用することですべてのviewをStatelessにすることも可能です。コードをクリーンに保ちつつ、永続的でない状態を管理したい場合に最適なのがGetBuilderです。

また一意のIDをGetBuilderに割り当てることで通知するBuilderを限定することができます。

GetBuilder<Controller>(
  id: 'aVeryUniqueID', // ここでID割り当て
  init: Controller(),
  builder: (value) => Text(
    '${value.counter}', // これは変わる
  ),
),
GetBuilder<Controller>(
  id: 'someOtherID', // ここでID割り当て
  init: Controller(),
  builder: (value) => Text(
    '${value.counter}', // これは変わらない
  ),
),

class Controller extends GetxController {
  int counter = 0;

  void increment() {
    counter++;
    update(['aVeryUniqueID']); // ここでID指定
  }
}

GetX(クラス)によるアプローチ

GetBuilder は処理が速くメモリーフットプリントも低く抑えられますが、リアクティブではありません。このため、GetXにはGetBuilderとは別にGetXというクラスも用意されています。

GetX(クラス)はGetBuilderとシンタックスが似ていますが、アプローチはStreamベースです。

まずはGetxControllerを作成しましょう。

class Controller extends GetxController {
  var counter = 0.obs; //注目!

  void increment() => counter.value++;
}

今度は update() を使用しない代わりに、変数の後に .obs を付けます。.obsと付けることでどんな変数もstreamになり、監視可能になるのです。obsはobservableの略です。

このcounterの例で言えば、intがStream<int>になるようなものと考えてください(実際は RxInt になる)。これでview側からGetXクラスを使って変更を監視することができます。

GetX<Controller>(
  init: Controller(),
  builder: (val) => Text(
    '${val.counter.value}',
  ),
),

GetX<Controller> は基本的にボイラープレートコードを省いたStreamBuilderです。

counterの型はRxIntなので、実際に値を取り出すには .value とします。controller側で.obsと付けた変数をview側で利用する際はすべてこれが必要です。

では、クラスオブジェクト自体を監視したい場合はどうするのでしょうか。実はこれも同じで、.obsを付けるだけです。 次の項目でやってみましょう。

Obxによるアプローチ

これは3つのアプローチのうち個人的に一番のお気に入りです。構文が一番シンプルで、導入しやすいと思います。使い方は、widget を Obx(() ⇒ ) でラッピングするだけ。

class User {
  String name;
  User({this.name});
}

class Controller extends GetxController {	
  var user = User(name: "Aachman").obs; //他の変数と同じ要領で
	
  void changeName() => user.value.name = "Garg"; //.valueでクラス変数にアクセス
}
view側
Obx(() => Text(
  '${controller.user.value.name}'
  ),
),

setStateに似た構文ですね。controllerの初期化はこのように行います。

class PageOne extends StatelessWidget {
  Controller controller = Get.put(Controller());
  // controller = Controller() の代わりに
}

put() することでcontrollerがルートに置かれます。これにより複数のwidgetから同じcontrollerにアクセスできるわけです。

それでは同じcontrollerインスタンスを他のクラスで使ってみましょう。

class PageSeven extends StatelessWidget {
  Controller controller = Get.find();
}

find() は以前使った同じ型のインスタンスを、ツリーのどこからでも自動で探してきてくれます。

これでPageSevenで変更を加えたUserが、PageOneにも反映されるようになりました。

結局どれを使えばいいの?

GetBuilder を使うケース

  • 永続的でない状態を管理するとき。つまり、setStateを使用する場面とほぼ同じ。
  • パフォーマンス優先なとき。状態はBuilder間でシェアされ、RAMをあまり消費しない。
  • streamを扱いたくないとき。

GetX を使うケース

  • リアクティブに状態管理をしたいとき。
  • 値が実際に変わったときのみwidgetを再描画したいとき。たとえば変数が"Garg"から"Garg"に変わっても再描画はされない。
  • Get.put()を使いたくないとき。

Obx を使うケース

  • シンプルな構文を好む場合。
  • 同じwidget内で複数のcontrollerを扱う必要があるとき。Obxは型を指定する必要がないので、このような使い方も可能。
Obx(() => Text(
  '${firstController.counter.value + thirtySeventhController.counter.value}'
  ),
),
  • Bindingsを使って依存オブジェクトを束ねて管理するときは常にObx(これについては次の記事で)。

MixinBuilder

え?まだあったの?!
はい、まだあります。。名前の通り、MixinBuilderは3つのアプローチのうちGetBuilderとObxを混ぜ合わせるものです。MixinBuilderを使えば、"Aachman" と "Aachman".obs を同じwidgetの中で利用することができます。

すごくないですか?なんで上で取り上げなかったかって?理由は2つあります。

  • 処理が重いアプローチなので一定規模のアプリではパフォーマンスに影響が出るから。
  • 永遠に出会わないような特殊なケースでしか使用が想定できないので。

念の為紹介します。

class Controller extends GetxController {
  int one = 1; // ただのint変数

  void incrementOne() {
    one++;
    update();
  }

  var two = 2.obs; // リアクティブなRxInt

  void incrementTwo() => two.value++;
}
view側
  MixinBuilder(
    builder: (controller) => Text(
      '${controller.one + controller.two.value}'
    ),
  ),

私の使い方

では私はGetXをどう使っているかというと、ほぼObxをBindingsと組み合わせて使っています。Obxならcontrollerを複数組み合わせられるので。

それ以外のシンプルなwidgetや、状態を永続化させる必要がない場合はGetBuilderを使います。

たとえばボタンを押して色を変える場合など。この場合はstreamを使うのはオーバーキルになりますし、普通に選択すればsetStateになるかと思います。しかしStatefulWidgetの使用を避け、ビジネスロジックとviewを切り離したい場合はGetBuilderがこの用途にぴったりです。

以上

GetXを使った状態管理の手法について触れてみました。状態管理のパッケージの中では私は個人的に一番好きです。GetXとこの記事があなたの開発体験(DX)の向上につながりますように。

後編
https://zenn.dev/inari_sushio/articles/3ce3494f37166c

Discussion