Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What are the problem?
@poe_hoshi

webフロントの人向けFlutterの状態管理

ReactやVueを用いてwebフロントなどに携わる方は随分増えたと思いますが、新たな勢力としてFlutterのweb対応も進んできました。トヨタの車載デバイスにも採用されるほど汎用性の高いFlutterですが、UIフレームワークの宿命である状態管理の問題も他と同様に抱えています。
上記ではそれぞれReduxやVuexなどのFlux的な手法がメジャーですがFlutterではその辺どうなのか、ということをまとめました

ちょっとした前置き

まずFlutterというフレームワークはDartという言語で書かれていますが、これはほぼTypeScriptと言いたいぐらいよく似た役割です。そしてTSに対してReactやAngularがあるようにFlutterがあります。

※いまでこそDart2はFlutterのための言語のように扱われていますが、元はGoogleがJavaScriptの代替(主に型付け)として開発した、TypeScriptやCoffeeScriptのようなalt-jsに近い立場でした。そのため他にもDartを利用したソフトは存在しています

そしてそれらのComponentのようにFlutterにはWidgetが存在します。

Stateの利用

単純なアプリの場合はこれらが有する「状態」はコンポーネントごとの変数のように扱う機能があります

Flutterも同様に、Widgetが保持する情報としてStateがありました。少し異なっている点として、FlutterのWidgetはStateを持つものと、持たないものに分かれています。
前者はStatefulWidget、後者はStatelessWidgetと呼ばれ、FlutterのWidgetはこれらと同名のクラスを継承することで定義されます。

Widget定義の例.dart
//StatefulWidgetの例
class SampleStatefulWidget extends StatefulWidget { //←StatefulWidget継承クラスの定義
  SampleWidget({Key key}) : super(key: key); //←コンストラクタ
  @override
  _SampleWidget createState() => _SampleWidget(); //←状態を扱うためのStateクラス
}

class _SampleWidget extends State<SampleWidget> { //State継承クラスの定義
  @override
  Widget build(BuildContext context) {
    return Container(
    );
  }
}


//StatelessWidgetの例
class SampleStatelessWidget extends StatelessWidget { //StatelessWidget継承クラスの定義
  @override
  Widget build(BuildContext context) {
    return Container(
    );
  }
}

上記クラスのbuildメソッド下で実際のコンテンツを描画するのですが、どちらもプロパティを定義して変数を受け取りながら描画を行うものの、StatelessWidgetは一度描画されればあとは変動することがありません。
つまり画面が描写された時点で静的な存在となります。一方StatefulWidgetはStateクラス内にsetState()という関数が存在し、ここでStateクラスのプロパティを更新することが可能で、更新の度に描画し直されます。

なぜこの2種類に分けたのか、という話ですが恐らくFlutterは多様な環境に最適化されたアプリを単一のコードで書くことを目指したため、処理を軽量化する重要性が高いという理由でしょう。

全てのWidgetがStateを持っていて、変化しないなら使わなければ済むじゃないか、ということが正しいように見えますが、UIコンポーネントであるWidgetの状態が変化するということは、画面の描画という重い処理を常に待ち受けるということになります。それを防ぐために、最初から変化しない=再描画がないWidgetはその機能を削いでいるのではないでしょうか。

ReactやVueと違ってiOS/Android、web、デスクトップという多様な環境に対応するコードを単一のDartからコンパイルするので言語レベルでの最適化に限界があることや、そもそもスペックの低い端末上で動作することも考慮しての姿勢だと思います。

Stateの問題点

しかしStateは「コンポーネントの状態」を扱うための機能であるため他のプラットフォーム同様、「アプリ全体、または少なくとも複数コンポーネント間の状態」を扱うと肥大化し複雑化が進み非常に冗長で管理しにくくなるというデメリットがありました。Widgetは自分の状態しか保持していないので別のWidgetの状態を参照できません。

解決策としては双方の親Widgetの状態として各自の状態をまとめて保持し、参照と更新の際には親を経由するというもの(FlutterではInheritedWidgetとされる)でしたが、問題点があります。せっかく自身の状態変化のみに呼応して描画を行っていたのに、自身の状態は親が持っているので兄弟Widgetの変化など親の状態が変化する度に関係のない自分も描画処理が必要です。また、一方は参照できても逆の方向には秘匿したいなどの場合でも親はその区別がないので混同してしまったり、そもそも状態の依存関係が広がれば広がるほど、コードは長く、処理は重く、管理は煩雑になってしまいます。

Providerの利用

そういう訳で実際には状態を扱うのにあまりStateは用いず(小規模なら効率的であるのと、内部的に使用するので必要な存在ではある)、大きな枠組みでいくつかのアプローチがあります。

かつてはBLoCというパターンが多かったり、ScopedModelや、そしてReduxなども存在していて運用向けのライブラリとともに開発者それぞれの解決策で取り組んでいたのですが、そのうち一つのProviderという仕組みが人気だと思います。というのもProviderについては特徴の優位性も評価され、現在ではGoogle公式に推奨されているためでしょう。(参考

少しだけ実際に触れておきます。詳しいガイドは公式にあるのでかいつまんで掲載します。

Providerの基本的な使い方はChangeNotifierクラスという、状態を保持し、それを参照するWidgetに変更を通知するオブジェクトを定義することです。

ChangeNotifierの例.dart
class CartModel extends ChangeNotifier {
  // cartオブジェクトの状態を表す定数
  final List<Item> _items = [];

  // 状態を更新する関数
  void add(Item item) {

    _items.add(item);

    // この呼び出しでリッスンしているWidgetに更新が通知される
    notifyListeners();

  }
}

そして、このChangeNotifierが保持する状態を、引き渡せるよう管理してくれるのがChangeNotifierProviderです。ChangeNotifierProviderは状態管理を用いたい最上位のWidgetに適用すれば、それ以下のどこからでも参照できるよう状態を管理し、同時にその状態を用いるWidgetのライフタイムも管理します。該当Widgetは依存性や影響を考慮せず、単なるプロパティのように状態を呼び出せます(そして描画の管理はChangeNotifierProviderが行うため全てStatelessWidgetとして定義できる)。

平たく言うとChangeNotifierProvider以下のWidgetはそこで指定した状態を簡単に扱えるというだけです。

ChangeNotifierProviderの例.dart
// アプリのエントリーポイント
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(), //←ここで使いたい状態を指定
      child: MyApp(), //これより下のWidgetはいつでも状態を扱える
      // 実際には個別で小さな状態をProviderで使いたいときなど、好きな階層で記述できる
    ),
  );
}

このように記述しておくとProvider.of<CartModel>(context).add(x);のようにするだけでどこからでも状態を更新できます。
"だけ"は言い過ぎました。実際にはbuild前か後か、複数用いるか、非同期を用いるか、などの選択肢があるのですが典型的な例ではこのようにして状態管理を処理しています

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?