はじめに
最近,僕の周りで話題のFlutterを触ってみました。
Write Your First Flutter Appの内容をそのままやっていきます。
前提
- Dart, Flutterはインストール済
- 開発環境は作成済(IntelliJを使用)
- iOSで実行を確認
Step1: Hello World
とりあえず,Flutterでアプリ作成します。
$ flutter create first_app
このコマンドでFlutterのアプリが作成されます。
変更するのは,基本lib/main.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: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}
よくわからないけど,とりあえずRun。
mainメソッドの=>
は1行で関数を書くときに使える記法。MyAppインスタンスを渡してアプリを実行してるっぽいです。
たぶん,Javaとか読める人はなんとなくやってることわかる気がする。。
全体のViewをStatelessWidget
が司っていて,Viewをbuildしてます。MaterialApp
がtitle, home, bodyプロパティを持っていてそれぞれに任意のview widgetを渡すことで画面を描画しているようです。
Dart,始めてでもなかなか読みやすいですね。
Step2: 外部のパッケージを使う
パッケージを導入してみます。FlutterのパッケージはFlutter Packagesで探せます。ここでは,english_wordsを導入しています。
Flutterではパッケージをpubspec.yaml
ファイルで管理します。
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.0
english_words: ^3.1.0
一番下にenglish_words
を追加。
追加するとIntelliJの上の方にPackages getというのがでるのでそれを押せばpackageが導入できます。
lib/main.dart
を以下のように書き換えます。
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
// child: new Text('Hello World'),
child: new Text(wordPair.asPascalCase),
),
),
);
}
}
これで,bodyに表示される文字列が変わるリロードする度に変わります。
非常に簡単ですね。
Step3: Stateful Widgetを追加する
Stateless Widgetはその名の通り,動きがないウィジェットです。
Stateful Widgetのライフタイムの中で変化する状態を保持します。Stateful Widgetは少なくとも2つのクラスを使用します。
- Stateクラス
- StatefulWidgetクラス
StatefulWidgetクラスはStateクラスのインスタンスを生成するクラスで,それ自体では変化することはないですが,StateクラスはWidgetのライフタイムを保持しています。
ここでは,RandomWordsStateクラスを生成するRandomWordsというStatefulクラスを追加してみます。RandomWordsStateクラスは,Widgetに表示されたお気に入りワードのペアを保持します。
まず,stateful RandomWordsウィジェットをmain.dart
に追加します。
MyAppクラスの外に記述することに注意!
class RandomWords extends StatefulWidget {
@override
State<StatefulWidget> createState();
}
次に,RandomWordsStateクラスを追加します。全てのアプリのコードは,RandomWordsウィジェットの状態を保持するこのクラスに属します。このクラスが具体的にすることは,
- ユーザのスクロールによって生成されるワードのペアを保持する
- ユーザがハートアイコンをトグルすることでリストに追加/削除されるお気に入りワードのペアを保持する
の2つです。
そして,このクラスを追加すると,buildメソッドがないよと怒られるので,buildメソッドを追加します。このbuildメソッドでは,ワードペアの生成を行います。
MyAppクラスからRandomWordsStateクラスへワード生成のコードを移しておきます。buildメソッドは以下のようになります。
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
MyAppの方のワード生成のコードは削除しておきます。
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return (new Text(wordPair.asPascalCase));
}
}
ここまでで一旦実行すると,Step2と同じ用にリロードする度に中央のワードが変更されるという動作を確認できると思います。
Step4: 無限Scroll Viewを作成する
このステップでは,RandomWordsStateクラスを拡張して,ワードのリストを生成して表示します。
ListViewのbuilder
ファクトリーコンストラクタは,要求に応じてリストビューを作成します。
まず,提案されたワードを保持する_suggestions
リストをRandomWordsStateクラスに追加します。
ちなみに,変数の頭のアンダースコア(_
)は,Dartにおいてプライバシー変数であることを意味します。
更に,biggerFont
変数もフォントサイズを大きくするために定義しておきます。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return (new Text(wordPair.asPascalCase));
}
}
次に,_buildSuggestions()
関数を追加します。このメソッドは提案されたワードを生成するリストビューを作成する関数となります。
ListViewクラスはビルダープロパティitemBuilder
(ファクトリビルダーとコールバック関数)
BuildContextとイテレータ i がこの関数に渡されるパラメータとなります。イテレータは0から始まり,ワードを提案する度に関数が呼ばれてインクリメントされます。
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// itemBuilderのコールバックはワードが提案される度に呼ばれ,ListTitleの行にそれを配置する
itemBuilder: (context, i) {
if (i.isOdd) return new Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
}
このままでは動きません。なぜなら,_buildSuggestions
関数は_buildRow
をワードペア毎に読んでいるからです。この関数は,リストタイトルに次のステップで注目する行生成のための関数で,新しいペアを表示する役割を担います。
_buildRow
関数は,以下のように実装されます。
class RandomWordsState extends State<RandomWords> {
...
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase, style: _biggerFont,
),
);
}
}
そして,RandomWordsStateを以下のように変更します。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
...
}
最後に,MyAppクラスを変更します。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new RandomWords(),
);
}
}
これで,ユーザがスクロールすると自動でワードを生成して表示するリストビューが完成です。
Step5: インタラクティブなアプリを作成する
次に,各行にお気に入りボタンを追加します。そのためには,ユーザがタップできるようにしないといけませんね。
まず,RandomWordsStateに_saved
セットを追加します。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
...
}
次に,_buildRow
関数にお気に入りに追加されているかを確認するためのalreadySaved
を追加し,ハート型のアイコンをリストに表示します。
class RandomWordsState extends State<RandomWords> {
...
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase, style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
);
}
ここまででリロードすると,リストの行の右端にハート型のアイコンが表示されているはずです。
最後に,タップできるように_buildRow
関数を変更します。
buildRow
関数の完成形は以下。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase, style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
Step6: 新しいScreenに誘導する
前のステップでお気に入りボタンを追加したので,このステップではお気に入りリストの画面を追加して,画面遷移をやってみます。
Flutterでは,Navigatorが画面遷移はアプリのルートを含むstackを管理します。
- Navigatorのスタックにルートがプッシュされると,そのルートに画面が更新される
- NavigatorのSタックからポップされると,前のルートに戻る
では,実際に追加して遷移処理してみます。
まず,RandomWordsStateのbuildメソッドでAppBarにリストアイコンを追加します。ユーザがこれをクリックすると,お気に入りが追加要素をもった新しいルートがNavigatorにプッシュされ,アイコンを表示します。
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
actions: <Widget>[
new IconButton(icon: new Icond(Icons.list), onPressed: _pushSaved)
],
),
body: _buildSuggestions(),
);
}
...
}
次に,RandomWordsStateクラスに_pushSaved()
関数を追加します。
class RandomWordsState extends State<RandomWords> {
...
void _pushSaved() {}
}
ここでアプリをホットリロードしてみると,AppBarにリストアイコンが追加されていることがわかります。
せっかくアイコンを追加したので,タップしたときの処理を追加します。
ユーザがAppBarのリストアイコンをタップした際に,ルートを生成してNavigatorスタックにプッシュします。このアクションで,新しいルートを表示して画面を変更します。
新しいページの内容は,無名関数MaterialPageRouteのbuilder
プロパティでビルドされます。
下のように,NavigatorのスタックにルートをプッシュするNavigator.push
を追加し,MaterialPageRouteとそのビルダーを追加します。ここでは,ListTileの行を生成するコードを追加しておきます。
ListTileのdivideTiles()
メソッドが各ListTileの間に水平のスペースを入れてくれます。
devided
変数が最後の行を保持していて,これは簡易関数toList()
によってリストに変換されます。
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
);
final divided = ListTile.divideTiles(
tiles: tiles,
context: context
).toList();
})
);
}
ビルダープロパティはScaffoldを返します。Scaffoldは新しいルートのためのAppBar,"SavedSuggestions"を持っています。新しいルートのbodyはListTiles行を含むListViewで構成されていて,各行はdividerによって分割されています。
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
);
final divided = ListTile.divideTiles(
tiles: tiles,
context: context
).toList();
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided)
);
}
)
);
}
ホットリロードし,適当にお気に入りを追加して画面遷移すると,新しい画面が表示されるはずです。ちなみに,return new Scaffold
以下を書かないと,次の画面が表示されず真っ黒な画面が表示されます。
ちなみに,前の画面に戻るためにわざわざNavigator.pop
を実装する必要はありません。勝手に新しい画面のAppBarに自動で"戻る"ボタンが追加されているはずです。
Step7: テーマを使ってUIを変更する
最後のステップですが,アプリのテーマで遊んでみます。
アプリのテーマはThemeDataクラスをいじることで簡単に変更することができます。今はデフォルトテーマを使っているので,白をベースとした色に変更してみます。
MyAppを次のように変更します。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
theme: new ThemeData(
primaryColor: Colors.amber
),
home: new RandomWords(),
);
}
}
ホットリロードすると色がかわることがわかります。
MaterialライブラリのColorsクラスはたくさんの色の定数を提供していますので,いろんな色に変更して遊んでみると良いと思います。ホットリロードしながら変更すると簡単に確認しながら変更できます。
終わりに
ここまでやった感想としては,Flutterは非常によく作られてるなと思いました。
dartも始めてでも直感的でわかりやすいし,学習コストは低そうという印象。
本気でいろいろやり始めると壁があるのかもしれませんが,個人的にはやってみる価値はあるかなという感じです。