💻

FlutterでResponsive layout gridに対応したい

2021/05/03に公開
1

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:

Use the LayoutBuilder class

From 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.

Use the MediaQuery.of() method in your build functions

This 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.

このように LayoutBuilderMediaQuery を利用し、画面のリビルドを利用して対応することが期待されています。
では、画面の横幅に応じて"どのような"レイアウトを考えていけば良いでしょうか? もしもアプリがマテリアルデザインを採用している場合、マテリアルデザインが提唱しているレスポンシブ対応に乗っかると良さそうです。

https://material.io/design/layout/responsive-layout-grid.html


コードでレイアウトを組んでいく際、マテリアルデザインのレスポンシブ対応に乗っかろうとすると「マジックナンバー」が発生してしまいます。というのも、マテリアルデザインでは dp の横幅でデザインのベースが切り替わるため、LayoutBuilderMediaQuery で取得した横幅に対して分岐を書かなければならないためです。
しかしながら、画面ごとに画面の幅の定義を書くのは避けたい話です。さらにいえば、アプリごとに定義を書くのも避けたい話です。そんなわけで breakpoints のライブラリをアップロードしました。

https://pub.dev/packages/breakpoints_mq

リポジトリはこちら。MITライセンスです。

https://github.com/koji-1009/breakpoints_mq

なお、flutter community製のライブラリもあります。[1]
個人的に BreakpointBuilder のようなBuilderは不要だなと感じる[2]のですが、本質的には買わない + 安心のflutter community製なので、お好きな方を利用するのが良いと思います。

https://pub.dev/packages/breakpoint


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というクラスを追加しました。
こちらはBreakpointWidgetchildに指定したWidgetに対して、Widgetの横幅から算出したPaddingを自動的に追加するものとなります。よければご利用ください。

脚注
  1. そのほかにも色々とありますが、殆どメンテナンスされていません ↩︎

  2. 実装が過剰だったり冗長だなと思ったのが、今回ライブラリを新たに作成した主な理由です ↩︎

GitHubで編集を提案

Discussion