🪣

FlutterのWidgetツリーでのバケツリレーの考察

2024/02/19に公開

Flutter アプリの Widget 設計において、しばしば遭遇する問題の一つが状態管理の「バケツリレー」です。この記事では、バケツリレーの問題を解消するための方法を考察します。

バケツリレーとは?

バケツリレーは一連の Widget が親から子にパラメータを渡す行為を指します。

Flutter では、このバケツリレーは主に二つの形で現れます。

  1. 親 Widget が子 Widget に状態を直接渡す場合:この場合、状態は親から子へと「手渡される」。このような状態の受け渡しは、有用で簡単な方法だが、Widget の階層が深くなったり複数の Widget がその状態を必要とする場合は問題となる。
  2. 親 Widget がコールバックを子 Widget に渡し、子 Widget がそのコールバックを呼び出すことで自身の状態を親に伝える場合:子 Widget は自身の状態を親 Widget に伝えることができる。しかし、Widget の階層が深くなったり、複数の Widget が同じ状態を管理する必要がある場合には、複雑になる可能性がある。

これらのバケツリレーは中規模から大規模な Flutter アプリケーションでは複雑さ増加させる可能性があります。このような問題を解決するために,複数の状態管理ソリューション(例えば Provider, Riverpod, Redux など)が提供されています.しかし、そうしたライブラリに頼る前に Widget の設計を見直すことでこの問題を軽減できる可能性があります。

避けるべきバケツリレーと避けなくても良いバケツリレー

バケツリレーは通常、コードの可読性と保守性を下げるため、一般的には避けるべきです。しかし、全てのバケツリレーが必ずしも悪いとは限りません。

避けるべきバケツリレー

  • 不必要に深いウィジェットツリーでのバケツリレー:深く入れ子になった Widget ツリーで複数のレベルにわたって状態が引き渡される場合、その状態は簡単に追跡、管理が困難になります。これはコードの理解を難しくし、バグを生み出す可能性があります。このようなバケツリレーは避けるべきです。

避けなくても良いバケツリレー

  • 浅いウィジェットツリーでのバケツリレー:直接の子 Widget に状態を引き渡す程度のバケツリレーは必ずしも避ける必要はありません。この場合、状態の削除や追加は直感的で、コードの複雑性は低いままに保てます。
  • 一貫性のあるバケツリレレー:一部の状態が特定の Widget サブツリーを通じて一貫して流れる場合、そのバケツリレーは避ける必要がありません。これは、一貫性のあるパターンとして予測可能で、コードの可読性や保守性を維持します。

避けるべきバケツリレーを避けるためには、以下の点に注目します。

単一責任原則と状態管理

単一責任原則(Single Responsibility Principle)は、コンポーネントやモジュールの設計原則の一つです。この原則は、一つのモジュールは「一つだけの役割に責任を持つべきである」というものです。これを Flutter の Widget 設計に適用すると、各 Widget はその必要とする状態のみを保持し、それ以外の情報は持つべきではないという指針になります。

単一責任原則に違反した設計では、ある Widget が別の Widget の状態を持つ必要があります。これにより、データが Widget のツリーを通過する際にバケツリレーが発生します。つまり、状態は上位のコンポーネントから下位のコンポーネントに一つずつ手渡され、それぞれのコンポーネントが状態を持つ必要があるため、管理が困難になることがあります。
したがって、単一責任原則を遵守すると、各コンポーネントや関数はそれぞれ一つの責任を持つので、それぞれの役割が明確になり、管理可能性が向上します。また、特定の状態を必要とするコンポーネントに対して直接その状態を渡すことができるため、バケツリレーの発生を防ぐことができます。その結果、データの流れがより直接的で予測可能になり、状態の管理が簡易化されます。

Atomic デザインを参考にすると単純な Widget ツリー構造は Page→Organisms→Molecules→Atoms のようになります。この場合は Organisms が外部状態を保持して Molecules とその子である Atom に値が渡っていきます。この程度であれば値を直接渡すことは問題ありませんが、状態が複雑になってくるとバケツリレーが発生しやすくなります。特に Molecules が Molecules を子に持つ場合は Organisms→Molecules→Molecules という複雑な状態の流れになるので、中間の Molecules が単一責任原則に違反する可能性が高くなります。この場合は例えば Organisms が孫 Molecules を作成して子 Molecules に渡す設計にできる可能性があります。

内部状態と外部状態

プログラミングにおける状態管理は、アプリケーションの行動や振舞いを制御します。これは通常「内部状態」と「外部状態」という 2 つに分けられます。

内部状態は、特定のコンポーネントまたはクラスの内部に閉じている状態です。内部状態は通常、そのコンポーネントがどう振る舞うべきか、またはある時点で何を表示するべきかを決定するために使用されます。また、それはコンポーネントのライフサイクルに紐付いており、そのコンポーネントがマウントされた時に初期化され、コンポーネントがアンマウントされたときに消去されます。内部状態は一般的に、コンポーネントの特定の機能や振る舞いを制御するときに使用されます。この状態はコンポーネント自体によって管理され、通常はそのコンポーネントだけがその状態を変更できます。

一方、外部状態は、通常、複数のコンポーネントや部分にわたるアプリケーション全体の振る舞いを制御します。外部状態は通常、全体的なアプリケーションの状態を持つための状態管理の仕組み(Provider や Riverpod など)を介して管理されます。外部状態は、アプリケーション全体に影響を与えるデータ(例えば、ユーザー情報、アプリケーションの設定、共有するリソースなど)を管理するのに最適であり、それにより必要なすべてのコンポーネントがその状態にアクセスし、変更することができます。外部状態は状態管理ライブラリで一元的に管理され、そのストアはアプリケーションのいずれかの部分で発生する可能性がある状態の変更をすべて処理します。

内部状態と外部状態を適切に分けることで、このバケツリレーを軽減することができます。

  • 内部状態: 内部状態は一般的にそのウィジェット自体が参照・操作するもので、他のウィジェットに影響を与えることはありません。したがって、この状態はそのウィジェット内部で完結し、他のウィジェットに手渡す必要がありません。これによりウィジェットツリー上での不必要な状態の引き渡し(バケツリレー)を防ぎます。
  • 外部状態: 外部状態はアプリケーション全体で共有される情報であり、複数のウィジェットが参照・操作する可能性があります。この情報は一元的なストア(状態管理ライブラリ)で管理され、必要なウィジェットは直接ストアから状態を取得します。ウィジェットは親から子へ逐次的に状態を引き渡す必要がないため、バケツリレーが軽減されます。

まとめ

バケツリレーは、Widget が状態を子 Widget に引き渡す過程となり、これが繰り返されると、Widget 間のデータの流れが複雑化し、可読性や保守性が低下します。この問題を解決するための一つの方法は状態管理ライブラリの利用ですが、その前にもっと基礎的な解決策として設計を考察することが重要です。

Widget 自体の設計と Widget ツリーの設計を適切に行うことで、バケツリレーの問題を大幅に軽減することができます。具体的には、単一責任原則に基づき、各ウィジェットは一つの責任だけを持つように設計し、内部状態と外部状態を分けるように設計します。

その上で、アプリケーションの規模が大きくなり、より複雑な状態管理が必要になった場合、状態管理ライブラリによるバケツリレーの解消を検討すると良いです。

結論として、ウィジェットとそのツリーの設計が最初の重要なステップであり、その後に状態管理ライブラリを効果的に利用することで、よりスムーズで効率的な Flutter アプリ開発を進めることができます。これらの手法を適切に組み合わせることで、バケツリレーの問題を解決し、コードの可読性、保守性、効率性を向上させることが可能となります。

合同会社CAPH TECH

Discussion