Flutter を聞いたことありますか?Flutterとは Google 製のアプリケーションUI構築ツールキットです。本記事では iOS/Android 両対応のモバイルアプリ制作のために Flutter を用いていますが、他にもWeb、さらにはデスクトップ向けのアプリケーションを構築することも可能です。 Flutter は Google によって『The best framework for developing beautiful experiences for any screen』をめざして開発が進められているフレームワークです。これは技術的な垣根を超え一つのコードベースから様々なポータル端末で同じユーザ体験を実現することを意味しており、Flutter の将来性が期待できます。
今回はこの Flutter を使って、モバイルアプリのヘッダーとフッター部分を作っていきます。Flutter でどのように UI を作っていくのか興味のある方は是非!
目次
Flutter は iOS/Android 両方でビルドできる便利なツール
Flutter については Flutter 公式サイトで『一つのコードベースからモバイル、ウェブ、デスクトップ向けの美麗なアプリケーションを構築するための Google 製の UI ツールキット』と述べられています。とくにモバイルアプリにおいては、単一のコードから iOS/Android の両方にビルドできることから、iOS版とAndroid版の2つの開発フローを統合することが期待できる便利なツールです。また、UIは全てコードベースであるため、『UIツールキット』の名前の通り、UIをコードで作成することによりそれぞれのプラットフォーム(iOS/Android)に応じた美麗な UI 画面を作成することができます。Flutter は Dart と呼ばれるプログラミング言語を採用しています。
今回はこの Flutter を用いて Line モバイルアプリのヘッダー部分とフッター部分を真似して作っていきます。完成品アプリ画面は質素なものですが、Flutter でどうやってUIを作っていくのか興味のある方はぜひ最後までお付き合いいただけると幸いです。
今回の記事の目標 : Flutterで某SNSアプリのヘッダーとフッターを真似して作る
この記事ではタイトルの通り、『Flutter でモバイルアプリのヘッダーとフッター』を作っていきます。とはいっても、アプリとしてのロジックは一切含みませんので、Flutter ってどうやって画面作っていくんだろう?と思った方向けの記事になっています。また、ただ単に作るよりも、既存のアプリを真似てつくるほうが勉強になると考えたため、Line アプリのデザインを真似していきます。iOS版の完成図は以下(図1)のようなものです。
図1 : 本記事でのアプリ完成画面
早速つくってみよう!
Flutter の環境の構築
まずはFlutter の環境を構築する必要があります。が、本記事では、環境構築については取り扱っていません。理由としては構築方法については公式ドキュメントにて良質な資料があるためです。リンク先資料を参照すれば特に苦労することなく構築できると思います。ただし、ダウンロードおよびインストール処理がたくさんあります。全くのゼロ(Xcode なし、Android Stuido なし)から環境を作る人は、覚悟を決めましょう。私は1日が潰れました…。
私は MacOS で作業をしているため、iOS Simulater と Android Emulater の両方をセットアップしています。
プロジェクト作成と最初のビルド
それでは、ヘッダーとフッターを作る前にまず土台を作ります。Flutter でプロジェクトを作成すると(私はAndroid Stuidoでやっています)、lib/main.dart
というファイルが生成されると思います。まずは、生成された lib/main.dart
のコードをすべて削除し、以下のコードを書きます。
import 'package:flutter/material.dart'; void main() { runApp(App()); } class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Text("オラオラオラオラ"), ), ); } }
まずは初ビルドです。iOS Simulator または Android Emulator を起動し、ビルドしましょう。
図2 : 最初のビルド画像
無事ビルド(図2)できました。しかし、文字列が左上に位置しており、あわやスマホの時刻表示と被りそうです。これは文字列を表示するための Text Widget に対して位置の指定がないためです。この文字列自体にたいした意味はありませんが、見栄えがよくないので、画面の真ん中に持っていきましょう。ついでに、画面右上に表示されている Debug の文字も今は消しておきます。
import 'package:flutter/material.dart'; void main() { runApp(App()); } class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, // <- Debug の 表示を OFF home: Scaffold( body: Center(child: Text("オラオラオラオラ")), // <- Text の位置を指定 ), ); } }
ひとまずはこれで完了です。この後再ビルドしてもいいのですが、Flutter の特徴である Hot Reload を使ってみましょう。このままファイルを保存します。すると Reload が走り加えた変更が画面が反映されました(図3)。
図3 : Center Widget を追加
Flutter はこの通り保存するごとに Reload してくれる Hot Reload 機能をもっています。アプリが大きくなってくると再ビルドに時間がかかるようになってきますが、Flutter であれば このHot Reload 機能のおかげで開発の高速化が望めます。
さて、最初のビルドが完了したので、コードがシンプルなうちに簡単に Flutter の考え方についてまとめておきましょう。 Flutter ではコードベースでUIを作成します。ここで登場するのが Widget です。いってしまえば、Flutter での UI 作成は Widget と呼ばれる小さなパーツを組み合わせることです。今回のケースではもっとも最上位のルート Widget は 私が勝手に作った App Widget です。App Widget はMaterialデザインを作るための MaterialApp Widget をもっており、MaterialApp Widget は モバイルアプリの基本的な構造を持つ Scaffold Widget からなります。Scaffold Widget の body プロパティは アプリ( Scaffold Widget )のメインコンテンツの Widget を 表しています。今回はこのbodyプロパティは 子(childプロパティ)の Widget を真ん中に配置する位置指定の Center Widget を持っており、Center Widget は 文字列を表示する Text Widget を持っているという入れ子構造になっています。言葉で説明するとややこしいですが、図にすると(図4)イメージしやすいです。
図4 : Widget 構成図
この後、ヘッダーとフッターをつくっていきますが、Widget を 入れ子にして UI 作っていくということを念頭に置いておくと後の流れがわかりやすいと思います。
ちなみに、記事の途中ですが、どんな Widget があるのか詳細を知りたい方は個人的に下記のページをおすすめします。私もこの記事の作成にあたり、どの Widget を使うかで参考にさせていただきました。
AppBar Widget を使ってヘッダー部分を作る
では最初にヘッダーを作っていきます。完成図は以下(図5)です。
図5 : フッター部分の完成図
まずは、モバイルアプリの基本構造を持つ Scaffold Widget の appBar プロパティ に AppBar Widget をいれてビルドしてみましょう。
class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: AppBar(title: Text("ホーム"),), // appBar プロパティに AppBar Widget を追加 body: Center(child: Text("オラオラオラオラ")), ), ); } }
おっと、Hot Reload なので保存するだけでOKです。
図6 : ヘッダー部分のビルド
これで簡単なヘッダー(図6)が表示されました。とても簡単ですね。それでは、某SNSアプリのヘッダーを真似をするため、AppBar Widget をカスタマイズしていきましょう…といきたいところですが、このまま lib/main.dart
で、AppBar Widget にコードを書き込みカスタマイズをしていくと、Widget を入れ子にしていくという構造上から lib/main.dart
はスパゲティーコードになってしまいます。ということで、新しく Header Widget と表す Header クラスを作りこれを別ファイルに分けましょう。
lib/header.dart
を作成し、下記のコードを書きます。
import 'package:flutter/material.dart'; class Header extends StatelessWidget with PreferredSizeWidget{ @override Size get preferredSize => Size.fromHeight(kToolbarHeight); @override Widget build(BuildContext context) { return AppBar( title: Text('ホーム'), ); } }
そして、lib/main.dart
側も少し変更します。
import 'package:flutter/material.dart'; import 'header.dart'; // <- header.dart を インポート void main() { runApp(App()); } class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: Header(), // <- 新しく作った Header Widget (Header Instance) を指定 body: Center(child: Text("オラオラオラオラ")), ), ); } }
これで、保存してみましょう。エラーがでなければ分離完了です。以後、ヘッダーのカスタマイズは lib/header.dart
で行なっていきます。
ではヘッダーにアイコンをつけていきましょう。これにかんしては AppBar Widget のドキュメントにわかりやすい図があります。
AppBar Widget の leading プロパティと actions プロパティに Icons Widget を指定してあげれば良さそうです。また、タイトルを真ん中にするために centerTitle プロパティを True にして、backgroundColor に黒っぽい色を指定してあげます。また、アイコンの詳細な位置は Padding Widget で指定してあげます。
import 'package:flutter/material.dart'; class Header extends StatelessWidget with PreferredSizeWidget{ @override Size get preferredSize => Size.fromHeight(kToolbarHeight); @override Widget build(BuildContext context) { return AppBar( leading: Padding( padding: const EdgeInsets.all(8.0), child: Icon(Icons.settings), ), actions: [ Padding( padding: const EdgeInsets.all(8.0), child: Icon(Icons.add), ), ], title: Text( 'ホーム', ), backgroundColor: Colors.black87, centerTitle: true, elevation: 0.0, ); } }
Hot Reload でみてみると…。
図7 : ヘッダーコード修正後画面
できました(図7)。アイコンに関しては Flutter が提供しているデフォルトの物を使っていますIconsにアイコンに関するリストがあるので、眺めてみると面白いかも。私は大量のアイコンの中からヘッダーに使う2つのアイコンを探すのにかなり苦労しました…。ひとまずは、ヘッダー画面の完成です。
最後に、私の Header Widget が 既存の Widget でどのような構造になっているか図(図8)を載せます。既存の Widget を組み合わせてヘッダーを作っていることがわかります。
図8 : ヘッダー部分の Widget 構成図
ではこの調子でフッターも作って行きます。完成図は以下(図9)の通りです。
図9 : フッター完成図
フッターにはBottomNavigationBar Widget を使います。ヘッダー同様にまずは lib/main.dart
に直接書き込んでみましょう。
class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: Header(), body: Center(child: Text("オラオラオラオラ")), // ------- 追加部分 ------------------------ bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), title: Text('Home'), ), BottomNavigationBarItem( icon: Icon(Icons.home), title: Text('Home'), ), ], ), // ----------------------------------------- ), ); } }
保存して、Hot Reload を走らせます。
図10 : フッターの最初のビルド画像
フッター(図10)も比較的簡単に追加できたと思います。アプリの土台となる Scaffold Widget はいわゆるフッター部分を表すための、 bottomNavigationBar プロパティを持っているので、そこに BottomNavigationBar Widget を指定してあげます。BottomNavigationBar Widget はこの部分にアイコンなどを表示するための BottomNavigationBarItem Widget の リストを受け取る items プロパティを持っており、この items プロパティに2つのBottomNavigationBarItem Widget を渡しています。ここで確認しておきたいことは、今は画面左側のアイコンが青くなっていますが、右側のアイコンをタップしても画面に変化がみられないところです。
では、ヘッダーの時と同様にファイルを分けます。新しくlib/footer.dart
というファイルを作り、下記のコードを描きます。
import 'package:flutter/material.dart'; class Footer extends StatefulWidget{ const Footer(); @override _Footer createState() => _Footer(); } class _Footer extends State
また、lib/main.dart
の方も修正します。
import 'footer.dart'; // footer.dart をインポート (省略) class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: Header(), body: Center(child: Text("オラオラオラオラ")), bottomNavigationBar: Footer() // <- Footer Widget (Footer Instance)を指定 ) ); } }
これで、エラーがでなければ無事ファイルを分離できました。この記事では以後 lib/main.dart
を編集することはありません。
また、フッターのコードはヘッダーの時と違い、StatefulWidget を継承していることに気がつきましたか?これについては後で解説します。このまま一気に画面を作ってしまいます。せっかくコードベースで書いているので、プログラムっぽく記述しましょう。
class _Footer extends State<Footer> { int _selectedIndex = 0; final _bottomNavigationBarItems = <BottomNavigationBarItem>[]; // アイコン情報 static const _footerIcons = [ Icons.home, Icons.textsms, Icons.access_time, Icons.content_paste, Icons.work, ]; // アイコン文字列 static const _footerItemNames = [ 'ホーム', 'トーク', 'タイムライン', 'ニュース', 'ウォレット', ]; @override void initState() { super.initState(); _bottomNavigationBarItems.add(_UpdateActiveState(0)); for ( var i = 1; i < _footerItemNames.length; i++) { _bottomNavigationBarItems.add(_UpdateDeactiveState(i)); } } /// インデックスのアイテムをアクティベートする BottomNavigationBarItem _UpdateActiveState(int index) { return BottomNavigationBarItem( icon: Icon( _footerIcons[index], color: Colors.black87, ), title: Text( _footerItemNames[index], style: TextStyle( color: Colors.black87, ), ) ); } /// インデックスのアイテムをディアクティベートする BottomNavigationBarItem _UpdateDeactiveState(int index) { return BottomNavigationBarItem( icon: Icon( _footerIcons[index], color: Colors.black26, ), title: Text( _footerItemNames[index], style: TextStyle( color: Colors.black26, ), ) ); } void _onItemTapped(int index) { setState(() { _bottomNavigationBarItems[_selectedIndex] = _UpdateDeactiveState(_selectedIndex); _bottomNavigationBarItems[index] = _UpdateActiveState(index); _selectedIndex = index; }); } @override Widget build(BuildContext context) { return BottomNavigationBar( type: BottomNavigationBarType.fixed, // これを書かないと3つまでしか表示されない items: _bottomNavigationBarItems, currentIndex: _selectedIndex, onTap: _onItemTapped, ); } }
ふぅ。一気に書いてしまいましたが、簡単に解説します。まず、_bottomNavigationBarItems というBottomNavigationBarItem
のリスト型の変数を宣言しています。そして、IconData
型のリストとString
型のテキストを宣言し、initState
メソッドで、フッター部分に表示する BottomNavigationBarItem
型のリストを生成しています。プログラムっぽくなりましたね。このような形にしたのには理由があります。最初の実行ではフッターのアイコン等を表すBottomNavigationBarItem Widget を2つ宣言しましたが、今回は5つにしています。書いてみるとわかりますが5つも地道に書いていくのは面倒ですし、入れ子の構造からネストが深くなり記述ミスが発生しやすくなります。おまけにコード量も増えてしまうので、フッターの各要素となる Widget はプログラムで作ってしまうことにしました。そしてもうひとつ、プログラムで書いている理由があります。それは私の作った Footer Widget は StatefulWidget であるためです。ややこしくなってきました。ではここで Flutter で重要な要素である StatelessWidget と StatefulWidget について解説します。
Flutter での UI 画面作成は Widget を組み合わせて作っていくと書きましたが、このWidget は 2種類あります。この2つがそれぞれ StatelessWidget と Stateful Widget です。名前からなんとなく想像できるかと思いますが、それぞれ性質が異なります。StatelessWidget はその名前の通り、状態がない Widget です。もっといえば、ビルド時に Widget がインスタンス化されてその後一切の変更ができない immutable な Widget です。思い出してください。一つ前の節で作った Header Widget は StatelessWidget
を継承して作りました。 そのため、ビルド後ヘッダーの画面は変わることがありません。アイコン部分にボタン系の Widget をかませて、ボタンが押された時になんらかのアクションを起こすなどは可能ですが、Header Widgetの状態はもう変えることができません。対して、今回のFooter Widget は StatefulWiget
を継承してつくっています。StatefulWidget は StatelessWidget の逆で、ビルド後もsetState()メソッドを呼び出すことで、その状態を変えることができます。Footer Widget は 各アイコンが押された時そのアイコンがアクティブになるように状態が変化します(アプリ本体の画面は切り替わりませんが…)。これを実現するためにStatefulWidget にしました。この状態の変更はプログラムで書く必要があるため、Footer class はプログラムっぽくなっています(まあ、Dart 言語で書いてるので厳密にはもともと全部プログラムなんですけどね)。
最後は文字が多くなりましたが、これで完成です(図11)。自分でいうのもなんですが、フッターとヘッダーのみだと寂しいですね…。
図11 : フッター完成図
Footer Widget は最終的に以下(図12)のような構成になっています。
図12 : フッターの Widget 構成図
Android でビルドしよう
この記事では iOS Simurator のみで画面を確認してきましたが、最後に Android Emulator でもビルドしてみましょう。方法はAndroid Emulator を立ち上げて、ビルド対象を Android Emulator に変更するだけです。
図13 : Android でビルドした画面
できました(図13)!ヘッダーとフッターのみなので、見た目にほとんど変化はみられませんが Flutter でつくったアプリを iOS/Android 両方でビルドできることが示せたのではと思います。
まとめ
今回はFlutter で モバイルアプリのヘッダーとフッターを作ってみました。完成品はかなり寂しい記事になりましたが、Flutter での Widget の組み合わせて画面を作っていく過程が少し理解していただけたなら、嬉しく思います。また、公式ドキュメントにもあった美しいデザインの通り、(少なくとも今のところは)デザインも綺麗ですよね。 また、たいした苦労もなくiOS/Androidでビルドできていると思います。とはいえ、流石にヘッダーとフッターだけじゃ寂しすぎるので、次回は続編でbody部分を作ってみたという記事を書きたいと思います。