Google I/O の複数の動画が YouTube に上がっていて、たまたま見た一つが思いの外良い内容でした。
新しい情報は特にないですが、アダプティブ対応時に限らず役立つ基礎知識になるので、ウェブ検索などでたどり着く誰かのために箇条書きで残しておきます。
How to build Adaptive UI with Flutter - YouTube
この記事について
動画内の話のとおりには書いていません。
省略している部分があり、必要に応じて言い換えたり情報を少し補強したりもしています。
動画のタイトルにアダプティブ UI という言葉が含まれているのに記事がレスポンシブのことばかりなのは、動画がそうだったからです。
デバイスの種類に依らない UI / レイアウトのことがほとんどで、アダプティブの話は入力のことと折りたたみスマホのこと、および federated plugin に関する過去動画の紹介(これは本記事では省略)くらいでした。
公式ドキュメント
公式ドキュメントにも専用のページがあります。
この記事を書いている時点では動画と被った内容になっていますが、ドキュメントのほうが少し詳しいかもしれません。
SafeArea
- ネストしても大丈夫
- 子で指定されている padding が MediaQuery.removePadding() で除去されるようになっているため
- ゆえに、コンテンツを SafeArea に収めたいときに MediaQueryData の padding は直に使わずに SafeArea を使ったほうがいい
- アプリをアダプティブにしようとして widget の位置をあれこれ入れ替えたりしている間に SafeArea が複数になってしまってもおかしくならない
- 子で指定されている padding が MediaQuery.removePadding() で除去されるようになっているため
AppBar
- AppBar は SafeArea に収まるように考慮されている
- 自分で SafeArea で包まなくていい
- SafeArea を使うべき対象は Scaffold ではなく body
MediaQuery.sizeOf() と LayoutBuilder
MediaQuery.sizeOf()
- アプリのウインドウサイズが得られる
- デバイス/画面のサイズではない
- 一つの画面上に複数のアプリを並べて表示する場合などにウインドウサイズは画面サイズと異なるので、
sizeOf()
でウインドウサイズが得られることは重要
- そのサイズは、密度非依存な論理ピクセル(dp)
- デバイスが異なっても同じようなサイズに見えるピクセル
LayoutBuilder
- 親から渡された constraints が得られる
- ツリー上の特定箇所のサイズ情報を得ることができる
- 固定サイズである Size ではなく最小~最大サイズの範囲である BoxConstraints
- カスタムな widget を作るときに有用(BoxConstraints の最大サイズを利用する)
使い分け
- LayoutBuilder
- 局所的な constraints が必要なら LayoutBuilder
- 特定の widget に与えられるサイズに基づいたレイアウトなど
- 例)表示できる領域が横長の場合だけ横並びにする
- 特定の widget に与えられるサイズに基づいたレイアウトなど
- 局所的な constraints が必要なら LayoutBuilder
- MediaQuery.sizeOf()
- アプリ全体のウインドウサイズで widget を切り替える場合に適している
- ボトムナビとサイドナビの切り替えなど
- アプリ全体のウインドウサイズで widget を切り替える場合に適している
- Wrap、GridView を使うだけでいい場合もある
折りたたみスマホ
- Android で orientation を制限すると大画面互換性モードが有効になる
- レターボックスという中央に寄った表示(展開した状態の実際の幅より狭い表示)になる
MediaQuery.sizeOf()
で得られるウインドウサイズもレターボックスのサイズになる- レターボックスは portrait なので、
sizedOf()
で取得した値に基づいて orientation を固定すると portrait になってしまう
- 代わりに物理的な画面のサイズの取得が必要
- Flutter 3.13 で FlutterView に
display
が追加されてView.maybeOf(context)?.display
で取得できるようになった
- Flutter 3.13 で FlutterView に
- レターボックスという中央に寄った表示(展開した状態の実際の幅より狭い表示)になる
レスポンシブの具体例
紹介されていたいくつかの例の中で、ウインドウの幅が狭ければ全画面、広ければダイアログでコンテンツを表示する例が特に参考になりました。
自分で作るなら全画面表示は通常の画面として作りそうですが、動画ではどちらにも Dialog
+ showDialog()
を使っていました。
Dialog
と Dialog.fullscreen
を使い分ける分岐をして共通の子を渡すだけなので楽ですね。
showDialog(
context: context,
builder: (context) {
// コンテンツは共通
const dialogContent = DialogDetailScreen();
// ウインドウ幅600をブレイクポイントとする
final showFullScreenDialog = MediaQuery.sizeOf(context).width < 600;
if (showFullScreenDialog) {
// 幅が狭ければ全画面表示のダイアログ
return const Dialog.fullScreen(child: dialogContent);
} else {
// 幅が広ければモーダルダイアログ
return const Dialog(child: dialogContent);
}
},
);
アダプティブな入力
モバイルアプリを作るときにはタッチを唯一の入力方法と考えるかもしれませんが、Android のタブレットをサポートすることになったら入力方法を広げることも作業範囲に含まれます。
Android の Large screen app quality ガイドライン
- 三つの Tier がある
- Tier 1: Large screen differentiated
- Tier 2: Large screen optimized
- Tier 3: Large screen ready
- 最低限のレベル である Tier 3 でもマウスとスタイラスによる入力のサポートが必要
FocusableActionDetector
Shortcuts
、Actions
、Focus
、MouseRegion
の機能を含んでいるので、入力をアダプティブにする際に便利- Material のライブラリでよく使われている
留意点 / ベストプラクティス
- orientation はなるべく固定しない
- あらゆるサイズや形のデバイスをサポートすべき
- 縦向きに固定すれば MVP のスコープを狭めることはできるが、将来的にアダプティブにするときに大変
- マルチウインドウアプリのサポートは一般的になってきている
- widget を分割する
- アダプティブ にするときに widget が分割されていると共通利用しやすい
- 小さい const な widget をたくさん持つほうがパフォーマンスの面で良い
- 大規模で複雑なアプリでリビルドの回数を抑えることができて効果がある
- 小さいほうが widget が複雑にならない
- 読みやすい
- リファクタリングしやすい
- おかしな挙動になりにくい
- 分割の基準
- ない
- widget の基準を doc コメントとして書き、そのドキュメントの複雑さで判断するといい
- 例えば条件によって widget の挙動が変わる場合、それをドキュメントに書いてごちゃごちゃするなら widget を分けたほうがいい
- レイアウトの決定をデバイスの種類に頼らない
- アプリのサイズは画面サイズと同じとは限らない
- 例
- デスクトップでのウインドウをリサイズする
- タブレットでアプリを並べて使う
- マルチウインドウモード
- picture-on-picture
- 例
- アプリのサイズは画面サイズと同じとは限らない
- MediaQueryData.orientation、OrientationBuilder にも頼らない
- アプリのウインドウが持つサイズは画面の向きではわからない
- 3 レイアウトが出発点として良い
- compact(~600)、medium(600~840)、expanded(840~)
- 三つでなければならないわけではない
- ブレイクポイントは幅でなくてもいい
- アプリのウィンドウサイズとピクセルratioを組み合わせてもいい
- ゲームではもっと大きなブレイクポイントがいいかもしれない