【Flutter】簡単なレスポンシブ対応

レスポンシブ対応について悩んでいる方も多い方思います。
私もレスポンシブ対応を調べましたが、これだ!ってなるものがなかなか見つからなかったので色々試してみた結果、一番簡単に見た目をきれいにできる方法を思いついたので共有していきます。

##簡潔なレスポンシブ対応 MediaQuery
MediaQueryを使用すると現在使用しているデバイスのSizeを取得することができます。
以下に使用例を記載します。

```
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class ResponsiveTest extends StatefulWidget {
  const ResponsiveTest({Key? key}) : super(key: key);

  @override
  State<ResponsiveTest> createState() => _ResponsiveTestState();
}

class _ResponsiveTestState extends State<ResponsiveTest> {
  @override
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    return Scaffold(
      appBar: AppBar(
        title: Text('レスポンシブテスト'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            Container(
              width: size.width * 0.8,
              height: size.height * 0.2,
              color: Colors.red,
              child: Center(
                child: Text('レスポンシブテスト画面',
                    style: TextStyle(fontSize: 30, color: Colors.white)),
              ),
            )
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}


```

この　final Size size = MediaQuery.of(context).size; を使用するとsize.width(横幅)やsize.height(縦幅)を取得でき、
その後 size.width * 0.8 といった形で画面の横幅の8割の大きさを指定できます。
しかし文字の大きさが合わず自動で折り返してしまってますね...

![Screenshot_1641994695.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2501119/e73b96fa-1f7e-a6c6-2085-b2d67fb2d1f3.png)

##FittedBox
そこで出てくるのがFittedBoxです！ 先ほどのTextwidgetをFittedBoxで囲ってあげると文字がはみ出すとき自動で一行になるように調整してくれます。

```
FittedBox(
     child: Text('レスポンシブテスト画面',
          style: TextStyle(fontSize: 30, color: Colors.white)),
  ),
```
![Screenshot_1641995404.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2501119/6da13565-ca29-ba3a-9945-c629db13bcb5.png)

##flutter_screenutil
次に【flutter_screenutil】というパッケージを紹介します。
このパッケージはビルドの最初に基準となるデバイスの幅と高さを入力しておくことでそこからの差分を補ってくれるというものです。
[flutter_screenutil](https://pub.dev/packages/flutter_screenutil)

まずパッケージをインポートします。ターミナルで以下のコマンド

```
flutter pub add flutter_screenutil
```

####使い方

画面全体で使用したいのでrunAppに現在開発の基準にしている端末の横・縦の大きさを記述しておきます。

```
runApp(ScreenUtilInit(designSize: Size(392, 759), builder: () => MyApp()));
```
次にwidthなら「.w」 heightなら「.h」 fontSizeなら「.sp」を数字の後につけることで比率を保ってくれます。
先ほどの文章を例にすると以下のような感じです。
※文字の大きさを「.sp」で合わせようとするとどうしても予想よりも文字が「大きくなりすぎた」となるときがあります。
そういう時は「.h」の倍率が低いので文字の大きさに.hをつけると少し大きくなった程度で解決できます。もしくは先述した「FittedBox」を用いれば親要素より大きくなることはありません。

```
import 'package:flutter_screenutil/flutter_screenutil.dart';
.
.
.

 Container(
              width: 300.w,
              height: 200.h,
              color: Colors.red,
              child: Center(
                child: FittedBox(
                  child: Text('レスポンシブテスト画面',
                      style: TextStyle(fontSize: 30.sp, color: Colors.white)),
                ),
              ),
            )
```

##ここからが本題...アスペクト比による画面崩れ
どんなに比率を合わせても画面がきれいに収まることはありません。それはなぜか...
端末によってアスペクト比率が違うからです！

アスペクト比とは...
アスペクト比とは、画面や画像の縦と横の長さ（画素数）の比。一般的に「横：縦」と表記し、640×480ピクセルを4：3と表記します。

ここで問題になってくるのが例えば以下のようにiphone11はアスペクト比が「9:19.5」とスリムな形をしていますが、ipad12.9インチになると「3:4」と正方形に近い形になっているのがわかると思います。

* iphone11 「828×1792」(9:19.5)
* ipad12.9インチ「2048×2732] (3:4)

つまり、比率で合わせると画面のパーツは横長になっていくということです！
以下の画像をご覧ください。
![Screenshot_1641998895.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2501119/bc180f9d-93d8-4bbf-e78a-95c5d2cd886c.png)
![Screenshot_1641999024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2501119/d012e786-429d-48d8-721a-3d5221021811.png)

基準としていた端末ではきれいに収まっているのにサイズを大きくしたら縦ははみ出してしまい横のパーツに至っては形が崩れて横長になっていますね。

###アスペクト比の取得

アスペクト比の取得は以下のコードで行います。

```
final Size size = MediaQuery.of(context).size;
double aspectRatio = size.aspectRatio;
```
ではアスペクト比を取得して何をしたいのか...
アスペクト比が1に近いほど正方形に近いということなのでアスペクト比で場合分けしてそもそものwidthをスリムにしてしまおうということです。具体的には以下のようになります。

```
final Size size = MediaQuery.of(context).size;
    double width;
    double height = size.height - AppBar().preferredSize.height;
    double aspectRatio = size.aspectRatio;
    if (aspectRatio < 0.5) {
      width = size.width;
    } else if (aspectRatio < 0.65) {
      width = size.width * 0.9;
    } else if (aspectRatio >= 0.65) {
      width = size.width * 0.8;
    } else {
      width = size.width;
    }
```
アスペクト比が大きくなるたびに元のwidthに0.8や0.9などをかけてスリムにしているということです。
ちなみにheightに関しましてもAppBarの高さを引いた高さ使用しています。画像で言ったら青い部分ですね。
全体のコードは以下の通りです。

```
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

void main() {
  runApp(ScreenUtilInit(designSize: Size(392, 759), builder: () => MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ResponsiveTest(),
    );
  }
}

class ResponsiveTest extends StatefulWidget {
  const ResponsiveTest({Key? key}) : super(key: key);

  @override
  State<ResponsiveTest> createState() => _ResponsiveTestState();
}

class _ResponsiveTestState extends State<ResponsiveTest> {
  @override
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    double width;
    double height = size.height - AppBar().preferredSize.height;
    double aspectRatio = size.aspectRatio;
    if (aspectRatio < 0.5) {
      width = size.width;
    } else if (aspectRatio < 0.65) {
      width = size.width * 0.9;
    } else if (aspectRatio >= 0.65) {
      width = size.width * 0.8;
    } else {
      width = size.width;
    }
    return Scaffold(
      appBar: AppBar(
        title: Text('レスポンシブテスト'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            Container(
              width: width * 0.8,
              height: height * 0.2,
              color: Colors.red,
              child: const Center(
                child: FittedBox(
                  child: Text('レスポンシブテスト画面',
                      style: TextStyle(fontSize: 30, color: Colors.white)),
                ),
              ),
            ),
            SizedBox(height: height * 0.03),
            SizedBox(
              width: width * 0.7,
              height: height * 0.06,
              child: ElevatedButton(
                  onPressed: () {},
                  style: ElevatedButton.styleFrom(
                    primary: Colors.black, //ボタンの背景色
                  ),
                  child: const Text('ボタン')),
            ),
            SizedBox(height: height * 0.04),
            Container(
                width: width * 0.9, height: height * 0.3, color: Colors.yellow),
            SizedBox(height: height * 0.03),
            SizedBox(
              width: width * 0.9,
              child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    Container(
                        width: width * 0.4,
                        height: width * 0.4,
                        color: Colors.pinkAccent),
                    Container(
                        width: width * 0.4,
                        height: width * 0.4,
                        color: Colors.deepOrangeAccent)
                  ]),
            )
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

```

どうしても画面の横幅すべてほしいときは「size.width」を状況に応じて使用すればいいですしまた「double.infinity」でパーツを最大にできます。
※個人的にはheightで「double.infinity」は使用しないほうがいいです。Appbarの高さやAndroidのデフォルトでついている下のバーを含めて最大の高さなので何かと画面崩れが起きます。
結果が以下の画像になります。少し基準にしてた端末に余白ができてしまいましたが一応見た目の崩れは気にならない程度ではないでしょうか。
もっと突き詰めればAndroidの下についているバーを高さから引いたりすればより改善されると思います。
![Screenshot_1642000747.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2501119/fbd1f5d3-176b-a0e7-e903-1c0e1d3a023e.png)
![Screenshot_1642000583.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2501119/a3503dd6-b25f-9807-afec-134a8dd2662f.png)

今回は記事を読んでいただきありがとうございました。もっと突き詰めれば「LayoutBuilder」を使用して画面の大きさによってそもそものレイアウトを変更する方法があります。横画面にしたりipadのときだけレイアウトが違うという形ですね。今回は「簡単な」レスポンシブ対応なのでそこまでこだわりませんでした。皆様のお役に立てれば幸いです。