Flutter 3 の状態管理: アプローチ (概念)

作成
( 更新 )
@nabbisen

はじめに

Flutter の状態管理はどうなっているのでしょうか ?」という疑問を友だちから聞いた時に、少し調べてみようと思いました。 調べてみると、既存の有名フレームワークと大きく異なってはいないようだとわかりました。 しかしながら特徴的な点もあります。それをここでまとめてみます。

大きい特徴の一つが「宣言的 (declarative、ディクララティヴ)」であるということです。

宣言的の対になるのが「命令的 (imperative、イムペラティヴ)」です。「こうしてこうしなさい」と指示を記述するのが「命令的」で、「こうありなさい」「こう出力しなさい」と結果にフォーカスして記述するのが「宣言的」です。

Flutter は状態 (state、ステート) 管理よりもさらに上位の構想レベルで、宣言的な実装を志向していて、そのための専門ドキュメント「宣言的な UI (英語) 」が公開されています。

オブジェクト指向言語やシェルスクリプトでコーディングする場合は、命令的記述の方がなじみがあるはずです。一方で宣言的記述の身近で代表的なものは SQL です。細かいチューニングが可能なのは命令的記述ですが、宣言的に書くことでシンプルな記述が可能です。宣言的な記述は、関数型プログラミングに触れたことのある人ならとっつきやすいかもしれません。

さらにこれらは二律背反ではありません。例えば SQL で CASE 式を使うことが部分的に命令的 (指示的) になることもありますし、またインデックスディクショナリを DDL で定義したりその内のどれかを優先的に使うよう「命令」することもできます。

余談ですが、この「宣言的」というパラダイムは、昨今のトレンドの一つのように感じます。一例ですが、仮想 DOM を使わずに非常に高速 / 省メモリを実現しているクライアントフレームワークである SolidJS は、README.md の冒頭で “Solid is a declarative JavaScript library for creating user interfaces.” (私訳: Solid は宣言的な JavaScript のライブラリで、ユーザーインタフェースを構築できます。) とうたっています。


概観

flutter android ide

以下、現在 (2023-01-08 時点) の Flutter の公式ドキュメント中の「State management (英語))」からエッセンスを抜粋して、俯瞰してみようと思います。(引用に対する私訳をイタリックスタイルで記述します。そして私の補足事項を “# " 始まりの通常スタイルで記述します。)

冒頭の注釈

* (原文の関連箇所)

If you are already familiar with state management in reactive apps, you can skip this section, though you might want to review the list of different approaches.

リアクティブな (reactive、サクサク操作できる) アプリの状態管理について詳しい方は、この節をスキップしても大丈夫です。 なおこれとは異なるアプローチを検討したい場合は こちらのリスト (英語) をご覧ください。

宣言的なスタイル

* (原文の関連箇所)

Flutter is declarative. This means that Flutter builds its user interface to reflect the current state of your app:

Flutter は 宣言的 です。すなわち Flutter は、アプリのその時の状態を反映してユーザーインタフェースを構築します:

数式 UI = f(state) 。ここで ‘UI’ は画面レイアウト。‘f’ は実装するメソッド。‘state’ はアプリケーションの状態。

(Flutter より引用の図)

# 式中に他の因子や if / case が無いことに注目してください。状態が決まれば UI は決まります。(ただし f 関数でその状態について定義しておくことは必要でしょう。)

There is no imperative changing of the UI itself (like widget.setText)—you change the state, and the UI rebuilds from scratch.

UI そのものに対して (例えば widget.setText のような) 命令的な変更を与える必要はありません ―― ただ状態を変更するだけで良いのです。そうすれば UI は一から再構築されます。

The declarative style of UI programming has many benefits. Remarkably, there is only one code path for any state of the UI. You describe what the UI should look like for any given state, once—and that is it.

UI プログラミングにおいて宣言的な記述形式は、多くの利点をもたらします。特筆すべきなのが、一つのコードパスだけで、UI のいかなる状態もまかなえるということです。あなたはその状態の時に UI としてどのように表示されるべきかということを一度だけ記述する ―― それだけです。

アプリの状態の定義と種類

* (原文の関連箇所)

In the broadest possible sense, the state of an app is everything that exists in memory when the app is running. This includes the app’s assets, all the variables that the Flutter framework keeps about the UI, animation state, textures, fonts, and so on. While this broadest possible definition of state is valid, it’s not very useful for architecting an app.

First, you don’t even manage some state (like textures). The framework handles those for you. So a more useful definition of state is “whatever data you need in order to rebuild your UI at any moment in time”. Second, the state that you do manage yourself can be separated into two conceptual types: ephemeral state and app state.

広く受け入れられている感覚からすると、アプリの状態とは、アプリが動いている間メモリに蓄えられているすべてです。 この中には、アプリのアセット、つまり UI / アニメーションの状態 / テクスチャ (外観) / フォントなどに関して Flutter フレームワークが保持するすべての変数が、含まれます。 この広く受け入れられている感覚による状態の定義は、的を射たものではありますが、アプリのアーキテクチャを考える上であまり使いやすいとは言えません。

まず初めに、一部の状態 (例えばテクスチャ) は、管理さえ不要です。フレームワークが担います。 このため状態の定義は次のようにできます。「UI 再構築時に必要なデータの総体。必要が生じた時すぐ使えるように管理されている」。こちらの方が使いやすいでしょう。 次に、自分で管理する状態は、概念上、2 つのタイプに分けられます: イフェメラル (ephemeral、一時的 / 一過性) ・ステートと、アプリ・ステートです。

イフェメラル・ステート

* (原文の関連箇所)

Ephemeral state (sometimes called UI state or local state) is the state you can neatly contain in a single widget.

イフェメラル・ステート (UI ステートとか、ローカルステートと呼ばれることもあります) とは、単一のウィジェットにコンパクトに収められる状態のことです。

This is, intentionally, a vague definition, so here are a few examples.

  • current page in a PageView
  • current progress of a complex animation
  • current selected tab in a BottomNavigationBar

この定義は意図してぼかしたものにしています。それではいくつかの例を見てみましょう。

  • PageView (英語) における現在ページ
  • 複雑なアニメーションの進行状況
  • BottomNavigationBar における選択中のタブ

Other parts of the widget tree seldom need to access this kind of state. There is no need to serialize it, and it doesn’t change in complex ways.

In other words, there is no need to use state management techniques (ScopedModel, Redux, etc.) on this kind of state. All you need is a StatefulWidget.

ウィジェット・ツリーの他の部分が、この種の状態にアクセスすることはめったにありません。 状態は、シリアライズすることが不要ですし、複雑な流れで変更されることもありません。

別の言い方をすると、状態管理の技法 (ScopedModel / Redux などがありますね) は、この種の状態については、要らないのです。 必要なのは StatefulWidget (状態を持つウィジェットのクラス) だけです。

Below, you see how the currently selected item in a bottom navigation bar is held in the _index field of the _MyHomepageState class. In this example, _index is ephemeral state.

以下で見てみましょう。底部のナビゲーション・バーで選択中のアイテムが、_MyHomepageState クラスの _index フィールドとして保持されています。この例の _index がイフェメラル・ステートです。

class MyHomepage extends StatefulWidget {
  const MyHomepage({super.key});

  @override
  State<MyHomepage> createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...
    );
  }
}
(Flutter 説明より引用のコード例)

Here, using setState() and a field inside the StatefulWidget’s State class is completely natural. No other part of your app needs to access _index. The variable only changes inside the MyHomepage widget. And, if the user closes and restarts the app, you don’t mind that _index resets to zero.

ここでは setState() と StatefulWidget の State クラス中のフィールドを使うのが、最も自然な方法です。 アプリの他の部分は _index へアクセスする必要がありません。 この変数は MyHomepage ウィジェットの中でのみ変更されます。 さらにユーザーがアプリを閉じて再起動した時のことを考えてみましょう。この時、_index をゼロにリセットすることは、気にかけなくて良いのです。

アプリ・ステート

* (原文の関連箇所)

State that is not ephemeral, that you want to share across many parts of your app, and that you want to keep between user sessions, is what we call application state (sometimes also called shared state).

イフェメラルでは無く、アプリの多くの部分と共有しなければならず、しかもユーザーのセッション更新時も保持したい状態について、考えてみましょう。 これがアプリ (ケーション)・ステートです。(共有ステートと呼ばれることもあります。)

Examples of application state:

  • User preferences
  • Login info
  • Notifications in a social networking app
  • The shopping cart in an e-commerce app
  • Read/unread state of articles in a news app

アプリ・ステートの例を見てみましょう:

  • ユーザー設定
  • ログイン情報
  • ソーシャルネットワーキングアプリにおける通知
  • E-コマースアプリにおけるショッピング・カート
  • ニュースアプリにおける記事 既読 / 未読 の状態

For managing app state, you’ll want to research your options. Your choice depends on the complexity and nature of your app, your team’s previous experience, and many other aspects. Read on.

アプリ・ステートを管理する上で、どのような選択肢があるかを知りたいでしょう。 何を選ぶかは、アプリの複雑さと特性、チームの経験値、それに他の多くの観点から、検討する必要があります。 この点について考えてみましょう。

明確な規約はありません

* (原文の関連箇所)

To be clear, you can use State and setState() to manage all of the state in your app. In fact, the Flutter team does this in many simple app samples (including the starter app that you get with every flutter create).

はっきりと言っておきます。 StatesetState() を使えば、アプリの状態管理のすべてを行えます。 事実、Flutter チームは多数のシンプルなアプリサンプルでこのようにしています。(ここには flutter create で作成できるスターターアプリも含まれます。)

There is no clear-cut, universal rule to distinguish whether a particular variable is ephemeral or app state. Sometimes, you’ll have to refactor one into another. For example, you’ll start with some clearly ephemeral state, but as your application grows in features, it might need to be moved to app state.

For that reason, take the following diagram with a large grain of salt:

ステートをイフェメラルにするかアプリにするか決めるための、明確で普遍的な規約というものはありません。 一方を他方にリファクタする必要が生じることもあるでしょう。 例えば、初めはイフェメラル・ステートで綺麗に収められていたものでも、アプリの機能拡充によってアプリ・ステートに移行することが必要になるかもしれません。

この理由から、次のダイアグラムに沿うと良いでしょう。ただしこちらに盲目的に従うのでは無く、批判と検証の精神は忘れずに。

フローチャート。&lsquo;Data&rsquo; から始まる。&lsquo;必要としている主体はどれ ?&rsquo; に続き、3 つの選択肢がある: &lsquo;大半のウィジェット&rsquo;、&lsquo;いくつかのウィジェット&rsquo;、そして &lsquo;一つのウィジェット&rsquo;。最初の 2 つの選択肢は &lsquo;アプリ・ステート&rsquo; につながる。&lsquo;一つのウィジェット&rsquo; 選択肢は &lsquo;イフェメラル・ステート&rsquo; につながる。

(Flutter より引用の図)

# その状態を必要としているウィジェットの割合が、どちらで状態管理するかを決める参考になる、というフローです。

When asked about React’s setState versus Redux’s store, the author of Redux, Dan Abramov, replied:

“The rule of thumb is: Do whatever is less awkward.”

React の setState vs. Redux の store に関して、Redux 作成者の Dan Abramov は次のように言いました:

“経験則によると: より洗練されている方を採用するのが良い。”

まとめ

In summary, there are two conceptual types of state in any Flutter app. Ephemeral state can be implemented using State and setState(), and is often local to a single widget. The rest is your app state. Both types have their place in any Flutter app, and the split between the two depends on your own preference and the complexity of the app.

まとめます。 Flutter アプリには、それで何をつくるにしても、概念上、2 つの種類の状態 (ステート) があります。 イフェメラル・ステートは StatesetState() で実装できます。単体のウィジェットにローカル的に適用されることが多いです。 もう一方がアプリ・ステートです。 両者ともに Flutter アプリの中でしかるべき位置を占めています。 この 2 つのどちらに出番が来るかは、つくるアプリの設定と複雑さ次第です。


おわりに

Flutter の状態管理がどのようなものであるかについて、公式ドキュメントに沿って概観しました。

宣言的に書けることで得られる恩恵はありがたいですね。学習コストも比較的低くて済みそうです。変数の初期化不備や意図せぬ再代入によるバグが抑えられる等で、減少するリスクもあります。

一方で、冒頭のくり返しになりますが、関数型プログラミングに触れたことがあるかどうかで、実際の学習コストは異なるかもしれません。少なくとも「再帰 (recursion、リカージョン)」については知っておくのが良いでしょう。

さらに、状態変更都度、そのクラスオブジェクト全体の再描画が動くということなので、パフォーマンス上の問題が懸念されます。この点について、モバイルアプリのウィジェット操作の上では、十分なパフォーマンスが担保されている、ということです。

状態管理とパフォーマンスの関連については、ベストプラクティス集や StatefulWidget の API ドキュメントでも触れられています。 本記事の引用でもあった通り、ステートのスコープを Ephemeral or App で、一度決めたら終わりでは無く、適宜検討することが大切になりそうです。

Flutter 公式ドキュメントによる状態管理の説明は、実は、本記事の引用末尾の後も続いてます。シンプルな状態管理の実装例について書かれています。時間ができた時に実践編として、そちらについても記事にしてみます。

参考

Series

Flutter apps
  1. Flutter 3 の状態管理: アプローチ (概念)

Comments or feedbacks are welcomed and appreciated.