Flutterとは、マルチプラットフォームで動作するUIフレームワークです。単一のソースコードからiOS/Android/Web/デスクトップ用にコンパイルすることが可能です。様々な「ウィジェット」と呼ばれるUIパーツが初めから用意されており、それを組み合わせることで、簡単に美しいUIを実装することができます。
プログラミング言語には「Dart」を使用します。Dartは、元々JavaScriptを置き換える目的で開発された言語ですが、今ではFlutterで使用できることで有名です。今回は、Dartの説明はあまり深掘りしませんが、JavaScriptやJavaをやったことのある開発者であれば、すんなり理解できると思います。
Flutterはこちらからインストールできます。インストール方法は各OSにより異なりますが、ここではmacOSでのインストール方法を説明します。
https://flutter.dev/docs/get-started/install
まず、Flutterのリポジトリをクローンします
$ mkdir ~/development $ cd ~/development $ git clone https://github.com/flutter/flutter.git -b stable
次に、パスを通します。bashの場合は ~/.bash_profile
、zshの場合は ~/.zshenv
に記述します。
$ export PATH="$PATH:~/development/flutter/bin"
source
コマンドでパスを読み込むか、ターミナルを再起動して、dockerのコマンドを試してみましょう。
$ flutter doctor
このコマンドは開発環境をチェックし、必要なセットアップを表示してくれます。それでは、進めていきましょう。
(Dart SDKはFlutterに含まれているため、セットアップは不要です。)
Android Studioに含まれているAndroid SDKを使用します。以下からAndroid Studioをインストールしてください。
https://developer.android.com/studio/?hl=ja
インストール後、Android Studioを起動し、セットアップウィザードに従い、Android SDKをインストールしてください。
(Android Studioは、後ほどIDEとして使用します。)
iOS ビルドにはXcodeをインストールします。(Mac AppStoreからインストールしてください。)
インストール後、コマンドラインツールの設定を行います。
$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer $ sudo xcodebuild -runFirstLaunch
ライセンスに同意し、セットアップを進めてください。
Flutterの開発環境として、Android StudioもしくはVisual Studio Codeが推奨されています。いずれも、Flutterプラグインを導入することで、Flutterのデバッグ・ホットリロードができるようになります。
今回は、Android Studioでのセットアップ方法を紹介します。
flutter
と検索します。Install
ボタンでインストールします。以上で、IDEのセットアップは完了です。これで、Android Studio上でFlutter開発を一通り行う準備ができました。
iOS実機ビルドを行うため、署名設定を行います。
ios/Runner.xcworkspace
をXcodeで開きます。この後、Android Studioで接続された実機を選択し、Runをクリックすると、実機でFlutterアプリが起動します。
Androidの実機を接続し、iOSと同じように実機を選択し、Runすると起動します。
Flutterの開発中の便利機能として、ホットリロードがあります。これは、コードをSaveすると、デバッグ中のアプリがランタイムで更新され、最新のコードが適用される機能です。
一方で、ホットリスタートは、一般的なAndroid/iOSアプリ開発と同じように、アプリをRunし直すことで、最新のコードを適用する方法です。
レイアウト変更などの、軽微な変更はホットリロードが効きますが、変更内容によってはホットリロードが効かない場合もあります。そのような場合は、ホットリスタートを行いましょう。(Android Studioであれば、 Ctrl + r
でホットリスタートです)
Flutterプロジェクトでは、主に以下のディレクトリ・ファイルを開発に使用します。
Flutterでは、UIをWidgetという単位で構築し、アプリのルートから全てツリー構造となっています。
以下に、プロジェクト作成直後のWidgetツリーを図にしました。
ルートは MaterialApp
Widgetとなり、 home
に渡すWidgetが、初期画面となります。ここでは、 MyHomePage
を設定し、さらにその下に画面に表示するUIがツリーとなっています。
MyHomePage
直下のScaffold
は、1画面を構成する土台です。 AppBar
,Body
,FloatingActionButton
などの枠にWidgetを当てはめることで、簡単にアプリのUIを構築できます。大抵の場合は、 Scaffold
を使用することをお勧めします。
それでは、このツリー構造をクラスで表すと、どのようになるでしょうか。
まず、画面が「動的に変化するかどうか」で大きく分かれます。動的に変化する画面の場合、 StatefulWidget
を継承したクラスを作成します。逆に、静的な画面であれば、 StatelessWidget
を継承したクラスを使用します。
StatefulWidget
を使用する場合は、次の2つのクラスが必要です。
StatefulWidget
を継承したクラスState<T>
を継承したクラスプロジェクト作成直後のコードを例に、解説します。まずは、 StatefulWidget
を継承した MyHomePage
クラスを見てみましょう。ccreateState()
をオーバーライドし、後述する State<T>
のインスタンスをセットします。
class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); }
次に、State<T>
を継承した _MyHomePageState
クラスを見ていきましょう。
まず、状態を保持しておくフィールド変数を定義します。 build()
メソッドで、フィールド変数を使用して、Widgetを構築していきます。
フィールド変数を更新するときは、 setState()
のクロージャ内で変数を更新すると、 Widget build(BuildContext context)
がコールされWidgetが再構築されることで、UIが更新されます。
class _MyHomePageState extends State; { int _counter = 0; void _incrementCounter() { // フィールド変数をこのように更新すると、Widgetが再構築される setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children:[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
StatelessWidget
を継承したクラスを作成します。 StatelessWidget
は状態を変化させることができません。そのため、 State<T>
クラスは不要で、直接 build()
メソッドを実装します。
class HelloWorldPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold(appBar: AppBar( title: Text('Hello World Page'), ), body: Text('Hello Flutter!'), ); } }
画面遷移を行うには、 Navigator
クラスを使用します。
画面遷移するには、 Navigator.push()
メソッドを使用します。第2引数で MaterialPageRoute
クラスを使い、次の画面のインスタンスを生成して渡します。
Navigator.push( context, MaterialPageRoute(builder: (context) => SecondPage() ));
前の画面に戻るには Navigator.pop(context)
と実装すればOKです。また、Push遷移すると、左上に戻るボタンが表示されるため、この機能で十分であれば、Pop遷移の実装は必要ありません。
モーダル遷移の場合も Navigator.push()
を使用します。 fullscreenDialog
引数を true
に設定すると、モーダル遷移になります。
Navigator.push( context, MaterialPageRoute(builder: (context) => SecondPage(), fullscreenDialog: true ));
次の画面に値を渡すには、遷移先でプロパティを定義し、コンストラクタで値を渡します。
まず、遷移先のコードです。
import 'package:flutter/material.dart'; class SecondPage extends StatelessWidget { // プロパティ定義 final String name; // コンストラクタ SecondPage(this.name); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('SecondPage') ), body: Text('こんにちは$nameさん') // 受け取った値を表示する ); } }
name
をコンストラクタで受けとり、 Widget build(BuildContext context)
内でテキスト表示しています。
プロパティを final
としているため、コンストラクタの実装が必須となります。(ただし、後からプロパティの値を変更する場合は、 final
を付ける必要はありません)
次に、遷移元です。
class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('First Page'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // ボタンをタップして遷移用メソッドを呼び出す RaisedButton( onPressed: _openSecondPage, child: Text('次の画面へ') ) ], ), ), ); } // ボタンタップ時に呼ばれるメソッド void _openSecondPage() { Navigator.push( context, MaterialPageRoute(builder: (context) => SecondPage('山田')) // コンストラクタでの値渡し ); } }
プロジェクトテンプレートのHomePageを変更し、次の画面に値を渡すようにしました。
値を渡す際は、 MaterialPageRotue()
で次の画面を生成する際、コンストラクタに値を渡すことで、実現できます。
値を戻す際は、 Navigator.pop()
メソッドに値を渡します。
RaisedButton( child: Text('戻る'), onPressed: () { Navigator.pop(context, 'もう一度'); // 第二引数に、戻したい値をセット } )
次に、戻した値を受け取る実装です。
class _MyHomePageState extends State { String _buttonText = '次の画面へ'; // ボタンテキストを変更するためのプロパティ @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('1画面目'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ RaisedButton( onPressed: () async { // awaitを使用してpopしたときに返り値を受け取ることができるようにする String message = await Navigator.push( context, MaterialPageRoute(builder: (context) => SecondPage('山田')) ); // 受け取った値を使用して、ステート更新(ボタンのテキストが変わります) setState(() { _buttonText = message; }); }, child: Text(_buttonText) ) ], ), ), ); } }
このサンプルでは、戻ってきた値を使用し、ボタンのテキストを変化させるようにしました。
戻り値の受取は非同期となるため、 await
を使用します。また、 await
を使用する場合は、関数を async
とします。
Widgetは、FlutterのUIを定義するオブジェクトです。Widgetの特徴としては、UIのほぼ全てをWidgetやそのツリーで構成されていることが挙げられます。
また、ユーザ操作に対するレスポンスが優れているUI/UXが、Widgetとして数多く存在しています。また、その多くはカスタマイズ可能であり、もちろん自作のWidgetを作成することも可能です。
ここでは、使用頻度の高いWidgetを紹介します
画面の骨組みとなるレイアウトです。AppBar, FAB, BottomNavigationといった、基本的なUIの骨組みを提供します。基本的に、各画面のルートWidgetは、Scaffoldを使用することをお勧めします。
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('SecondPage') ), body: Column( children: [ Text('こんにちは'), ], ), floatingActionButton: FloatingActionButton( child: Icon(Icons.search), tooltip: 'Search', onPressed: () {}, ), ); }
画面内のコンテンツは body
に実装します。直接、UIパーツを置くと、1つしか配置できないため、後ほど説明する Column
などを使用して、複数配置できるようにすることがほとんどです。
様々な用途で使用できる、便利なWidgetです。色付きのボックス、サークルを作成したり、それを回転したりすることができ、柔軟にUIを表現することができます。
Container( child: Text('Hello!'), color: Colors.green, padding: EdgeInsets.all(20), // paddingやmarginを使用したレイアウトも可能 margin: EdgeInsets.all(20), ),
円形にするには、このようにします。
Container( child: Text('Hello!'), decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.green ), padding: EdgeInsets.all(20), margin: EdgeInsets.all(20), ),
実行結果は次のようになります。
BoxDecoration
クラスを使用して、円形を指定します。このとき、 Container
の color
引数は渡さず、 BoxDecoration
の color
を渡す必要があるため、注意してください。
子Widgetをセンタリングして表示したいときに使用します。
Center( child: Text('こんにちは'), )
child
にColumn
や Row
といった、複数のWidgetを表示できるWidgetを渡すことで、複数パーツをセンタリングすることも可能です。
Column( children: [ Text('こんにちは'), Text('こんばんは'), ], ),
Columnでは、子Widgetを複数持つことができるため、引数名が children
となり、Widget配列を渡します。
UIパーツを横に並べたいときに使用します。
Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 均等にスペースを取る設定 children: [ RaisedButton( child: Text('YES'), onPressed: () {}, ), RaisedButton( child: Text('NO'), onPressed: () {}, ), ], );
mainAxisAlignment: MainAxisAlignment.spaceEvenly
とすることで、均等にスペースを取るようにしています。
テキストを表示するために使用します。
Text( 'Hello Flutter', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, color: Colors.green ), ),
ほとんどは、第一引数のテキスト指定のみで済みますが、フォントスタイルを設定したい場合は、 style
引数に TextStyle
を渡します。TextStyle
では、フォントサイズ、太字・イタリック、カラーなどを設定できます。
画像を表示するWidgetです。
まずは、アプリ埋め込みのアセットを表示する方法です。アセットを表示するには、画像を配置した後、 pubspec.yaml
で登録する必要があります。
flutter: # To add assets to your application, add an assets section, like this: # ↓コメントアウトし、画像のパスを記載 assets: - images/italy.jpg
登録したアセットを表示するには、以下にようにします。
Image.asset('images/italy.jpg')
引数に、 pubspec.yaml
で記載した画像のパスを渡します。
また、URLを指定して、画像をダウンロードして読み込むこともできます。
Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
Flutterサンプルでお馴染みのフクロウが表示されます。
立体的なボタンを表示します。テキストボタンの他に、 icon()
ファクトリメソッドを使用することで、アイコン付きのボタンを表示することができます。
Column( children: [ RaisedButton( child: Text('ボタン'), color: Colors.blue, onPressed: () => { // ここにボタンが押された時の処理を記述 }, ), RaisedButton.icon( // このファクトリメソッドを使用すると、アイコン付きのボタンを表示できます。 onPressed: () => { // ここにボタンが押された時の処理を記述 }, color: Colors.orange, icon: Icon(Icons.home), label: Text('ホーム') ) ], )
ボタンが押された時の処理は onPressed
に、クロージャとして記載します。以下のように、別メソッドを作成して、呼び出すことも可能です。
class HelloPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('SecondPage') ), body: Column( children: [ RaisedButton( child: Text('ボタン'), color: Colors.blue, onPressed: _buttonTapped, // ボタンが押された時に呼び出すメソッドを指定 ), ), ); } void _buttonTapped() { // ここにボタンが押された時の処理を記述 }
なお、 StatelessWIdget
では、 Widget build(BuildContext context)
以外でBuildContextを取得することはできないため、ボタンタップ時にBuildContextが必要な場合(画面遷移など)は、クロージャで書くようにしてください。
TextFieldはテキスト入力UIを提供するWidgetです。
Widget build(BuildContext context) { return Scaffold(appBar: AppBar( title: Text('Hello World Page'), ), body: TextField( decoration: InputDecoration( labelText: '名前を入力してください' // プレースホルダーの設定 ), onSubmitted: (String value) => { print(value) // エンターキーや、テキストフィールドのフォーカスが外れたときに呼ばれる }, ), ); }
onSubmitted
を実装し、入力した値を受け取ります。
また、 decoration
引数に InputDecoration
クラスを設定することで、TextFieldの見た目を設定できます。 このサンプルでは、labelText
ではプレースホルダーを設定します。その他にも、 border: OutlineInputBorder()
とすることで、枠付きの見た目にすることも可能です。
AppBarは、画面上部に表示されるヘッダーです。
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('SecondPage'), actions: <Widget>[ IconButton( icon: Icon(Icons.settings), onPressed: () => { // ここにアクションボタンをタップした時の処理を記述] }, ) ], ), ); }
Scaffoldの apppBar
引数に渡します。 actions
に IconButton
を渡すと 、AppBar右側にボタンを追加することができます。
画面下部にタブのようなUIを表示することができます。
import 'package:basic_sample_app/main.dart'; import 'package:flutter/material.dart'; class TabSamplePage extends StatefulWidget { @override _TabSamplePageState createState() => _TabSamplePageState(); } class _TabSamplePageState extends State { // 現在選択されているBottomNavigationのタブを保持する int _currentIndex = 0; // BottomNavigationで表示する画面のインスタンス final List<Widget> _children = [ MyHomePage(title: 'Flutter App'), HelloWorldPage() ]; @override Widget build(BuildContext context) { return Scaffold( body: _children[_currentIndex], bottomNavigationBar: BottomNavigationBar( // BottomNavigationの表示内容の設定 currentIndex: _currentIndex, items: [ new BottomNavigationBarItem( icon: new Icon(Icons.home), title: Text("ホーム") ), new BottomNavigationBarItem( icon: new Icon(Icons.message), title: Text("メッセージ") ), ], onTap: _onBottomNavigationBarTap ), ); } void _onBottomNavigationBarTap(int index) { // indexをsetStateで更新し、タブを切り替える setState(() { _currentIndex = index; }); } }
BottomNavigationを実装する際は、画面を切り替える処理は自分で実装する必要がある点に気をつけてください。今回の場合、画面のインスタンスを _children
配列に保持し、Scaffoldで body: _children[_currentIndex],
と渡すようにしています。
そして、BottomNavigationBarがタップされたときに呼ばれる _onBottomNavigationBarTap(int index)
で、 _currentIndex
を変更することで、画面を更新し、タブが切り替わったように見せています。そのため、BottomNavigationを使用する画面では、StatefulWidgetとなります。
同じレイアウトを使いまわしたいときに、自作Widgetを作成するには、StatelessWiidgetもしくはStatefulWidgetを継承したクラスを作成します。
class MyCustomButton extends StatelessWidget { // コンストラクタ MyCustomButton({Key key, this.onPressed, this.child}) : super(key: key); VoidCallback onPressed; Widget child; @override Widget build(BuildContext context) { return RaisedButton( onPressed: onPressed, color: Colors.orange, child: child ); } } class HelloWorldPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold(appBar: AppBar( title: Text('Hello World Page'), ), body: MyCustomButton( child: Text('カスタムボタン'), onPressed: () { print('Custom Button!!') }, ), ); } }
onPressedとchildは外部から設定できるよう、プロパティとコンストラクタを実装しています。そのほかは、これまで紹介したWidgetと同じように、 Widget build(BuildContext context)
でWidgetを返却するようにします。
冒頭で示したWidgetツリー図は、PlantUMLで作成しました。記事画像用なので特にコードの整理はしていませんが、参考になればと思い、PlantUMLのコードを公開します。
ちなみに、冒頭で skinparam shadowing false
とすることで、デフォルトで描画される影を消去しています。
@startuml skinparam shadowing false rectangle MaterialApp rectangle MyHomePage rectangle Scaffold rectangle AppBar rectangle Body rectangle Center rectangle Column rectangle "Text('You have pushed the button this many times:')" as TextDescription rectangle "Text(カウンター)" as TextCounter rectangle "Title" as TitleWidget rectangle FloatingActionButton rectangle "Text('Flutter Demo Home Page')" as TitleText MaterialApp -down-> MyHomePage: home MyHomePage -down-> Scaffold Scaffold -down-> AppBar AppBar -down-> TitleWidget TitleWidget -down-> TitleText Scaffold -down-> Body Body -down-> Center Center -down-> Column Column -down-> TextDescription Column -down-> TextCounter Scaffold -down-> FloatingActionButton note right of MaterialApp : MyAppクラス note right of MyHomePage : _MyHomePageStateクラス @enduml
Flutterは様々なWigetが用意されているされており、Wigetを組み合わせることでスピーディにクロスプラットフォームアプリが実現出来ることが分かりました。今後活用していきたいも思います!