7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Flutter】スマホとタブレットのレスポンシブ対応

Last updated at Posted at 2023-11-15

はじめに

スマホとタブレットに対応するためのレスポンシブ対応にトライしてみたいと思います

対応前

対応前のコードは下記にあります。
https://github.com/AppDeveloperMLLB/responsive_app_sample/tree/feat/support_orientation

上記は、【Flutter】画面回転に対応するで実装したコードになります。
スマートフォンで縦横の画面に対応しましたが、タブレットで表示すると以下のような表示になります。

一覧画面

タブレット縦
Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-09 at 21.16.05.png
タブレット横
Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-09 at 21.16.12.png

詳細画面

タブレット縦
Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-09 at 21.16.23.png
タブレット横
Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-09 at 21.16.17.png

スマホだと違和感のない表示でしたが、タブレットだと以下が気になりました。

  • 文字が小さい
  • 余白が目立つ
  • 横長になりすぎる箇所がある

それぞれ対応していきたいと思います。

タブレット対応

上記の気になる点を踏まえ、以下の対応をしていきます。

  • 文字の大きさの対応
  • レイアウトの対応

文字の大きさ対応

いくつかの方法があるみたいです。

参考記事

上記の記事を参考にすると以下の方法がありました。

  • auto_size_textパッケージを使う
  • FittedBox
  • LayoutBuilderを使う
  • MediaQueryを使う
  • flutter_screenutilパッケージを使う

個人的に良さそうだと思ったMediaQueryを使う方法flutter_screenutilパッケージを使う方法について説明したいと思います。

MediaQuery

MediaQueryを使う方法は、表示領域の割合でフォントサイズを決める方法です。
以下のような形で、画面の幅に応じてフォントを決めるという方法です。

double screenWidth = MediaQuery.of(context).size.width;
double large = screenWidth * 0.06;
double medium = screenWidth * 0.04;
double small = screenWidth * 0.02;

final largeText = Text("large", style: TextStyle(fontSize: large),);
final mediumText = Text("medium", style: TextStyle(fontSize: medium),);
final smallText = Text("small", style: TextStyle(fontSize: small),);

Listの文字にmediumを設定すると下記のようになりました。

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-10 at 21.22.41.png

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-10 at 21.22.53.png

スマートフォンでもレイアウト崩れはなかったです。

Simulator Screenshot - iPhone 15 - 2023-11-10 at 21.35.18.png

こちらを取り入れる際は、コード例のように、デザイン時点でテキストの大きさの種類を定義して、画面幅に対してそれぞれいくつにするかを決めておく必要がありそうです。

flutter_screenutil

flutter_screenutilを導入して、下記のようにするとレスポンシブ対応ができます。

return ScreenUtilInit(
  // 基準となるデバイスサイズ
  designSize: const Size(360, 690),
  // デバイスの解像度に合わせて文字サイズが小さくなりすぎることを防ぐ
  minTextAdapt: true,
  // スプリットスクリーンーモードに対応するか
  splitScreenMode: true,
  builder: (context, child) {
    return child!;
  },
  child: MaterialApp(
    title: 'Responsive App Sample',
    theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      useMaterial3: false,
    ),
    home: const ListPage(),
  ),
);

使うときは以下のように、フォントサイズの指定時に.spをつけるだけです。

Text(
  "text",
  style: TextStyle(
    fontSize: 20.sp,
  ),
)

Simulator Screenshot - iPhone 15 - 2023-11-10 at 21.40.53.png

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-10 at 21.49.11.png

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-10 at 21.49.07.png

このパッケージの利点は、高さと幅を指定もレスポンシブ対応できることです。
フォントと同じく、高さには.h、幅には.wをつけると、デバイスの解像度に合わせて調整してくれます。

SizedBox(
  width: 100.w
  height: 100.h
)

このパッケージを使うとGoldenTestを実装する際に注意が必要みたいです。

レイアウトの対応

レイアウトの対応の対策は以下の二つがあると思います。

  • 余白や大きさをレスポンシブに対応する
  • デザインをスマホとタブレットで変える

余白や大きさをレスポンシブに対応する

スマートフォンように大きさや余白を調整しているので、下の画像のように余白の大小があったり、Widgetのサイズが小さすぎたりします。

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-09 at 21.16.23.png

ユーザーの画像を表示する紫の円とその横の名前のコードは下記になっています。

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          decoration: BoxDecoration(
            color: Colors.deepPurple.shade300,
            shape: BoxShape.circle,
          ),
          width: 72,
          height: 72,
          child: const Center(
            child: Text(
              "Image",
              style: TextStyle(
                fontWeight: FontWeight.w500,
                color: Colors.white,
              ),
            ),
          ),
        ),
        const SizedBox(width: 16),
        Flexible(
          child: Text(
            name,
            style: Theme.of(context).textTheme.headlineLarge,
          ),
        ),
      ],
    );
  }

余白や大きさをレスポンシブに対応する方法も二つありますので以下に記載します。

割合で対応

レスポンシブな対応をする場合、width: 72height: 72のような数値による指定はレイアウト崩れの原因になります。
なので、割合で指定するように変更しました。

@override
  Widget build(BuildContext context) {
    // スマートフォンで小さくなりすぎる場合があったので、72を最小値としている
    final imageWidth = max(MediaQuery.of(context).size.width * 0.1, 72.0);
    final padding = MediaQuery.of(context).size.width * 0.025;
    // スマートフォンで小さくなりすぎる場合があったので、14を最小値としている
    final fontSize = max(MediaQuery.of(context).size.width * 0.02, 14.0);
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          decoration: BoxDecoration(
            color: Colors.deepPurple.shade300,
            shape: BoxShape.circle,
          ),
          width: imageWidth,
          height: imageWidth,
          child: Center(
            child: Text(
              "Image",
              style: TextStyle(
                fontWeight: FontWeight.w500,
                color: Colors.white,
                fontSize: fontSize,
              ),
            ),
          ),
        ),
        SizedBox(
          width: padding,
        ),
        Flexible(
          child: Text(
            name,
            style: Theme.of(context).textTheme.headlineLarge,
          ),
        ),
      ],
    );
  }

スマホ
Simulator Screenshot - iPhone 15 - 2023-11-11 at 11.55.37.png

Simulator Screenshot - iPhone 15 - 2023-11-11 at 11.55.42.png

タブレット
Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-11 at 08.58.55.png

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-11 at 08.58.59.png

これでユーザーの画像と名前を表示する部分に関しては、調整ができました。
(ID、email、ageの部分はまだ変わってません)

また今回は使用しませんでしたが、別の対応としては、Flexible(またはExpanded)のflexで指定するという方法もありかと思います。

flutter_screenutilで対応

flutter_screenutilは、フォントサイズだけでなく、高さと幅に対するレスポンシブ対応も可能です。
高さには.hをつけて、幅には.wをつければレスポンシブに対応できます。

@override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          decoration: BoxDecoration(
            color: Colors.deepPurple.shade300,
            shape: BoxShape.circle,
          ),
          width: 72.w,
          // 高さの場合本来は72.hとするが、同じ大きさとしたいので72.wとしている
          height: 72.w,
          child: Center(
            child: Text(
              "Image",
              style: TextStyle(
                fontWeight: FontWeight.w500,
                color: Colors.white,
                fontSize: 16.sp,
              ),
            ),
          ),
        ),
        SizedBox(
          // 横幅の場合は.w, 高さの場合は.hをつける
          width: 16.w,
        ),
        Flexible(
          child: Text(
            name,
            style: Theme.of(context).textTheme.headlineLarge,
          ),
        ),
      ],
    );
  }

スマホ
Simulator Screen Shot - iPhone 14 Pro Max - 2023-11-14 at 15.28.41.png

Simulator Screen Shot - iPhone 14 Pro Max - 2023-11-14 at 15.28.52.png

タブレット
Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-14 at 15.51.40.png

Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-11-14 at 15.51.36.png

デザインをスマホとタブレットで変える

スマホとタブレットでデザインを別にする対応もレスポンシブの対策として有効かと思います。

今回のデザインは以下で実装します。
スマホ : リスト画面 → 詳細画面の二画面の実装

s.gif

タブレット : 左側にリスト、右側に詳細表示の一画面の実装

th.gif

以下の記事を参考にしました。

タブレットかスマホかの判定

タブレットかスマホかの判定は横幅かアスペクト比で判定するみたいです。

iOSとAndoridのタブレットの大きさを調べると下記のようになっていたので、横幅が600以上ならタブレットと判定して問題なさそうでした。

device size aspect
iPad Pro 12.9-inch (2nd generation) 1024 x 1366 0.74
iPad Pro 10.5-inch 1112 x 834 1.33
iPad Pro (12.9-inch) 1024 x 1366 0.74
iPad Pro (9.7-inch) 768 x 1024 0.75
iPad Air 2 768 x 1024 0.75
iPad Mini 4 768 x 1024 0.75
7インチタブレット 600x1024 0.58
10インチタブレット 720x1280 0.56
10インチタブレット 800x1280 0.625

下記が参考記事です。
iOSのデバイスごとのサイズ
Androidのデバイスごとのサイズ
上記の記事内の引用

他の最小幅の値と、一般的な画面サイズの対応を次に示します。
320dp: 通常のスマートフォンの画面(240x320 ldpi、320x480 mdpi、480x800 hdpi など)。
480dp: 5 インチ未満の大画面スマートフォン(480x800 mdpi)。
600dp: 7 インチ タブレット(600x1024 mdpi)。
720dp: 10 インチ タブレット(720x1280 mdpi、800x1280 mdpi など)。

下記のように判定する部分を実装しました。

extension BuildContextExtension on BuildContext {
  bool isTablet() {
    const tabletWidth = 600;
    //  縦の場合は横幅で、横の場合は高さでタブレットか判定する
    return MediaQuery.orientationOf(this) == Orientation.portrait
        ? MediaQuery.sizeOf(this).width > tabletWidth
        : MediaQuery.sizeOf(this).height > tabletWidth;
  }
}

タブレットとスマホの画面を出し分ける部分の実装

参照記事を真似した形になります。

class MasterDetailPage extends StatefulWidget {
  const MasterDetailPage({super.key});

  @override
  State<MasterDetailPage> createState() => _MasterDetailPageState();
}

class _MasterDetailPageState extends State<MasterDetailPage> {
  User selectedValue = userList[0];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("UserList"),
      ),
      body: OrientationBuilder(
        builder: (context, orientation) {
          return Row(
            children: <Widget>[
              Expanded(
                flex: 2,
                child: UserListView(
                  userList: userList,
                  onTap: (value) {
                    // 一覧をタップした時の動作
                    if (context.isTablet()) {
                      // タブレットの場合は詳細の表示を更新する
                      setState(() {
                        selectedValue = value;
                      });
                    } else {
                      // スマホの場合は詳細画面に遷移する
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) {
                            return DetailsPage(
                              user: value,
                            );
                          },
                        ),
                      );
                    }
                  },
                ),
              ),
              if (context.isTablet())
                // タブレットの場合は詳細を横に表示
                Expanded(
                  flex: 8,
                  child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: 16.w),
                    child: UserDetails(
                      user: selectedValue,
                    ),
                  ),
                )
            ],
          );
        },
      ),
    );
  }
}

ユーザー名の部分が表示が崩れていて、微調整が必要ですが以下のような感じになってます。

タブレット

tw.gif

th.gif

スマホ

sh.gif

sw.gif

おわりに

MasterDetailPageの実装は、個人的にはわかりづらいので別の実装の方が良さそうな気がしてます。
下記みたいな実装の方がわかりやすいかなと思いました。

context.isTablet() ? _buildTabletPage() : _buildSmartphonePage();

または、GoRouterを使っている場合は画面遷移の定義をスマホ用とタブレット用に分けて、起動時に判定して、遷移の定義を使い分けるとかでも良さそうです(試してないのでできるかは不明です)

この記事のコード全体は、以下のブランチにPushしたので参考になれば幸いです。
https://github.com/AppDeveloperMLLB/responsive_app_sample/tree/feat/support_tablet

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?