あけましておめでとうございます。アプリエンジニアのくまもん(YOUTRUST/X)です。最近はぷよぷよテトリス2にハマっています。ぷよぷよテトリスSはネット対戦そこそこやっていたのですが、バージョンアップしてからは手がつけられていませんでした。年始にSteamのセールで$7.49のときに買って、ストーリーモード的なものもやったのですが、新旧のキャラクターがバランスよく出てきてかなり良い!今のところ2024のベストバイです。以前やったときにはうまく理解できなかったテクニックも、現代はYouTubeでレベルに応じて分かりやすい解説動画がたくさんあるので、それらを見たりして練習しています。
さて本題ですが、スマホアプリでは、モーダルと呼ばれるよく使われる部品群があります。モーダルとは、簡単にいうとアプリの前面に出現して、ユーザーのアクションを待つためのものです。この記事では、モーダルついての解説と、Flutterでの実装方法、YOUTRUSTアプリでの実際の使われ方などについて紹介します。
モーダル(Modal)
Apple の HIG(Human Interface Guideline) によると、モーダリティ(Modality)*1 とは、
親ビューとのインタラクションを妨げ、解除するために明示的なアクションを必要とする、独立した専用モードでコンテンツを表示するデザイン手法である。
と説明されています。モーダルを使用すると:
・人々が重要な情報を受け取り、必要であればそれに基づいて行動できるようにする。
・直近の行動を確認または変更できるオプションを提供する
・前の文脈を見失うことなく、範囲を絞った明確なタスクを実行できるようにする。
・没入感を与えたり、複雑なタスクに集中できるようにする。
が可能になるとされています。
以下の部品についても、 Modal
だとか Modal View
だとか説明されています。
- Alerts*2: An alert is a modal view と説明されている
- Action sheets*3 :An action sheet is a modal view と説明されている
- Sheets*4: By default, a sheet is modal と説明されている
Material Design では、以下のような部品でModalという言葉が出てきます*5。
- Dialog*6: A dialog is a type of modal window と説明されている
- Sheets: bottom *7: Modal bottom sheets are an alternative to inline menus or simple dialogs... と説明されている
Drawer の画面が暗くなるバージョンに関しても*8 Modal navigation drawers use a scrim to block interaction with the rest of an app’s content. と説明されており、これもモーダルの一種といえるようです。
まとめると、モーダルというのはユーザーの操作を明示的なアクションがあるまでブロックするデザインの総称のようで、Dialog や Sheets と呼ばれる部品も含まれるようです。Material Design3 には full-screen dialog*9 と呼ばれるものもあるようなので、画面が暗くなるとかオーバーレイしているかどうかとかは本質的な部分ではなさそうです。
肌感でいうと、モーダルや半モーダルという言葉が単体で部品を指す語として使用されていたときは、 (Bottom) Sheets
のことを指していることが多いように思いますが、「モーダリティを持つ部品の総称」という意味で使ったほうが、よりAppleやGoogleが定めているものに近いのではないかと思いました。
半モーダルに当たる語をどう表現するかは微妙に悩ましいところですが、HIGでもMaterial Designでも Sheets
と呼称されているので、これらはシートとかボトムシートと呼称したほうが好ましいかもしれません。
余談: モーダル、ポップアップ、ダイアログの違い
同じような言葉で、ポップアップというものもあります。ポップアップは「急にぽんっと上に出てくるもの」を意味する英単語で、よく「ポップアップ広告」とか言ったりします。その定義でいうと、アプリで使われているダイアログに関しても、ポップアップの一種と捉えても間違いではなさそうです。HIGやMaterial Design の文脈ではダイアログのことをポップアップと呼んでいることはなさそうですが、ポップアップという言葉も依然として現場では使われているため、その場合は Dialog のことだなと読み替える必要があります。
モーダルは「ユーザーの操作を妨げ、解除するためにアクションを要求するものの総称」なので、ポップアップも、モーダルの定義には合いそうなのでモーダルと言えそうです(ただし、Flutterの文脈ではボタンを押したときに出てくる小さなメニューがPopUpMenuという部品なので、個人的にはそちらを連想します)。
ダイアログは、モーダルの一種になります。同じように、画面下部に出てくるシートや Drawer(サイドメニュー) もモーダルと言えそうです。さらに、ダイアログは上にぽんっと出てくるものでもあるので、ポップアップでもあるとも言えそうです。
なんだかややこしくなってきましたが、上記の話を無理やり図にまとめるとこんな感じになります。違う文脈では違う言葉の使われ方をしているかもしれませんし、個人的に Material Design のドキュメントを読んだときのニュアンスの一解釈をまとめたものということでご承知おきください(言い訳がましい)。
Flutter におけるモーダルの実装
Flutter では、モーダルは関数を呼び出して内容を指定するだけで簡単に実装できます。
Dialog は showDialog
関数*10を使うことで表示できます。
showGeneralDialog
*11 というのもあり、こちらを呼び出すと、 showDialog
より詳細にカスタムアニメーションとバリア(背景の暗い部分)の動作を設定できるようです。
showDialog
関数の動きがデフォルトで美しいため、個人的にはユースケースが思いつきませんでしたが、showDialog 自体も、実装を覗いてみると、 Navigator.of(context, rootNavigator: useRootNavigator).pushshowDialog
に複雑な実装があるというわけではなく、*13 Navigator.push のごく薄いラッパーだということがわかります。
showDialog
関数には任意のWidgetを渡すこともできますし、AlertDialog
*14やSimpleDialog
*15などMaterial Designの実装が用意されています。
また iOS っぽい見た目の Alert を実装するには CupertinoAlertDialog
*16 というのがあり、後述しますがこれはもう直接使わなくていいです。
ModalBottomSheet
は showModalBottomSheet
関数*17 を使うことで表示できます。
showModalBottomSheet
も showDialog
/ showGeneralDialog
関数と同様に、内部的には PopupRoute<T> extends ModalRoute<T>
を Navigator.push
しているだけで、 *18 基本的なメカニズムは同じであることがわかります。
簡単に中身が見れて、中身を見るとシンプルかつ統一的になっており、しかも実用的なユースケースを見越してちょうどいい粒度の関数がすべて用意されているところが個人的にFlutterの好きなところの一つだなと再実感しました。
標準の関数を利用するメリット
カスタムのモーダルを定義する方法自体は色々あるかとは思いますが、 showDialog
や showModalBottomSheet
関数を利用することで、以下のような恩恵を得ることができると考えます。
- 画面が暗くなるトランジション、下からにょきっと生えてくるトランジションがデフォルトで実装されている
- Scrim(バリア=背景の暗い部分)*19をタップした際に、キャンセルする挙動が実装されている
PopScope
などでカスタムの挙動を指定しなくても、バックキーの挙動が実装されている
標準の関数で実装しておけば、 ThemeData.useMaterial3
フラグが true のときでも false のときでも、デザインシステムに応じた実装することができます。
YOUTRUSTアプリ におけるモーダルの使われ方
YOUTRUSTアプリ におけるモーダルの使用例を、一部を紹介します。
Dialog
2023年8月16日にリリースされたFlutter 3.13*20 からは、プラットフォームに応じたダイアログの見た目を実現するために、 AlertDialog.adaptive
*21 という実装を利用できます。この場合、 showDialog
の代わりに showAdaptiveDialog
を呼ぶ必要がありそうでした。
以下に OK のみのパターンと、 OK/Cancel が両方あるパターンのダイアログについて、それぞれサンプルを示します。
OKのみのパターン
(画面上部の「▶ Run」ボタンから、実際の挙動を見ることができます。)
OK/Cancel 両方あるパターン
プラットフォームに応じた実装は、 AlertDialog.adaptive
以外にもサードパーティの mono0926/adaptive_dialog *22 パッケージがあり、この公式実装が登場する以前からパターンにも幅広く対応できるために、よく使用されています。
通知の許可を促す際に表示しているダイアログなどはカスタムのWidgetを設定して showDialog 関数を呼び出していますが、今のところ共通の関数を用意して、それを経由させて呼び出しています。
ModalBottomSheet
showModalBottomSheet
はフィルターを選択するUIなどで使用されています。
また、showModalBottomSheet
を使った例として、 HIG で Action sheets*23 風のUIがあります。
- コメントの編集/削除を選択する
- SNSへのシェア方法を選択する
- カメラ/ライブラリを選択する場面
など、色々な場面でこの部品が使われています。個人的にはこの部品がお気に入りで、Androidで出てきても主観的には違和感がありません。
実装上は showModalBottomSheet 関数 の外側に Padding と cornerRadius を使う形で実装されています*24。下記にサンプルを示します。
おわりに
今回モーダルについて考えるにあたって、 HIG や Material Design を読み直ししたが、やはりどちらもかなり洗練されていて読み物としてかなり面白く、他の項目も読み直したいと感じました。 たとえば、Material Design 3 の Transitions のページを見ると、主要なトランジションパターンを6つに分類していて、そんなシンプルな分類ができたのか!と衝撃を受けました。
また、YOUTRUSTアプリのデザインについてエンジニアとデザイナー間で議論するときには、HIGをもとに議論することはあまりなく、基本的にMaterial Designを参照することが多いのですが、YOUTRUSTのデザインは全体に渡って HIG が提唱している Modality のベストプラクティスに思ったより準拠した形で設計されているように見えて、本質というのはプラットフォームに依らず同じようなところに収束していくってことなのかなあと思ったりしました。
実際にFlutterでモーダルを呼び出す実装が各々どのように実装されているのかも今見てみましたが、実はモーダルの実装というものがあるわけではなく、Navigation の push の薄いラッパーとしてシンプルかつ統一的に実装されていることがわかり、そういった面からもFlutterのAPIの気遣いというかさじ加減というか、総合的な完成度の高さを再実感しました。
今後もMaterial Design や Flutter の標準の関数の実装を眺めて、アプリデザインに関しての理解を深めていきたいと思います!
株式会社YOUTRUSTでは、エンジニアを積極募集中です!
*1:https://developer.apple.com/design/human-interface-guidelines/modality
*2:https://developer.apple.com/design/human-interface-guidelines/alerts
*3:https://developer.apple.com/design/human-interface-guidelines/action-sheets
*4:https://developer.apple.com/design/human-interface-guidelines/sheets
*5:https://m2.material.io/search.html?q=modal
*6:https://m2.material.io/components/dialogs
*7:https://m2.material.io/components/sheets-bottom
*8:https://m2.material.io/components/navigation-drawer
*9:https://m3.material.io/components/dialogs/guidelines
*10:https://api.flutter.dev/flutter/material/showDialog.html
*11:https://api.flutter.dev/flutter/widgets/showGeneralDialog.html
*12:https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/widgets/routes.dart#L2238
*13:https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/material/dialog.dart#L1400
*14:https://api.flutter.dev/flutter/material/AlertDialog-class.html
*15:https://api.flutter.dev/flutter/material/SimpleDialog-class.html
*16:https://api.flutter.dev/flutter/cupertino/CupertinoAlertDialog-class.html
*17:https://api.flutter.dev/flutter/material/showModalBottomSheet.html
*18:https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/material/bottom_sheet.dart#L1225
*19:https://m2.material.io/components/dialogs
*20:https://medium.com/@ak187429/flutter-3-13-framework-improvements-41be37e925d8
*21:https://api.flutter.dev/flutter/material/AlertDialog/AlertDialog.adaptive.html
*22:https://github.com/mono0926/adaptive_dialog
*23:https://developer.apple.com/design/human-interface-guidelines/action-sheets
*24:本稿で紹介しているソースコードは、公開用にYOUTRUSTで実際に使われているコードを参考に書き直しているため、実際のコードとは異なります