はじめに
スマホとタブレットに対応するためのレスポンシブ対応にトライしてみたいと思います
対応前
対応前のコードは下記にあります。
https://github.com/AppDeveloperMLLB/responsive_app_sample/tree/feat/support_orientation
上記は、【Flutter】画面回転に対応するで実装したコードになります。
スマートフォンで縦横の画面に対応しましたが、タブレットで表示すると以下のような表示になります。
一覧画面
詳細画面
スマホだと違和感のない表示でしたが、タブレットだと以下が気になりました。
- 文字が小さい
- 余白が目立つ
- 横長になりすぎる箇所がある
それぞれ対応していきたいと思います。
タブレット対応
上記の気になる点を踏まえ、以下の対応をしていきます。
- 文字の大きさの対応
- レイアウトの対応
文字の大きさ対応
いくつかの方法があるみたいです。
参考記事
上記の記事を参考にすると以下の方法がありました。
- 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を設定すると下記のようになりました。
スマートフォンでもレイアウト崩れはなかったです。
こちらを取り入れる際は、コード例のように、デザイン時点でテキストの大きさの種類を定義して、画面幅に対してそれぞれいくつにするかを決めておく必要がありそうです。
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,
),
)
このパッケージの利点は、高さと幅を指定もレスポンシブ対応できることです。
フォントと同じく、高さには.h、幅には.wをつけると、デバイスの解像度に合わせて調整してくれます。
SizedBox(
width: 100.w
height: 100.h
)
このパッケージを使うとGoldenTestを実装する際に注意が必要みたいです。
レイアウトの対応
レイアウトの対応の対策は以下の二つがあると思います。
- 余白や大きさをレスポンシブに対応する
- デザインをスマホとタブレットで変える
余白や大きさをレスポンシブに対応する
スマートフォンように大きさや余白を調整しているので、下の画像のように余白の大小があったり、Widgetのサイズが小さすぎたりします。
ユーザーの画像を表示する紫の円とその横の名前のコードは下記になっています。
@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: 72
やheight: 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,
),
),
],
);
}
これでユーザーの画像と名前を表示する部分に関しては、調整ができました。
(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,
),
),
],
);
}
デザインをスマホとタブレットで変える
スマホとタブレットでデザインを別にする対応もレスポンシブの対策として有効かと思います。
今回のデザインは以下で実装します。
スマホ : リスト画面 → 詳細画面の二画面の実装
タブレット : 左側にリスト、右側に詳細表示の一画面の実装
以下の記事を参考にしました。
タブレットかスマホかの判定
タブレットかスマホかの判定は横幅かアスペクト比で判定するみたいです。
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,
),
),
)
],
);
},
),
);
}
}
ユーザー名の部分が表示が崩れていて、微調整が必要ですが以下のような感じになってます。
タブレット
スマホ
おわりに
MasterDetailPage
の実装は、個人的にはわかりづらいので別の実装の方が良さそうな気がしてます。
下記みたいな実装の方がわかりやすいかなと思いました。
context.isTablet() ? _buildTabletPage() : _buildSmartphonePage();
または、GoRouterを使っている場合は画面遷移の定義をスマホ用とタブレット用に分けて、起動時に判定して、遷移の定義を使い分けるとかでも良さそうです(試してないのでできるかは不明です)
この記事のコード全体は、以下のブランチにPushしたので参考になれば幸いです。
https://github.com/AppDeveloperMLLB/responsive_app_sample/tree/feat/support_tablet