Flutterを使って、簡単なアプリを開発してみましょう!
Flutterのインストールは、公式ドキュメントに従って実施してください。
Android / iOS の開発環境は、今回使いたい方どちらか一方の準備でOKです。
公式プラグインが以下の環境向けに用意されています。
- Android Studio
- Visual Studio Code
- IntelliJ IDEA
上記以外でも、お好きなエディタ+コマンドだけでも開発可能です。
※本ドキュメントは、IntelliJ IDEA もしくは Android Studio に Flutter プラグインを入れた環境を対象に記述されています(一部対応するコマンドを併記)。なお、他の環境についても、手順に大きな違いはありません。
インストールが完了後 flutter doctor
コマンドを実行すると、初回に関連ファイルのダウンロード&セットアップが実行されます。
その後、以下のように表示されます。
myhome: flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v0.5.4, on Mac OS X 10.12.6 16G1314, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 27.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.2)
[✓] Android Studio (version 3.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2018.1)
[✓] VS Code (version 1.24.0)
[✓] Connected devices (3 available)
Flutter と Android toolchain か iOS toolchain の使う方がチェックマークになっていればOKです。
- プロジェクト作成
- ホットリロード
- リストビュー
- HTTP通信
- 状態を持ってみる
- アニメーション
行けるところまででOK!
まずはプロジェクトの雛形を作成します。
Android Studio: File > New Flutter Project から進み、アプリ名(myapp)を入力します。コマンドの場合は flutter create myapp
と実行します(myappはアプリ名)。
myappというディレクトリが作成され、その下にファイルやディレクトリが作成されます。今回のコードラボで主に使うのは lib/main.dart
と pubspec.yaml
です。
詳細や、他の環境は以下を参照: https://flutter.io/get-started/test-drive/#androidstudio
アプリの雛形を作成した時点で、最低限の実行できるコードが生成されています。まずは実行してみましょう。
ツールバー(多くの場合は右上に表示されています)から、実行対象を選択します。対象がない場合は「Open Android Emulator」「Open iOS Simulator」から起動します。Androidのエミュレータがセットアップされていない場合は、AVDを作成してください。
AVD作成について: https://developer.android.com/studio/run/managing-avds
エミュレータ起動後、それを選んだ状態で、右にある再生ボタンを押すと、起動が開始されます。
初回の起動は時間が少しかかります。
画面の中央に数字があり、右下のボタンを押すとカウントアップするデモが表示されたでしょうか?
アプリが起動したら、起動したままで、次のステップへ進みます。
※エディタとエミュレータが同時に見えるように横並びにしておくと、開発中のホットリロードが分かりやすいと思います
アプリを作成したディレクトリにある lib
の下に、Dartのファイルが作成されています。lib/main.dart
を開いてみましょう。
以下のようになっています(コメントを省略して表示しています)。
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}
main
関数は、起動時に最初に呼ばれるエントリ・ポイントです。そこで runApp
関数に Widget を渡してアプリを開始しています。
MyApp
は状態を持たない StatelessWidget
で build
メソッドで Widget を return して UI を定義します。
MyHomePage
は状態のある StatefulWidget
で、その状態である _MyHomePageState
を生成します。_MyHomePageState
は State
を継承し、状態を持ち、なおかつ build
メソッドで Widget を return して UI を定義しています(このように Widget と State が分かれています)。
このように Flutter では、状態のない StatelessWidget
と、状態のある StatefulWidget
を使いわけながら build
メソッドで UI の定義(Widget ツリー)を return することで UI を作っていきます。
下の方にある Text
widget の中身の文字を変えてみましょう。
'You have pushed the button this many times'
となっている箇所を、別の文字列にしてみましょう(例: 'こんにちはFlutter!'
)。
変更したら「ホットリロード」してみましょう。
アプリがエミュレータで実行中であれば、再生ボタンの右の方にある稲妻のボタンが有効になっています。このボタンを押すと「ホットリロード」が実行されます。ボタンを押してからエミュレータを見ると Flutter の「ホットリロード」によって、内容が変わっていることが確認できます。
コマンドラインの場合は r キーでホットリロードされます。
エミュレータを見ると、文字が変わっていることが確認できると思います(数字が維持されたまま!)。
テキスト(定数・変数)だけでなく、ロジックも変更&ホットリロードできます。
_incrementCounter
というメソッド内の、カウンタをインクリメントしている処理を、デクリメントに変更してみましょう。
void _incrementCounter() {
setState(() {
_counter--;
});
}
ホットリロードし、ボタンを押してみてください。数値が減少するように動作が変わりましたね。
続いて、リストビューの実装をしてみます。
_MyHomePageState
の中身をごっそり入れ替えます。build
内を以下のように ListView
クラスを使った実装に置き換えてみましょう。
ListView
の children
は Widget の List を受け取ります。Widget であれば、どのような Widget を入れてもかまいません。ここの実装で使っている ListTile
は「よくあるリストの行」を実装した便利な Widget です。
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'App DOJO',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'リストビュー'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView(
children: <Widget>[
new ListTile(title: new Text('A')),
new ListTile(title: new Text('B')),
new ListTile(title: new Text('C')),
new ListTile(title: new Text('D')),
new ListTile(title: new Text('E')),
new ListTile(title: new Text('F')),
new ListTile(title: new Text('G')),
new ListTile(title: new Text('H')),
new ListTile(title: new Text('I')),
new ListTile(title: new Text('J')),
new ListTile(title: new Text('K')),
new ListTile(title: new Text('L')),
new ListTile(title: new Text('M')),
new ListTile(title: new Text('N')),
new ListTile(title: new Text('O')),
new ListTile(title: new Text('P')),
new ListTile(title: new Text('Q')),
new ListTile(title: new Text('R')),
new ListTile(title: new Text('S')),
new ListTile(title: new Text('T')),
new ListTile(title: new Text('U')),
new ListTile(title: new Text('V')),
new ListTile(title: new Text('W')),
new ListTile(title: new Text('X')),
new ListTile(title: new Text('Y')),
new ListTile(title: new Text('Z')),
],
),
);
}
}
ホットリロード後、リストが表示されたでしょうか?
※もしホットリロードでうまくいかない場合や何らかのエラーが表示された場合は「ホットリスタート」を行ってください。プラグインの場合はツールバーの「再生ボタン」を、コマンドの場合は Shift+R でホットリスタートできます。
- https://docs.flutter.io/flutter/widgets/ListView-class.html
- https://docs.flutter.io/flutter/material/ListTile-class.html
HTTP 通信を使ってみましょう。
Dart の http
パッケージを使うために pubspec.yaml
というファイルに依存ライブラリとして追加します。
myapp
ディレクトリにある pubspec.yaml
というファイルを開き dependencies
に http
を追加します。
dependencies:
flutter:
sdk: flutter
http: any
any
とあるのは、バージョンは「何でもよい」という指定です。
pubspec.yaml
を更新したあとは、依存関係を更新するための処理が必要です。
まずアプリを停止してください。ツールバーの停止ボタン、コマンドは Q キーで停止できます。 (パッケージの追加の場合は「ホットリロード」が効かないため、アプリを停止しています)
IDE + プラグインで開発している場合、pubspec.yaml
を開いてみて、エディタ画面上部に表示される「Packages get」を押してください。
コマンドの場合は myapp
ディレクトリで flutter packages get
と実行してください。
この処理が完了すると依存関係に http
パッケージが追加されています。
import に http
を追加します。
import 'package:flutter/material.dart';
// 以下の行を追加
import 'package:http/http.dart' as http;
as http
とあるのは import
したパッケージのプレフィックスをつけるためです。
プレフィックスをつけない場合は、そのパッケージが公開している名前を直接使うことになります。ここでは http
パッケージのものを使っていることが分かりやすいようにプレフィックスをつけます。
import 'package:http/http.dart';
var resp = get('https://example.com')
import 'package:http/http.dart' as http;
var resp = http.get('https://example.com')
さて http
パッケージを使ってみましょう。以下のコードでは _fetch
メソッド内で HTTP 通信によってデータを取得してきて、そのデータをそのまま画面に表示しています。_MyHomePageState
の中を以下のように実装してみましょう。
class _MyHomePageState extends State<MyHomePage> {
String _data = '';
@override
void initState() {
super.initState();
_fetch();
}
void _fetch() async {
var resp = await http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
setState(() {
_data = resp.body;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(_data),
),
);
}
}
initState
は State
が作られたときに1度だけ呼び出されるメソッドで、通常はここで初期処理を行います。
HTTP 通信で得られたデータをインスタンス変数 _data
に代入しています。これを setState
というメソッドに渡した無名関数の中で実行しています。setState
は Flutter の State
が提供しているもので、Flutter に「状態が変わった」ことを通知します。その結果、Flutter は最新の状態で該当の Widget を描画します。状態を変更した場合は、setState
を使うことで再描画させましょう。
また _fetch
メソッドには async
キーワードがついています。これは非同期処理を行うメソッドを示しています。async
メソッド内では await
キーワードによって、非同期処理の結果を待つことができます。http.get
関数の戻り値は Future
という「いずれ結果が得られる」非同期処理における結果オブジェクトで、この実装ではキーワード await
によって Future
が完了するのを待って、結果を取り出しています。
async / await を使わない場合は以下のよう then
メソッドを使ってコールバック方式で書くことができます。
void _fetch() {
var future = http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
future.then((http.Response resp) {
setState(() {
_data = resp.body;
});
});
}
HTTP で取得したデータは JSON 形式になっています。これを変換して Dart で扱いやすい形式にしましょう。
Dart 標準の convert
パッケージを import
します。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
json.decode
関数によって、データを List<dynamic>
に変換します。今回のデータでは、リスト内には文字列しかないので、さらに List<String>
に変換しています。また、そのリストを ListView
を使ってリスト表示しています。
class _MyHomePageState extends State<MyHomePage> {
List<String> fruits = <String>[];
@override
void initState() {
super.initState();
_fetch();
}
void _fetch() async {
var resp = await http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
setState(() {
List<dynamic> list = json.decode(resp.body);
fruits = list.map<String>((dynamic elem) {
return elem as String;
}).toList();
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView(
children: fruits.map<Widget>((String fruit) {
return new ListTile(title: new Text(fruit));
}).toList(),
),
);
}
}
リストの各行に「お気に入り」ボタンを付けてみましょう。
お気に入りかどうかは、インスタンス変数 favorites
で管理します。
ListTile
の trailing
プロパティは、リスト行の後ろ(右)側の Widget です。ここに IconButton
を使って、タップできるボタンを配置します。
IconButton
の onPressed
で「お気に入り」の状態を変更する処理を書きます。
class _MyHomePageState extends State<MyHomePage> {
List<String> fruits = <String>[];
// お気に入りかどうかの状態を管理するMap
Map<String, bool> favorites = <String, bool>{};
@override
void initState() {
super.initState();
_fetch();
}
void _fetch() async {
var resp = await http.get('https://gist.githubusercontent.com/najeira/4ea8c4ca93570dfb1468fae5c8d6c616/raw/4d61f74e66e81b1336e965056a977fe7e906cf5a/fruits.json');
List<dynamic> list = json.decode(resp.body);
setState(() {
fruits = list.map<String>((dynamic elem) {
return elem as String;
}).toList();
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView(
children: fruits.map<Widget>((String fruit) {
// お気に入りかどうか
bool fav = favorites[fruit] ?? false;
return new ListTile(
title: new Text(fruit),
// お気に入りボタン
trailing: new IconButton(
// アイコンはお気に入りかどうかで見た目を変える
icon: new Icon(
fav ? Icons.favorite : Icons.favorite_border,
color: fav ? Colors.red : Colors.grey,
),
// タップされたらお気に入りの状態を変える
onPressed: () {
bool fav = favorites[fruit] ?? false;
setState(() {
favorites[fruit] = !fav;
});
},
),
);
}).toList(),
),
);
}
}
Map
の要素へのアクセス favorites[fruit]
で、要素がない場合は null を返します。??
は、左辺が null の場合に右辺を返す演算子です。このため Map
に要素がない場合は false
(お気に入りではない)となるようにしています。
- https://api.dartlang.org/stable/1.24.3/dart-core/Map-class.html
- https://docs.flutter.io/flutter/material/IconButton-class.html
- https://docs.flutter.io/flutter/widgets/Icon-class.html
- https://docs.flutter.io/flutter/material/Icons-class.html
お気に入りボタンにアニメーションをつけてみましょう。
FlutterではアニメーションもWidgetです。
アニメーションするお気に入りボタンのWidget FavIcon
を以下のように実装します(ファイルの末尾に追加しましょう)。
class FavIcon extends StatefulWidget {
FavIcon({Key key, this.fav}) : super(key: key);
final bool fav;
@override
State<StatefulWidget> createState() {
return new FavIconState();
}
}
class FavIconState extends State<FavIcon>
with SingleTickerProviderStateMixin {
AnimationController controller;
@override
void initState() {
super.initState();
controller = new AnimationController(
value: 1.0,
vsync: this,
duration: const Duration(milliseconds: 700),
);
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
@override
void didUpdateWidget(FavIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.fav != widget.fav) {
controller.forward(from: 0.0);
}
}
@override
Widget build(BuildContext context) {
return new ScaleTransition(
scale: new CurvedAnimation(
parent: controller,
curve: Curves.bounceOut,
),
child: new Icon(
widget.fav ? Icons.favorite : Icons.favorite_border,
color: widget.fav ? Colors.red : Colors.grey,
),
);
}
}
また、これを使うリストビュー側の実装として _MyHomePageState
の build
メソッドは以下のようになります。IconButton
の中身を変えて FavIcon
を使うようにしています。
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView(
children: fruits.map<Widget>((String fruit) {
bool fav = favorites[fruit] ?? false;
return new ListTile(
title: new Text(fruit),
trailing: new IconButton(
// FavIconにお気に入りの状態を渡してWidgetを作る
icon: new FavIcon(fav: fav),
onPressed: () {
bool fav = favorites[fruit] ?? false;
setState(() {
favorites[fruit] = !fav;
});
},
),
);
}).toList(),
),
);
}
FavIconState
を見てみましょう。AnimationController
がアニメーションにおける値の変化を管理してくれます。これをインスタンス変数に持っています。例によって initState
で初期化をしています。また dispose
で後処理をしています。
また FavIconState
は with SingleTickerProviderStateMixin
となっています。これは、アニメーションのための Tikcer を提供する機能を State へ "Mixin" して持たせるためです。
didUpdateWidget
はこの State に関連する Widget (ここでは FavIcon
)の設定が変更されたときに呼び出されます。つまり変数 bool fav
が変更された場合です。このときに AnimationController
を値 0.0 から forward
させているので、お気に入り状態が変わったときにアニメーションが開始します。
build
内ではアニメーション対象である Icon
を ScaleTransition
でラップしています。CurvedAnimation
はアニメーションの変化を定義するクラスで、これに AnimationController
を渡しているため、そのコントローラーの変化によって最終的に Icon
のサイズが変化するようになっています。
さて、ボタンを押したときに、大きさが変化するアニメーションが確認できたでしょうか?
- https://docs.flutter.io/flutter/animation/AnimationController-class.html
- https://docs.flutter.io/flutter/widgets/SingleTickerProviderStateMixin-class.html
このコードラボは以上です。
Flutterのいくつかの要素は確認できたと思います。
- プロジェクト作成
- ホットリロード
- リストビュー
- HTTP通信
- 状態を持ってみる
- アニメーション
お疲れ様でした!
更新用メモ: