Flutterはなぜ全てがWidgetなのか
はじめに
Flutterを使い始めると、UIは「全てがWidget」ということを叩き込まれます。
パーツの配置方向やパディングまでUIのプロパティではなく、UIの1要素として扱います。
はじめのうちは「そういうもんなんや」と思っていましたが、「そもそも何で?」「そのせいでCenter()とかPadding()とかがあるだけでコードのネスト深くなって見にくいんだが?」と思い始めました。
そこで色々調べ、考えてみたところ、自分なりに納得いく結論が出たのでまとめておきたいと思います。
なお、僕はFlutterの開発者でもなんでもなく、これから書くことはあくまで僕の予想に過ぎない、ということに注意してください。
背景 MVCモデルの問題点
グラフィカルなアプリケーションにおいては、あるUIの操作による状態の変化が他のUIの見た目を変化させます。このような相互作用を記述するためによく使われるのがMVC構造です。M(内部状態)とV(UIの見た目)をC(コントローラ)が媒介します。
MVCには問題点があります。それは
- UIに対する操作が「生成」と「変更」の2種類ある
- これらがごっちゃになる
ということです。
例えばあるボタンは最初表示した場合は無効である条件を満たすと有効になって押せるようになる、という場合に
- ボタン生成処理(無効状態で生成)
- ボタン状態変更処理(条件をチェックし、満たしていれば有効に)
が各々できる、といった具合です。
こうなると、画面が複雑になりコードが大きくなっていった場合、予期せぬところでUIが更新されてしまった、ということになりがちです。
Flutterの取った解決策
そこでFlutterが取った解決策は
UIは生成のみとする
というものです。
「変更」をなくし、UIに影響を与えるコードは1か所に集約するということですね。
widgetはimmutableであり、外観を変更するだけの場合でもbuild()で生成し直すのはこのためです。
生成のみとした場合の課題
ただし、その場合別の問題が生じます。それは
UIを毎回生成し直すのは非効率
ということです。
例えばボタンのパディングが変わっただけでテキストも含めて再生成することになるのです。
さらにページ単位で見ても、ボタンの1プロパティが変わっただけでページ全体をリビルドし、変更していないUIまで再描画することになります。
(だからこそ従来は「変更」メソッドが別にあったわけです)
なぜ全てがWidgetなのか
これに対する対策が、
じゃあ変わったところだけ再描画すればいいじゃんです
処理前後で各パーツを比較し、変わった部分だけ再描画、をしているのです。
ただしこれも一筋縄ではいきません。以下のような課題があります。
- 再描画の単位が1UIだと無意味
- そもそも毎回全プロパティを比較するだけでも非効率
これへの解決策が「全てをWidgetとする」なのです。
以下詳しく見ていきます。
まずは1について。
ボタンのパディングが変わった場合、「ボタンが変わった」と捉えてボタン全体を再描画するのでは先に挙げた問題点そのままです。
「ボタンのパディングが変わった」と捉えてパディングだけを変えたいのです。
これを解決するために、パディングなどまで1つのパーツとして定義し、「UIツリーの1要素」としているのです。つまり「全てをWidgetとする」のです。
こうすれば、パディング同士を比較し、変わっていたらパディングだけ再描画、ということができるようになるのです。
次に2について。
いくら変わった部分だけを再描画といっても、そもそも個々のプロパティが「変わったか否か」を毎回チェックするだけでもかなり非効率です。
このため、変わる可能性のないプロパティは端から比較しない、ということをしたいです。
しかし、比較の最小単位が「ボタン」だとそこまで効率化できません。
例えば、あるボタンは色は変わらないがパディングは変わり得る、としましょう。
このような場合でも内部のプロパティはすべて比較する羽目になります。
たとえ色は変わらないとしても、パディングが変わる可能性があるので「ボタン」は変わり得るのです。
一方、パディングと色が別々の要素の場合、色が変わらないことが分かっているなら端から色は比較しない、ということができるわけです。
最後に
まあこれを知っていたところで動作の速いアプリが作れるようになるとかではないと思いますが、自分が使っているツールについて疑問点をちゃんと調べるのは重要かなと思い調べてみました。
参考文献
Discussion