[Flutter] Cupertinoでナビゲーションバーを実装する際に少しハマったこと

2022.11.19

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

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

今回は、FlutterのCupertinoでナビゲーションバーを実装する際に少しハマったことについて共有します。

Cupertinoとは

Cupertinoを使用すると、FlutterアプリケーションにiOSスタイルのデザインのWidgetを導入することができます。

CupertinoスタイルのWidgetは様々なものが用意されており、既定のMaterial DesignスタイルのWidgetを置き換えて、iOSネイティブのようなデザインのUIを簡単に実装できます。

下記エントリでは、CupertinoとMaterial DesignのUIの実装を比較しているので合わせてご覧ください。

ハマったこと

さて、そんなCupertinoのCupertinoNavigationBar classを使用してナビゲーションバーを実装しようとしたところ、いくつかハマりどころがありました。

下記はドキュメントにあるサンプルコードです。このコードを例にハマった箇所について紹介します。

lib/main.dart

import 'package:flutter/cupertino.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: NavBarExample(),
    );
  }
}

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

  @override
  State<NavBarExample> createState() => _NavBarExampleState();
}

class _NavBarExampleState extends State<NavBarExample> {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        backgroundColor: CupertinoColors.systemGrey.withOpacity(0.5),
        middle: const Text('CupertinoNavigationBar Sample'),
      ),
      child: Column(
        children: <Widget>[
          Container(height: 50, color: CupertinoColors.systemRed),
          Container(height: 50, color: CupertinoColors.systemGreen),
          Container(height: 50, color: CupertinoColors.systemBlue),
          Container(height: 50, color: CupertinoColors.systemYellow),
        ],
      ),
    );
  }
}

アプリケーションをChromeで実行した様子。

デフォルトで背景色が透過となる

まず、CupertinoNavigationBarではデフォルトで背景色が透過され、オブジェクトがオーバーレイしてしまいます。

例えば次のようにbackgroundColorを指定しない場合は既定の透過度0となります。

lib/main.dart(backgroundColorがデフォルト値)

      navigationBar: const CupertinoNavigationBar(
        //backgroundColor: CupertinoColors.systemGrey.withOpacity(0.5),
        middle: Text('CupertinoNavigationBar Sample'),
      ),

透過させないようにするためには、次のように透過度1.0を明示的に指定するか、透過なしのColorオブジェクトを指定するようにします。

lib/main.dart(backgroundColorの透過度が1.0)

      navigationBar: CupertinoNavigationBar(
        backgroundColor: CupertinoColors.systemGrey.withOpacity(1.0),
        middle: const Text('CupertinoNavigationBar Sample'),
      ),

lib/main.dart(backgroundColorの透過度が1.0)

      navigationBar: CupertinoNavigationBar(
        backgroundColor: CupertinoColors.systemGrey,
        middle: const Text('CupertinoNavigationBar Sample'),
      ),

別Widget化する場合は ObstructingPreferredSizeWidget で implements する

クラスを再利用出来るように別Widget化したい場合があると思います。そこでCupertinoNavigationBarを使用して次のようなCommonNavigationBarWidgetを実装しました。

lib/main.dart(エラー)

class _NavBarExampleState extends State<NavBarExample> {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CommonNavigationBar('CupertinoNavigationBar Sample'),
      child: Column(
        children: <Widget>[
          Container(height: 50, color: CupertinoColors.systemRed),
          Container(height: 50, color: CupertinoColors.systemGreen),
          Container(height: 50, color: CupertinoColors.systemBlue),
          Container(height: 50, color: CupertinoColors.systemYellow),
        ],
      ),
    );
  }
}

class CommonNavigationBar extends StatelessWidget {
  final String middleText;
  const CommonNavigationBar({Key? key, this.middleText = ''}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CupertinoNavigationBar(
      backgroundColor: CupertinoColors.systemGrey,
      middle: Text(middleText),
    );
  }
}

しかし上記実装は次のようなエラーとなってしまいます。

原因としては、エラーメッセージの通りなのですが、CupertinoPageScaffold.navigationBarに指定可能なObstructingPreferredSizeWidget以外のタイプが指定されたためです。

The argument type 'CommonNavigationBar' can't be assigned to the parameter type 'ObstructingPreferredSizeWidget?'

ObstructingPreferredSizeWidgetクラスでは、Widgetの優先サイズを取得するgetterであるPreferredSizeWidget.preferredSizeと、透過の有無を返すObstructingPreferredSizeWidget.shouldFullyObstructの実装が必要となります。

そこでCommonNavigationBarを次のように修正し、ObstructingPreferredSizeWidgetを継承したWidgetを実装します。

lib/main.dart

class _NavBarExampleState extends State<NavBarExample> {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CommonNavigationBar(
          middleText: 'CupertinoNavigationBar Sample'),
      child: Column(
        children: <Widget>[
          Container(height: 50, color: CupertinoColors.systemRed),
          Container(height: 50, color: CupertinoColors.systemGreen),
          Container(height: 50, color: CupertinoColors.systemBlue),
          Container(height: 50, color: CupertinoColors.systemYellow),
        ],
      ),
    );
  }
}

class CommonNavigationBar extends StatelessWidget
    implements ObstructingPreferredSizeWidget {
  final String middleText;
  const CommonNavigationBar({Key? key, this.middleText = ''}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CupertinoNavigationBar(
      backgroundColor: CupertinoColors.systemGrey,
      middle: Text(middleText),
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(40); // Widgetの高さを指定

  @override
  bool shouldFullyObstruct(BuildContext context) {
    return true; // 透過させない場合はtrue
  }
}

これで無事CupertinoNavigationBarを別Widget化できました。

おわりに

FlutterのCupertinoでナビゲーションバーを実装する際に少しハマったことについて共有しました。

公式ドキュメントをよく読みましょうということに尽きると思います。

以上