【Flutter】綺麗で見栄えの良いSafeArea
FlutterのSafeAreaは扱いを間違えると汚くなる問題
FlutterでSafeAreaを考慮した作りをした際、コンテンツをスクロールしている時にbottom部分のSafeAreaで切れて、SafeArea部分から急にコンテンツがスクロールインしてくる作りをしがちだった。
理由としては、Screen > SafeArea > Contents という階層をイメージして作っているためで、ScaffoldのbodyをSafeAreaで囲むのが構造として正しいと思ってたからだ。
SafeAreaが透明だったならたぶんこれでも問題なかったが、背景色と同じ色で表示されるため、SafeAreaに入った途端Contentsが急に表示されるように見える不自然な実装になる。
iOSのNative実装でも同様なアプリが存在するが、適切な実装をしてやれば、コンテンツをスクロールしている時は、bottom部分のSafeArea外からスクロールインして、スクロールの終わりはSafeArea内に収まるようになる。
言葉では伝わりにくいが、SafeAreaの見栄えを意識して実装している開発者からしたら伝わる噴飯物の作りだと思う。
| 現実 | 理想 |
|---|---|
![]() |
![]() |
現実と理想の画面下部のを見比べて欲しい。
現実の方は、SafeArea外は描画すらされてないないが、理想の方は、SafeArea外でも12が描画されている。
これらをスクロールしていくと、現実の方は、何もない宙から12以降の数字が表示され、見た目も挙動も気持ち悪いものとなる。
日頃から、この理想の形で実装できないものかと頭を悩ませていたが、偶然実現できたのでまとめておこうと思った次第。
汚いSafeAreaになる構造
汚い挙動が発生する構造として、SafeArea + SingleChildScrollView + Columnの組み合わせが頻出しやすいと思う。
import 'package:flutter/material.dart';
class DirtySafeAreaPage extends StatelessWidget {
const DirtySafeAreaPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final numbers = [for (int i = 0; i < 20; i++) i];
return Scaffold(
appBar: AppBar(
title: const Text('dirty safe area'),
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: numbers
.map(
(e) => ListTile(title: Text('$e')),
)
.toList(),
),
),
),
);
}
}

ListViewを使うと綺麗
ListViewを使えば、適切にSafeAreaのpaddingが考慮されているため、スクロール中はSafeArea外でもコンテンツが表示され、スクロールの終わり時にはSafeArea内にコンテンツが綺麗に収まるる挙動をする。
import 'package:flutter/material.dart';
class ListViewSafeAreaPage extends StatelessWidget {
const ListViewSafeAreaPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final numbers = [for (int i = 0; i < 20; i++) i];
return Scaffold(
appBar: AppBar(
title: const Text('list view safe area'),
),
body: ListView.builder(
itemCount: numbers.length,
itemBuilder: (context, index) {
final number = numbers[index];
return ListTile(
title: Text('$number'),
);
},
),
);
}
}

綺麗なSafeAreaの構造
SafeAreaをSingleChildScrollViewより下に持ってくることで実現できる
import 'package:flutter/material.dart';
class SmartSafeAreaPage extends StatelessWidget {
const SmartSafeAreaPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final numbers = [for (int i = 0; i < 20; i++) i];
return Scaffold(
appBar: AppBar(
title: const Text('smart safe area'),
),
body: SingleChildScrollView(
child: SafeArea(
child: Column(
children: numbers
.map(
(e) => ListTile(title: Text('$e')),
)
.toList(),
),
),
),
);
}
}

比較
| dirty safe area | list view | smart safe area |
|---|---|---|
![]() |
![]() |
![]() |
GitHubで、この記事に用いたコードも公開している。


Discussion