[Flutter] showModalBottomSheetを使ったら特定機種(iPhone SE 3rd)でレンダリングオーバーフローエラーが出たので対処する

2023.03.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 Delivery部の若槻です。

今回は、FlutterでshowModalBottomSheetを使ったら特定機種(iPhone SE 3rd)でレンダリングオーバーフローエラーが出たので対処してみました。

showModalBottomSheetとは

Flutterのモバイルアプリ実装で大きめのモーダル領域を使用したい場合はshowModalBottomSheetを使うと便利です。

サンプルコードを試してみます。(height500に修正しています。)

lib/main.dart

import 'package:flutter/material.dart';

void main() => runApp(const BottomSheetApp());

class BottomSheetApp extends StatelessWidget {
  const BottomSheetApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Bottom Sheet Sample')),
        body: const BottomSheetExample(),
      ),
    );
  }
}

class BottomSheetExample extends StatelessWidget {
  const BottomSheetExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: const Text('showModalBottomSheet'),
        onPressed: () {
          showModalBottomSheet<void>(
            context: context,
            builder: (BuildContext context) {
              return Container(
                height: 500,
                color: Colors.amber,
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      const Text('Modal BottomSheet'),
                      ElevatedButton(
                        child: const Text('Close BottomSheet'),
                        onPressed: () => Navigator.pop(context),
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

下記のように画面下方からモーダルシートを出すことができます。左がiPhone SE 3rd、右がiPhone 14 Pro Maxです。

画面下部からオレンジ背景のモーダルを表示した様子。このモーダルシート上にWidgetを設置することができます。

事象

ここでshowModalBottomSheet内に高さを絶対値指定したWidgetを配置したとします。下記では代替としてSizedBox(height: 400)を追加しています。

lib/main.dart

class BottomSheetExample extends StatelessWidget {
  const BottomSheetExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: const Text('showModalBottomSheet'),
        onPressed: () {
          showModalBottomSheet<void>(
            context: context,
            builder: (BuildContext context) {
              return Container(
                height: 500,
                color: Colors.amber,
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      const Text('Modal BottomSheet'),
                      ElevatedButton(
                        child: const Text('Close BottomSheet'),
                        onPressed: () => Navigator.pop(context),
                      ),
                      SizedBox(height: 400),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

するとモーダルを開いた時に左のiPhone SE 3rdでレンダリングオーバーフローエラーが起きるようになりました。iPhone SE 3rdではボタンの高さ分だけモーダルが隠れてしまい、絶対高さ500px以下となりオーバーフローが発生してしまったようです。

══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 101 pixels on the bottom.

The relevant error-causing widget was:
  Column Column:file:///Users/wakatsuki.ryuta/projects/cm-rwakatsuki/flutter_sample_app/lib/main.dart:36:26

To inspect this widget in Flutter DevTools, visit:
http://127.0.0.1:9104/#/inspector?uri=http%3A%2F%2F127.0.0.1%3A52473%2FnO44gmovx8w%3D%2F&inspectorRef=inspector-0

The overflowing RenderFlex has an orientation of Axis.vertical.
The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and
black striped pattern. This is usually caused by the contents being too big for the RenderFlex.
Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the
RenderFlex to fit within the available space instead of being sized to their natural size.
This is considered an error condition because it indicates that there is content that cannot be
seen. If the content is legitimately bigger than the available space, consider clipping it with a
ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex,
like a ListView.
The specific RenderFlex in question is: RenderFlex#429d5 relayoutBoundary=up1 OVERFLOWING:
  creator: Column ← Center ← ColoredBox ← ConstrainedBox ← Container ←
    NotificationListener<DraggableScrollableNotification> ← DefaultTextStyle ←
    AnimatedDefaultTextStyle ← _InkFeatures-[GlobalKey#9c376 ink renderer] ←
    NotificationListener<LayoutChangedNotification> ← PhysicalModel ← AnimatedPhysicalModel ← ⋯
  parentData: offset=Offset(106.0, 0.0) (can use size)
  constraints: BoxConstraints(0.0<=w<=375.0, 0.0<=h<=363.9)
  size: Size(163.0, 363.9)
  direction: vertical
  mainAxisAlignment: center
  mainAxisSize: min
  crossAxisAlignment: center
  verticalDirection: down
◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤
════════════════════════════════════════════════════════════════════════════════════════════════════

アプリケーションの仕様の都合でモーダルの高さの絶対値を500から変えたくない場合に、解決する方法はあるのでしょうか。

試行:SafeArea対応では解消しなかった

iOS端末では端末毎にセーフエリア(SafeArea)を取得することができ、アプリケーションから取得してレンダリング領域の調整に使用することができます。

FlutterでもSafeArea Widgetが用意されています。

またshowModalBottomSheetではuseSafeAreaプロパティがあり、こちらを合わせて有効にすれば良さそうです。

The [useSafeArea] parameter specifies whether a [SafeArea] is inserted. Defaults to false. If false, no SafeArea is added and the top padding is consumed using [MediaQuery.removePadding].

(日本語訳)
[useSafeArea] パラメータは、[SafeArea] を挿入するかどうかを指定します。デフォルトは false です。 false の場合、SafeArea は追加されず、上部のパディングは [MediaQuery.removePadding] を使用して消費されます。

このSafeAreaを使って解消できるか試してみます。

先程のコードで、showModalBottomSheet内の実装をSafeArea Widgetでラップします。

lib/main.dart

class BottomSheetExample extends StatelessWidget {
  const BottomSheetExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: const Text('showModalBottomSheet'),
        onPressed: () {
          showModalBottomSheet<void>(
            useSafeArea: true,
            context: context,
            builder: (BuildContext context) {
              return SafeArea(
                  child: Container(
                height: 500,
                color: Colors.amber,
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      const Text('Modal BottomSheet'),
                      ElevatedButton(
                        child: const Text('Close BottomSheet'),
                        onPressed: () => Navigator.pop(context),
                      ),
                      SizedBox(height: 400),
                    ],
                  ),
                ),
              ));
            },
          );
        },
      ),
    );
  }
}

しかし解消はしませんでした。

解決:SingleChildScrollViewで抑制できた

SingleChildScrollViewを導入することによりエラーを抑制することができました。これによりモーダルシートの内部をスクロール可能にしています。

lib/main.dart

class BottomSheetExample extends StatelessWidget {
  const BottomSheetExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: const Text('showModalBottomSheet'),
        onPressed: () {
          showModalBottomSheet<void>(
            context: context,
            builder: (BuildContext context) {
              return SingleChildScrollView(
                  child: Container(
                height: 500,
                color: Colors.amber,
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      const Text('Modal BottomSheet'),
                      ElevatedButton(
                        child: const Text('Close BottomSheet'),
                        onPressed: () => Navigator.pop(context),
                      ),
                      SizedBox(height: 400),
                    ],
                  ),
                ),
              ));
            },
          );
        },
      ),
    );
  }
}

iPhone SE 3rdでは画面下部を表示するためにスクロール操作が必要となります。iPhone 14 Pro Maxではスクロールをせずにモーダル全体が表示されてします。

参考

以上