🐕

なんでドロワー、下に行ってしまうん??

2021/12/03に公開

はじめに

初めまして、福岡でアプリ開発をしている合同会社Moderationの華頂です。
この記事はFlutter Flutter #1 Advent Calendar 2021 4日目の記事です。
BottomNavigationBarとDrawerを実装したところ、DrawerがBottomNavigationBarの下に行ってしまうという問題にぶち当たりました。今回はその解決方法についてまとめてみました!

問題のコードと挙動は以下

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _selectedIndex = 0;

  static final List<Widget> _bodies = <Widget>[
    Scaffold(appBar: AppBar(), drawer: const Drawer()),
    Scaffold(appBar: AppBar()),
    Scaffold(appBar: AppBar()),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _bodies.elementAt(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            label: 'home',
            icon: Icon(Icons.home),
          ),
          BottomNavigationBarItem(
            label: 'message',
            icon: Icon(Icons.message),
          ),
          BottomNavigationBarItem(
            label: 'favorite',
            icon: Icon(Icons.favorite),
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

挙動を確認

原因

結論としては「RootScreen」の空間ではなく、「Home」の空間でDrawerを開いてしまっていることが原因でした。

「RootScreen」を見てみると、「RootScreen」のbodyの中に「Home」が入っている形になっています。


 Widget build(BuildContext context) {
   return Scaffold(
     body: _bodies.elementAt(_selectedIndex),   

そして「Home」の中にDrawerがあります。

 static final List<Widget> _bodies = <Widget>[
    Scaffold(appBar: AppBar(), drawer: const Drawer()),
    Scaffold(appBar: AppBar()),
    Scaffold(appBar: AppBar()),
  ];

この状態でDrawerを開くと、「Home」の空間にDrawerが出てきてしまいます。

範囲のイメージ

「Home」(紫枠)の中に、AppBarとDrawer(黄色枠)が入っている。

Drawerを開いたイメージ

「Home」(紫枠)の空間で、Drawer(黄色枠)が開かれている。

この状態を改善するために「RootScreen」(上記画像では「赤枠」)の中に、AppBarもbodies(それぞれの画面)もBottomNavigationBarも全て並列で置いてあげました。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _selectedIndex = 0;

  static final List<AppBar> _headers = <AppBar>[
    AppBar(),
    AppBar(),
    AppBar(),
  ];

  static final List<Widget> _bodies = <Widget>[
    const Home(),
    const Message(),
    const Favorite(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const Drawer(),
      appBar: _headers.elementAt(_selectedIndex),
      body: _bodies.elementAt(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            label: 'home',
            icon: Icon(Icons.home),
          ),
          BottomNavigationBarItem(
            label: 'message',
            icon: Icon(Icons.message),
          ),
          BottomNavigationBarItem(
            label: 'favorite',
            icon: Icon(Icons.favorite),
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

挙動を確認

これでちゃんとDrawerがBottomNavigationBarの上に来るようになります!

これで万事解決!!といきたいところですが、このままだと「RootScreen」が、それぞれの要素を並列に扱っているので、全ての画面にDrawerアイコンが出てきてしまいます。
(上記の動画でも、全ての画面にDrawerアイコンが出てきているのが、確認できると思います。)

不必要なところに出てきてしまっているDrawerアイコンを消すために、Appbarのleadingに空widgetを置いてあげると、Drawerアイコンをうまく隠すことができます。


  static final List<AppBar> _headers = <AppBar>[
    AppBar(),
    AppBar(leading: deleteDrawer()),
    AppBar(leading: deleteDrawer()),
  ];

...Widget deleteDrawer() {
  return const SizedBox.shrink();
}

全体のコード

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _selectedIndex = 0;

  static final List<AppBar> _headers = <AppBar>[
    AppBar(),
    AppBar(leading: deleteDrawer()),
    AppBar(leading: deleteDrawer()),
  ];

  static final List<Widget> _bodies = <Widget>[
    const Home(),
    const Message(),
    const Favorite(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const Drawer(),
      appBar: _headers.elementAt(_selectedIndex),
      body: _bodies.elementAt(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            label: 'home',
            icon: Icon(Icons.home),
          ),
          BottomNavigationBarItem(
            label: 'message',
            icon: Icon(Icons.message),
          ),
          BottomNavigationBarItem(
            label: 'favorite',
            icon: Icon(Icons.favorite),
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

class Message extends StatelessWidget {
  const Message({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

class Favorite extends StatelessWidget {
  const Favorite({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

Widget deleteDrawer() {
  return const SizedBox.shrink();
}

おまけTips

ここからは先は、これまでのコードを用いて「enum化」よる利点についてみていきたいと思います!

enum化すると、さまざまな恩恵があるのですが、今回はその中でも

enum化によって「コードの修正・拡張を容易にする」 という部分に焦点を当てていきたいと思います。

まずはコードの修正に関してですが

例えば、今回作ったサンプルアプリでbottomNavigationBarの、MessageとFavotriteの順番を入れ替えをするという修正をしたくなったとします。

enum化する前のコードで、これを実現するとなると

  static final List<Widget> _bodies = <Widget>[
    const Home(),
-   const Message(),
+   const Favorite(),
-   const Favorite(),
+   const Message(),
  ];
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            label: 'home',
            icon: Icon(Icons.home),
          ),
-         BottomNavigationBarItem(
-           label: 'message',
-           icon: Icon(Icons.message),
-         ),
+	  BottomNavigationBarItem(
+           label: 'favorite',
+           icon: Icon(Icons.favorite),
+         ),
-         BottomNavigationBarItem(
-           label: 'favorite',
-           icon: Icon(Icons.favorite),
-         ),
+         BottomNavigationBarItem(
+           label: 'message',
+           icon: Icon(Icons.message),
+         ),
        ],
 

という変更を加える必要があります。
(今回は、たまたまappBarが同一なので、修正箇所は2箇所ですが、そうでなければappBarにも修正を加える必要があります。)

しかし、enum化していれば、enumの順番を並び変えるだけで済みます。

enum RootScreenCategory {
  home,
- message,
+ favorite,
- favorite
+ message,
}

今回のような小さなソースコードでさえ、これだけの差が出るので、大きなプロジェクトになると修正箇所は多くなります。しかしenumを使っていれば、仕様の変更に対してコードの修正箇所が少なくなるので、保守性の向上に繋がります。

次にコードの拡張に関して見ていきたいと思います。

例えば、ルート画面にタブをもう1つ増やしたいとなった場合、enum化されていれば、switch文の箇所が全てエラーになって、新しく追加されたものに対応するappBar、Body、BottmNavigationItemを入れないとビルドが出来ず、どこに何を追加すればいいかdart analysisが教えてくれるので、コードの場所がわからなかったとしても機械的に実装出来ます。

ですが、enum化されていないと、appBar、Body、BottmNavigationItemの数がズレていても、静的解析ではエラーにならないので、ビルドした後に実行時エラーになってしまい、ログを見ながら、どこに何を追加すればいいのか手探りで対応することになります。

このようにenum化するとコードの修正・拡張が容易になります!

enum化は、SOLID原則における「The Open Closed Principle(オープン・クローズドの原則)」と関連のある部分なので、SOLID原則に馴染みのない方は、その辺りを深めていって頂くといいのかなと思います。

ここまで読んでくださり、ありがとうございました。

そして同じような状況で悩まれている方の少しでも参考になれば幸いです。

本記事のコードは、以下のGitHubリポジトリに公開しています!
https://github.com/tai0116/setsuko

enum化した全体のコード

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: RootScreen(),
    );
  }
}

class RootScreen extends StatefulWidget {
  const RootScreen({Key? key}) : super(key: key);

  
  State<RootScreen> createState() => _RootScreen();
}

class _RootScreen extends State<RootScreen> {
  int _selectedIndex = 0;

  static final _rootScreenItems = RootScreenItem.initItems;

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const Drawer(),
      appBar: _rootScreenItems.elementAt(_selectedIndex).appBar,
      body: _rootScreenItems.elementAt(_selectedIndex).body,
      bottomNavigationBar: BottomNavigationBar(
        items: _rootScreenItems.map((e) => e.bottomNavigationBarItem).toList(),
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

class RootScreenItem {
  const RootScreenItem({
    required this.appBar,
    required this.body,
    required this.bottomNavigationBarItem,
  });

  RootScreenItem.fromCategory(RootScreenCategory category)
      : appBar = category.appBar,
        body = category.body,
        bottomNavigationBarItem = category.bottomNavigationBarItem;

  final PreferredSizeWidget appBar;
  final Widget body;
  final BottomNavigationBarItem bottomNavigationBarItem;

  static List<RootScreenItem> get initItems {
    return <RootScreenItem>[
      for (final category in RootScreenCategory.values)
        RootScreenItem.fromCategory(category),
    ];
  }
}

enum RootScreenCategory {
  home,
  message,
  favorite,
}

extension on RootScreenCategory {
  PreferredSizeWidget get appBar {
    switch (this) {
      case RootScreenCategory.home:
        return AppBar();
      case RootScreenCategory.message:
        return AppBar(leading: const EmptyLeading());
      case RootScreenCategory.favorite:
        return AppBar(leading: const EmptyLeading());
    }
  }

  Widget get body {
    switch (this) {
      case RootScreenCategory.home:
        return const Home();
      case RootScreenCategory.message:
        return const Message();
      case RootScreenCategory.favorite:
        return const Favorite();
    }
  }

  BottomNavigationBarItem get bottomNavigationBarItem {
    switch (this) {
      case RootScreenCategory.home:
        return const BottomNavigationBarItem(
          label: 'home',
          icon: Icon(Icons.home),
        );
      case RootScreenCategory.message:
        return const BottomNavigationBarItem(
          label: 'message',
          icon: Icon(Icons.message),
        );
      case RootScreenCategory.favorite:
        return const BottomNavigationBarItem(
          label: 'favorite',
          icon: Icon(Icons.favorite),
        );
    }
  }
}

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

class Message extends StatelessWidget {
  const Message({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

class Favorite extends StatelessWidget {
  const Favorite({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

class EmptyLeading extends StatelessWidget {
  const EmptyLeading({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const SizedBox.shrink();
  }
}

Discussion