FlutterでResponsive layout gridに対応したい
Flutter 2でWebやDesktopアプリのサポートがstableになり、気になってくるのがRespoinsive Layoutです。AndroidやiOSでは、タブレットの対応を縦や横の画面固定にしていたケースもあったと思います。しかし、WebやDesktopではアプリのサイズはより頻繁に変更されることが想定されます。
公式ドキュメントのCreating responsive and adaptive appsでは、Youtubeの動画付きで実装例が掲載されています。ぜひ確認してみてください。
さて、公式ドキュメントのCreating a responsive Flutter appの段を引用します。
Creating a responsive Flutter app
Flutter allows you to create apps that self-adapt to the device’s screen size and orientation.
There are two basic approaches to creating Flutter apps with responsive design:
LayoutBuilder class
Use theFrom its builder property, you get a BoxConstraints object. Examine the constraint’s properties to decide what to display. For example, if your maxWidth is greater than your width breakpoint, return a Scaffold object with a row that has a list on the left. If it’s narrower, return a Scaffold object with a drawer containing that list. You can also adjust your display based on the device’s height, the aspect ratio, or some other property. When the constraints change (for example, the user rotates the phone, or puts your app into a tile UI in Nougat), the build function runs.
MediaQuery.of() method in your build functions
Use theThis gives you the size, orientation, etc, of your current app. This is more useful if you want to make decisions based on the complete context rather than on just the size of your particular widget. Again, if you use this, then your build function automatically runs if the user somehow changes the app’s size.
このように LayoutBuilder
か MediaQuery
を利用し、画面のリビルドを利用して対応することが期待されています。
では、画面の横幅に応じて"どのような"レイアウトを考えていけば良いでしょうか? もしもアプリがマテリアルデザインを採用している場合、マテリアルデザインが提唱しているレスポンシブ対応に乗っかると良さそうです。
コードでレイアウトを組んでいく際、マテリアルデザインのレスポンシブ対応に乗っかろうとすると「マジックナンバー」が発生してしまいます。というのも、マテリアルデザインでは dp
の横幅でデザインのベースが切り替わるため、LayoutBuilder
や MediaQuery
で取得した横幅に対して分岐を書かなければならないためです。
しかしながら、画面ごとに画面の幅の定義を書くのは避けたい話です。さらにいえば、アプリごとに定義を書くのも避けたい話です。そんなわけで breakpoints
のライブラリをアップロードしました。
リポジトリはこちら。MITライセンスです。
なお、flutter community製のライブラリもあります。[1]
個人的に BreakpointBuilder
のようなBuilderは不要だなと感じる[2]のですが、本質的には買わない + 安心のflutter community製なので、お好きな方を利用するのが良いと思います。
breakpoints_mq
では画面サイズをそのまま扱うためのenum(BreakpointRange
)と、xsmallからxlargeまで5段階に区切ったenum(BreakpointWindow
)を提供します。
また、画面のOrientationを踏まえた上で端末のパターンを提供するenum(BreakpointDevice
)も追加しました。とはいえ、Orientationの判定は画面サイズの縦と横のどちらが長いかを見ているだけなので、参考程度の利用になるかなと思っています。
enumの一文字目に数字を利用できないため、 lessThan360
のようなenumにしてしまいました。こちら、より英語的に良い表現がわかる方がいれば、PRを出していただけるととても嬉しいです😁。
breakpointsの使い方は、各ライブラリのサンプルプロジェクトが参考になります。breakpoints_mq
はWeb対応もしてあるので、プロジェクトをクローンしたらそのまま試せるようにしておきました。
タブレットでは画面回転時に遅れていた描画が、Webではすぐに反映される印象があります。
このため、WebやDesktopへの展開を考えているのであれば、早いタイミングで画面のレスポンシブ対応を進めておいた方が良さそうです。
ささっとコードを見たい人向け
import 'package:flutter/material.dart';
import 'package:breakpoints_mq/breakpoints_mq.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final data = MediaQuery.of(context);
final breakpoint = data.breakpoint; <- ここで取得
return Scaffold(
appBar: AppBar(
title: const Text('Breakpoints Demo'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Text('Screen Size: ${data.size.toString()}'),
),
Padding(
padding: const EdgeInsets.all(8),
child: Text('Breakpoint: ${breakpoint.toString()}'),
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: breakpoint.margins / 2), <- breakpointsに記載されていたmargins
child: GridView.count(
crossAxisCount: breakpoint.columns, <- breakpointsに記載されているグリッドの数
children: List.generate(
100,
(index) => Padding(
padding: EdgeInsets.all(breakpoint.gutters / 2), <- breakpointsに記載されていたgutters
child: Card(
child: Center(
child: Text('No.${index + 1}'),
),
),
),
),
),
),
)
],
));
}
}
なお、マテリアルデザインのiOS breakpointsはどう実装すればいいのか、判断に困るものになっています。Flutterアプリの場合はdevice_infoから取得した utsname.machine
を色々と頑張って判定しなければならないため、あまり深堀しない方が良いのではないかなと。
来年あたりには、この辺りの知見もいろいろなところで共有されるようになるのではないでしょうか。楽しみです。
2022.03.20追記
Flutter Webのような広い画面に対応する場合、画面を横に分割するケースが存在します。
そのようなケースでは、MediaQuery.of(context)
による「画面サイズ」の取得ではなく、LayoutBuilder.build
によるBoxConstraints
の取得が適するようになります。
このため、BoxConstraints
からBreakpoint
を取得するextensionを追加しました。
また、たいていのケースでは「Breakpointに応じたPaddingを、要素の親に追加する」処理になる妥当と思ったので、BreakpointWidget
というクラスを追加しました。
こちらはBreakpointWidget
のchild
に指定したWidgetに対して、Widgetの横幅から算出したPaddingを自動的に追加するものとなります。よければご利用ください。
Discussion
2021年5月13日に色々とMaterial Designが更新されました。
このため、破壊的な変更を反映してv2系としてリリースしました。