はじめに
Flutterとは、マルチプラットフォームで動作するUIフレームワークです。単一のソースコードからiOS/Android/Web/デスクトップ用にコンパイルすることが可能です。様々な「ウィジェット」と呼ばれるUIパーツが初めから用意されており、それを組み合わせることで、簡単に美しいUIを実装することができます。
プログラミング言語には「Dart」を使用します。Dartは、元々JavaScriptを置き換える目的で開発された言語ですが、今ではFlutterで使用できることで有名です。今回は、Dartの説明はあまり深掘りしませんが、JavaScriptやJavaをやったことのある開発者であれば、すんなり理解できると思います。
セットアップ
Flutterのインストール
Flutterはこちらからインストールできます。インストール方法は各OSにより異なりますが、ここではmacOSでのインストール方法を説明します。
https://flutter.dev/docs/get-started/install
まず、Flutterのリポジトリをクローンします
1 2 3 | $mkdir~/development $cd~/development $git clonehttps://github.com/flutter/flutter.git-bstable |
次に、パスを通します。bashの場合は ~/.bash_profile
、zshの場合は ~/.zshenv
に記述します。
1 | $export PATH="$PATH:~/development/flutter/bin" |
source
コマンドでパスを読み込むか、ターミナルを再起動して、dockerのコマンドを試してみましょう。
1 | $flutter doctor |
このコマンドは開発環境をチェックし、必要なセットアップを表示してくれます。それでは、進めていきましょう。
(Dart SDKはFlutterに含まれているため、セットアップは不要です。)
Android環境のセットアップ
Android Studioに含まれているAndroid SDKを使用します。以下からAndroid Studioをインストールしてください。
https://developer.android.com/studio/?hl=ja
インストール後、Android Studioを起動し、セットアップウィザードに従い、Android SDKをインストールしてください。
(Android Studioは、後ほどIDEとして使用します。)
Xcodeのインストール
iOS ビルドにはXcodeをインストールします。(Mac AppStoreからインストールしてください。)
インストール後、コマンドラインツールの設定を行います。
1 2 | $sudo xcode-select--switch/Applications/Xcode.app/Contents/Developer $sudo xcodebuild-runFirstLaunch |
ライセンスに同意し、セットアップを進めてください。
IDEのセットアップ(Android Studio)
Flutterの開発環境として、Android StudioもしくはVisual Studio Codeが推奨されています。いずれも、Flutterプラグインを導入することで、Flutterのデバッグ・ホットリロードができるようになります。
今回は、Android Studioでのセットアップ方法を紹介します。
- Android Studioを起動し、Preferences画面を開きます。(Welcome画面 > Configure > Preferences)
- Editor > Pluginsを開き、検索ボックスで
flutter
と検索します。 - Flutterプラグインの
Install
ボタンでインストールします。 - 途中、Dart Pluginのインストールを尋ねるプロンプトが表示された場合は、「Yes」ボタンでインストールします。
- 指示に従ってAndroid Studioを再起動します。
以上で、IDEのセットアップは完了です。これで、Android Studio上でFlutter開発を一通り行う準備ができました。
Flutterプロジェクトの新規作成
- Android StudioのWelcome画面で「Start a new Flutter Project」を選択します。
- ベースとなるプロジェクトを以下の中から選択します。
- Flutter Application
ユーザ向けのアプリを開発する際に選択。 - Flutter Plugin
AndroidやiOSのネイティブAPIをFlutterから使用できるようにするプラグインを開発 - Flutter Package
Dartコンポーネントを作成するために使用します。例えば、自作のWidgetを、複数のプロジェクトで共有する場合に使用できます。 - Flutter Module
既存のAndroid/iOSアプリにFlutterコンポーネントを組み込むモジュールを開発する場合に使用します。
- Flutter Application
- プロジェクト設定を行います。
Project name、Project locationを入力して進みます。Project nameはスネークケースで入力しましょう。 - パッケージネームの設定をします。
Company domainは、パッケージ名に使用する企業名を指定します。Androidでのパッケージ名、iOSでのBundle Identifierで同じ値が使用されます。
Android X、Kotlin support、Swift supportにチェックを入れます。
iOS実機ビルド
iOS実機ビルドを行うため、署名設定を行います。
- Flutterプロジェクト内の
ios/Runner.xcworkspace
をXcodeで開きます。 - プロジェクト設定の “Signing & Capabilities” を開き、チーム設定等の署名設定を行います。ここは、通常のiOSアプリ開発と同じです。
この後、Android Studioで接続された実機を選択し、Runをクリックすると、実機でFlutterアプリが起動します。
Android実機ビルド
Androidの実機を接続し、iOSと同じように実機を選択し、Runすると起動します。
ホットリロード・ホットリスタート
Flutterの開発中の便利機能として、ホットリロードがあります。これは、コードをSaveすると、デバッグ中のアプリがランタイムで更新され、最新のコードが適用される機能です。
一方で、ホットリスタートは、一般的なAndroid/iOSアプリ開発と同じように、アプリをRunし直すことで、最新のコードを適用する方法です。
レイアウト変更などの、軽微な変更はホットリロードが効きますが、変更内容によってはホットリロードが効かない場合もあります。そのような場合は、ホットリスタートを行いましょう。(Android Studioであれば、 Ctrl + r
でホットリスタートです)
Flutterプロジェクトの構成
Flutterプロジェクトでは、主に以下のディレクトリ・ファイルを開発に使用します。
- lib/
Dartコードを記載するディレクトリです。 - test/
Dartのテストコードを配置するディレクトリです。 - ios/
Xcodeプロジェクトが配置されるディレクトリです。 - android/
Androidプロジェクトが配置されるディレクトリです。 - pubspec.yaml
外部ライブラリを導入したり、アセットを登録する際に使用します。
Widgetツリー構造
Flutterでは、UIをWidgetという単位で構築し、アプリのルートから全てツリー構造となっています。
以下に、プロジェクト作成直後のWidgetツリーを図にしました。
ルートは MaterialApp
Widgetとなり、 home
に渡すWidgetが、初期画面となります。ここでは、 MyHomePage
を設定し、さらにその下に画面に表示するUIがツリーとなっています。
MyHomePage
直下のScaffold
は、1画面を構成する土台です。 AppBar
,Body
,FloatingActionButton
などの枠にWidgetを当てはめることで、簡単にアプリのUIを構築できます。大抵の場合は、 Scaffold
を使用することをお勧めします。
1画面に必要なクラス
それでは、このツリー構造をクラスで表すと、どのようになるでしょうか。
まず、画面が「動的に変化するかどうか」で大きく分かれます。動的に変化する画面の場合、 StatefulWidget
を継承したクラスを作成します。逆に、静的な画面であれば、 StatelessWidget
を継承したクラスを使用します。
StatefulWidgetを使用した画面の作り方
StatefulWidget
を使用する場合は、次の2つのクラスが必要です。
StatefulWidget
を継承したクラスState<T>
を継承したクラス
プロジェクト作成直後のコードを例に、解説します。まずは、 StatefulWidget
を継承した MyHomePage
クラスを見てみましょう。ccreateState()
をオーバーライドし、後述する State<T>
のインスタンスをセットします。
1 2 3 4 5 6 7 | classMyHomePageextendsStatefulWidget{ MyHomePage({Key key,this.title}):super(key:key); finalStringtitle; @override _MyHomePageState createState()=>_MyHomePageState(); } |
次に、State<T>
を継承した _MyHomePageState
クラスを見ていきましょう。
まず、状態を保持しておくフィールド変数を定義します。 build()
メソッドで、フィールド変数を使用して、Widgetを構築していきます。
フィールド変数を更新するときは、 setState()
のクロージャ内で変数を更新すると、 Widget build(BuildContext context)
がコールされWidgetが再構築されることで、UIが更新されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class_MyHomePageState extendsState;{ int_counter=0; void_incrementCounter(){ // フィールド変数をこのように更新すると、Widgetが再構築される setState((){ _counter++; }); } @override Widget build(BuildContext context){ returnScaffold( 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
を継承したクラスを作成します。 StatelessWidget
は状態を変化させることができません。そのため、 State<T>
クラスは不要で、直接 build()
メソッドを実装します。
1 2 3 4 5 6 7 8 9 10 | classHelloWorldPageextendsStatelessWidget{ @override Widget build(BuildContext context){ returnScaffold(appBar:AppBar( title:Text('Hello World Page'), ), body:Text('Hello Flutter!'), ); } } |
画面遷移
画面遷移を行うには、 Navigator
クラスを使用します。
Push/Pop
画面遷移するには、 Navigator.push()
メソッドを使用します。第2引数で MaterialPageRoute
クラスを使い、次の画面のインスタンスを生成して渡します。
1 2 3 4 | Navigator.push( context, MaterialPageRoute(builder:(context)=>SecondPage() )); |
前の画面に戻るには Navigator.pop(context)
と実装すればOKです。また、Push遷移すると、左上に戻るボタンが表示されるため、この機能で十分であれば、Pop遷移の実装は必要ありません。
Modal
モーダル遷移の場合も Navigator.push()
を使用します。 fullscreenDialog
引数を true
に設定すると、モーダル遷移になります。
1 2 3 4 5 | Navigator.push( context, MaterialPageRoute(builder:(context)=>SecondPage(), fullscreenDialog:true )); |
値の渡し方
次の画面に値を渡すには、遷移先でプロパティを定義し、コンストラクタで値を渡します。
まず、遷移先のコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import'package:flutter/material.dart'; classSecondPageextendsStatelessWidget{ // プロパティ定義 finalStringname; // コンストラクタ SecondPage(this.name); @override Widget build(BuildContext context){ returnScaffold( appBar:AppBar( title:Text('SecondPage') ), body:Text('こんにちは$nameさん')// 受け取った値を表示する ); } } |
name
をコンストラクタで受けとり、 Widget build(BuildContext context)
内でテキスト表示しています。
プロパティを final
としているため、コンストラクタの実装が必須となります。(ただし、後からプロパティの値を変更する場合は、 final
を付ける必要はありません)
次に、遷移元です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class_MyHomePageStateextendsState{ @override Widget build(BuildContext context){ returnScaffold( 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()
メソッドに値を渡します。
1 2 3 4 5 6 | RaisedButton( child:Text('戻る'), onPressed:(){ Navigator.pop(context,'もう一度');// 第二引数に、戻したい値をセット } ) |
次に、戻した値を受け取る実装です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class_MyHomePageStateextendsState{ String_buttonText='次の画面へ';// ボタンテキストを変更するためのプロパティ @override Widget build(BuildContext context){ returnScaffold( appBar:AppBar( title:Text('1画面目'), ), body:Center( child:Column( mainAxisAlignment:MainAxisAlignment.center, children:[ RaisedButton( onPressed:()async{ // awaitを使用してpopしたときに返り値を受け取ることができるようにする Stringmessage=await Navigator.push( context, MaterialPageRoute(builder:(context)=>SecondPage('山田')) ); // 受け取った値を使用して、ステート更新(ボタンのテキストが変わります) setState((){ _buttonText=message; }); }, child:Text(_buttonText) ) ], ), ), ); } } |
このサンプルでは、戻ってきた値を使用し、ボタンのテキストを変化させるようにしました。
戻り値の受取は非同期となるため、 await
を使用します。また、 await
を使用する場合は、関数を async
とします。
Widget
Widgetは、FlutterのUIを定義するオブジェクトです。Widgetの特徴としては、UIのほぼ全てをWidgetやそのツリーで構成されていることが挙げられます。
また、ユーザ操作に対するレスポンスが優れているUI/UXが、Widgetとして数多く存在しています。また、その多くはカスタマイズ可能であり、もちろん自作のWidgetを作成することも可能です。
ここでは、使用頻度の高いWidgetを紹介します
レイアウト
Scaffold
画面の骨組みとなるレイアウトです。AppBar, FAB, BottomNavigationといった、基本的なUIの骨組みを提供します。基本的に、各画面のルートWidgetは、Scaffoldを使用することをお勧めします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @override Widget build(BuildContext context){ returnScaffold( appBar:AppBar( title:Text('SecondPage') ), body:Column( children:[ Text('こんにちは'), ], ), floatingActionButton:FloatingActionButton( child:Icon(Icons.search), tooltip:'Search', onPressed:(){}, ), ); } |
画面内のコンテンツは body
に実装します。直接、UIパーツを置くと、1つしか配置できないため、後ほど説明する Column
などを使用して、複数配置できるようにすることがほとんどです。
Container
様々な用途で使用できる、便利なWidgetです。色付きのボックス、サークルを作成したり、それを回転したりすることができ、柔軟にUIを表現することができます。
1 2 3 4 5 6 | Container( child:Text('Hello!'), color:Colors.green, padding:EdgeInsets.all(20),// paddingやmarginを使用したレイアウトも可能 margin:EdgeInsets.all(20), ), |
円形にするには、このようにします。
1 2 3 4 5 6 7 8 9 | 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
を渡す必要があるため、注意してください。
Center
子Widgetをセンタリングして表示したいときに使用します。
1 2 3 | Center( child:Text('こんにちは'), ) |
child
にColumn
や Row
といった、複数のWidgetを表示できるWidgetを渡すことで、複数パーツをセンタリングすることも可能です。
Column
UIパーツを縦に並べたいときに使用します。使い方はシンプルです。
1 2 3 4 5 6 | Column( children:[ Text('こんにちは'), Text('こんばんは'), ], ), |
Columnでは、子Widgetを複数持つことができるため、引数名が children
となり、Widget配列を渡します。
Row
UIパーツを横に並べたいときに使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | Row( mainAxisAlignment:MainAxisAlignment.spaceEvenly,// 均等にスペースを取る設定 children:[ RaisedButton( child:Text('YES'), onPressed:(){}, ), RaisedButton( child:Text('NO'), onPressed:(){}, ), ], ); |
mainAxisAlignment: MainAxisAlignment.spaceEvenly
とすることで、均等にスペースを取るようにしています。
UIパーツ
Text
テキストを表示するために使用します。
1 2 3 4 5 6 7 8 | Text( 'Hello Flutter', style:TextStyle( fontSize:40, fontWeight:FontWeight.bold, color:Colors.green ), ), |
ほとんどは、第一引数のテキスト指定のみで済みますが、フォントスタイルを設定したい場合は、 style
引数に TextStyle
を渡します。TextStyle
では、フォントサイズ、太字・イタリック、カラーなどを設定できます。
Image
画像を表示するWidgetです。
まずは、アプリ埋め込みのアセットを表示する方法です。アセットを表示するには、画像を配置した後、 pubspec.yaml
で登録する必要があります。
1 2 3 4 5 | flutter: #Toadd assets toyour application,add an assets section,like this: #↓コメントアウトし、画像のパスを記載 assets: -images/italy.jpg |
登録したアセットを表示するには、以下にようにします。
1 | Image.asset('images/italy.jpg') |
引数に、 pubspec.yaml
で記載した画像のパスを渡します。
また、URLを指定して、画像をダウンロードして読み込むこともできます。
1 | Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'), |
Flutterサンプルでお馴染みのフクロウが表示されます。
RaisedButton
立体的なボタンを表示します。テキストボタンの他に、 icon()
ファクトリメソッドを使用することで、アイコン付きのボタンを表示することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Column( children:[ RaisedButton( child:Text('ボタン'), color:Colors.blue, onPressed:()=>{ // ここにボタンが押された時の処理を記述 }, ), RaisedButton.icon(// このファクトリメソッドを使用すると、アイコン付きのボタンを表示できます。 onPressed:()=>{ // ここにボタンが押された時の処理を記述 }, color:Colors.orange, icon:Icon(Icons.home), label:Text('ホーム') ) ], ) |
ボタンが押された時の処理は onPressed
に、クロージャとして記載します。以下のように、別メソッドを作成して、呼び出すことも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | classHelloPageextendsStatelessWidget{ @override Widget build(BuildContext context){ returnScaffold( 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
TextFieldはテキスト入力UIを提供するWidgetです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Widget build(BuildContext context){ returnScaffold(appBar:AppBar( title:Text('Hello World Page'), ), body:TextField( decoration:InputDecoration( labelText:'名前を入力してください'// プレースホルダーの設定 ), onSubmitted:(Stringvalue)=>{ print(value)// エンターキーや、テキストフィールドのフォーカスが外れたときに呼ばれる }, ), ); } |
onSubmitted
を実装し、入力した値を受け取ります。
また、 decoration
引数に InputDecoration
クラスを設定することで、TextFieldの見た目を設定できます。 このサンプルでは、labelText
ではプレースホルダーを設定します。その他にも、 border: OutlineInputBorder()
とすることで、枠付きの見た目にすることも可能です。
AppBar
AppBarは、画面上部に表示されるヘッダーです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Widget build(BuildContext context){ returnScaffold( appBar:AppBar( title:Text('SecondPage'), actions:<Widget>[ IconButton( icon:Icon(Icons.settings), onPressed:()=> { // ここにアクションボタンをタップした時の処理を記述] }, ) ], ), ); } |
Scaffoldの apppBar
引数に渡します。 actions
に IconButton
を渡すと 、AppBar右側にボタンを追加することができます。
画面下部にタブのようなUIを表示することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | import'package:basic_sample_app/main.dart'; import'package:flutter/material.dart'; classTabSamplePageextendsStatefulWidget{ @override _TabSamplePageState createState()=>_TabSamplePageState(); } class_TabSamplePageStateextendsState{ // 現在選択されているBottomNavigationのタブを保持する int_currentIndex=0; // BottomNavigationで表示する画面のインスタンス finalList<Widget>_children=[ MyHomePage(title:'Flutter App'), HelloWorldPage() ]; @override Widget build(BuildContext context){ returnScaffold( body:_children[_currentIndex], bottomNavigationBar:BottomNavigationBar(// BottomNavigationの表示内容の設定 currentIndex:_currentIndex, items:[ newBottomNavigationBarItem( icon:newIcon(Icons.home), title:Text("ホーム") ), newBottomNavigationBarItem( icon:newIcon(Icons.message), title:Text("メッセージ") ), ], onTap:_onBottomNavigationBarTap ), ); } void_onBottomNavigationBarTap(intindex){ // indexをsetStateで更新し、タブを切り替える setState((){ _currentIndex=index; }); } } |
BottomNavigationを実装する際は、画面を切り替える処理は自分で実装する必要がある点に気をつけてください。今回の場合、画面のインスタンスを _children
配列に保持し、Scaffoldで body: _children[_currentIndex],
と渡すようにしています。
そして、BottomNavigationBarがタップされたときに呼ばれる _onBottomNavigationBarTap(int index)
で、 _currentIndex
を変更することで、画面を更新し、タブが切り替わったように見せています。そのため、BottomNavigationを使用する画面では、StatefulWidgetとなります。
自作Widgetの作り方
同じレイアウトを使いまわしたいときに、自作Widgetを作成するには、StatelessWiidgetもしくはStatefulWidgetを継承したクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | classMyCustomButtonextendsStatelessWidget{ // コンストラクタ MyCustomButton({Key key,this.onPressed,this.child}):super(key:key); VoidCallback onPressed; Widget child; @override Widget build(BuildContext context){ returnRaisedButton( onPressed:onPressed, color:Colors.orange, child:child ); } } classHelloWorldPageextendsStatelessWidget{ @override Widget build(BuildContext context){ returnScaffold(appBar:AppBar( title:Text('Hello World Page'), ), body:MyCustomButton( child:Text('カスタムボタン'), onPressed:(){ print('Custom Button!!') }, ), ); } } |
onPressedとchildは外部から設定できるよう、プロパティとコンストラクタを実装しています。そのほかは、これまで紹介したWidgetと同じように、 Widget build(BuildContext context)
でWidgetを返却するようにします。
おまけ(PlantUML)
冒頭で示したWidgetツリー図は、PlantUMLで作成しました。記事画像用なので特にコードの整理はしていませんが、参考になればと思い、PlantUMLのコードを公開します。
ちなみに、冒頭で skinparam shadowing false
とすることで、デフォルトで描画される影を消去しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @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:')"asTextDescription rectangle"Text(カウンター)"asTextCounter rectangle"Title"asTitleWidget rectangle FloatingActionButton rectangle"Text('Flutter Demo Home Page')"asTitleText 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を組み合わせることでスピーディにクロスプラットフォームアプリが実現出来ることが分かりました。今後活用していきたいも思います!