Flutter の codelab part1 の翻訳です。
https://flutter.io/get-started/codelab/
英語力皆無 & Dart未経験 & 初Flutter な自分が書いているので、間違いや理解しづらい点などあるかと思います。変なところがあればご指摘ください
Write Your First Flutter App, part 1
これからスタートアップ会社の名前の候補を生成するアプリを作ってみます。このアプリはテーブル(List)の各セルに候補となる文字列を表示し、ユーザは表示された候補の中からベストな名前をselectしたりunselectすることができます。コードはユーザのスクロールに合わせて名前を遅延生成します。ユーザがスクロールすれば名前はそれに従って生成され、またどこまでもスクロールすることができます。
Part 1 で学ぶこと
- iOSとAndroidの両方で自然な見栄えのアプリをFlutterで書く方法
- Flutterアプリの基本的な構造
- 機能を拡張するためのパッケージ (package)の探し方と使い方
- より高速な開発のためのホットリロード (hot reload) の利用
- Stateful widget の作り方
- 無限かつ遅延的にロードされるリストの作り方
Step 1: Create the starter Flutter app
- Replace the contents of
lib/main.dart
の中身を以下の内容に置き換えてください。もともとあったコードはすべて削除し、以下のコードを貼り付けてください。このコードは画面の中央に「Hello World」と表示させるものです。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter'),
),
body: Center(
child: Text('Hello World'),
),
),
);
}
}
- IDEの緑色の矢印ボタンを押して実行すると、AndroidやiOSに出力されます。
iPhoneXだとこうなります。
Androidだとこう。
Observation
- この example では Material app を作っています。 Material とはモバイルやWebで標準的な visual design languageです。Flutter はリッチな Material Widget を提供します。
-
main
メソッドは=>
(太矢印, fat arrow) を利用しています。=>
表記は1行の関数やメソッドを記述するために使います。 -
MyApp
はStatelessWidget
を継承していてそれ自体が widget です。 Flutter の世界では、ほぼすべてが widget です。 alignment, padding, layout なども widget です。 - Material Library に所属する
Scaffold
widget は app bar, title, ホーム画面用の widget ツリーを保持する body プロパティを提供します。 widget のツリーはかなり複雑になりえます。 - widget の主な役割は
build()
メソッドを提供することです。 これは、下位レベルの widget を用いてその widget をどのように描画するべきなのかを記述するものです。 - 今回の例では body は
Text
widget を child として保持するCenter
widget で成り立っています。Center
widget はそのサブツリーを画面の中央に配置します。
Step 2: Use an external package
ここでは、 english_words
という open-source パッケージを使ってみましょう。このパッケージは数千よく使用される英単語といくつかの便利な関数を提供します。
english_words
パッケージは他の多くのパッケージと同様に、 pub.dartlang.org で見つけることができます。
-
pubspec ファイルは Flutter アプリにおいて asset と 依存するパッケージなどを管理するものです。
pubspec.yaml
の依存パッケージリストにenglish_words
(3.1.0以上) を追加しましょう。dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.0 english_words: ^3.1.0
-
IDE の Editor View にて、
Packages Get
をクリックします。これはパッケージをプロジェクトに引っ張ってきます。Console画面にこんな出力がされます。> flutter packages get Running "flutter packages get" in `YOUR_APP_NAME`... Process finished with exit code 0
これが実行されると、
pubspec.lock
が自動生成されプロジェクトに引っ張ってきたすべてのパッケージ名とバージョン番号が記述されます。 -
lib/main.dart
に新しいパッケージの import を記述します。import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart';
impor...
と入力していくと IDE はインポートするパッケージを補完してくれるでしょう。入力を完了すると、この1行がグレーアウトされます。これは(今の所)インポートしたパッケージが使用されていないことを示しています。 -
"Hello World" の代わりに english_words パッケージで生成した単語を表示してみましょう。
以下のように先程のlib/main.dart
の中身を書き換えます。import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = WordPair.random(); // ★追加 return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), ), body: Center( //child: Text('Hello World'), // ★コメントアウト child: Text(wordPair.asPascalCase), // ★追加 ), ), ); } }
ここで「Pascal case」というのは UpperCamelCase のことです。C++やSwiftなどの多くの言語におけるクラス名の命名法と同じ形式です。
-
今、アプリが動いている状態であれば、
CMD+S
(上書き保存)もしくは雷マークをクリックすることで hot reload が発動します。Hot reload を実行するたびに、ホーム画面の中央に違う名前がランダムに表示されます。なぜ毎回違う名前が表示されるかというと、wordPair
はbuild()
の中で生成されており、build()
はMaterialApp
が描画を要求するたび、または、Flutter Inspector で Platform を切り替えるたびに呼ばれるからです。
Step3: Add a Stateful widget
Stateless widgets (状態なしwidget) は immutable, つまり、すべてのプロパティは final
で、値の変更はできません。
Stateful widgets (状態ありwidget)はそれが生きている限り、状態(プロパティ)が維持・変更されます。Stateful widget を実装するためには、少なくとも2つのクラスが必要です。
- StatefulWidget
- State
StatefulWidget class が State class を作成します。StatefulWidget class は、それ自体は immutable つまり値変更できません、しかし State class は widget のが生きている限り存続します。
このステップでは、 RandomWords
という stateful widget を追加します。このクラスは、その State である RandomWordsState
クラスを生成します。そして RandomWords
widget をすでにある MyApp
stateless widget の中に child として追加します。
-
まず最小限の state class を作成します。以下を
main.dart
の最後に追加してください。class RandomWordsState extends State<RandomWords> { // TODO Add build method }
State<RandomWords>
という宣言に着目してください。これは、RandomWords
class が使用することに特化した generic State class を使用する、ということを意味します。App の状態の殆どはここに置かれます。ここにRandomWords
widget のための state が保持されます。この class はユーザのスクロール操作によって無限的に生成される word pair を保存します。また、 ユーザがハートアイコンをタップすることでトグルできるお気に入りの状態(これは Part 2 で!)も保存します。RandomWordsState
はRandomWords
class に依存します。それを追加しましょう。 -
状態を持つ
RandomWords
widget をmain.dart
に追加します。RandomWords
widget はその State class を作成すること以外、ほとんど何もしません。class RandomWords extends StatefulWidget { @override RandomWordsState createState() => new RandomWordsState(); }
この state class を追加すると、IDE は build method が足りない、と怒ります。なので次の段階で、基本的な word pair を生成する処理を build method として記述します。生成するコードを MyApp から
RandomWordsState
に移動させます。 -
RandomWordsState
にbuild()
method を追加します。class RandomWordsState extends State<RandomWords> { @override Widget build(BuildContext context) { final wordPair = WordPair.random(); return Text(wordPair.asPascalCase); } }
-
MyApp
から単語を生成するコードを削除します。class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = WordPair.random(); // ★ ここを削除 return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), ), body: Center( //child: Text(wordPair.asPascalCase), // ★この行を child: RandomWords(), // ★ こう変える ), ), ); } }
-
App をリスタート(緑の再生ボタンやCMD+Sキーで)させます。変更前と挙動は同じになるはずです。つまりHot Reload するたびに違う単語が表示されます。
もし hot reload の際に以下のような警告が出た場合、 App をリスタートさせてみてください。
Reloading…
Some program elements were changed during reload but did not run when the view was reassembled; you may need to restart the app (by pressing “R”) for the changes to have an effect.
これはおそらく偽陽性(つまり本当は問題がない)です。リスタートさせれば変更が UI に反映されていることが確認できるでしょう。
Step 4: Create an infinite scrolling ListView
このステップでは、 RandomWordsState
を拡張し、 word pair のリストを生成するようにします。ユーザがスクロールするにしたがって、リストは ListView
widget に表示されます。そしてこのリストは無限に増えます。 ListView
の builder
factory constructor を使うことで、list view を要求に応じて遅延的に作ることができます。
-
候補として生成した単語群を保存するために、
RandomWordsState
class に_suggestions
を追加します。また、フォントサイズをより大きくするために、biggerFont
変数も追加します。Dart言語では、アンダースコア
_
をつけることで、library 内からのみ見えるようにできます。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _biggerFont = const TextStyle(fontSize: 18.0); ... }
次は、
RandomWordsState
に_buildSuggestions()
を追加します。このメソッドは候補となる単語を表示するListView
を作成します。ListView
class はitemBuilder
という builder property を提供します。ここには匿名関数を渡します。渡した関数はBuildContext
と 行番号i
が渡されます。この行番号は 0 から始まり、1ずつインクリメントされます。これに応じて単語を生成します。この方式を採用することで、ユーザのスクロール操作に応じて無限に単語を提示するリストを作ることができます。 -
以下の
_buildSugge()
関数をRandomWordsState
class に追加します。(コメントは消しても良いです。)class RandomWordsState extends State<RandomWords> { ... Widget _buildSuggestions() { return ListView.builder( padding: const EdgeInsets.all(16.0), // itemBuilder は単語ごとに1回呼び出され、その単語を1つの ListTile として配置します。 // 偶数行なら、この関数は ListTile を生成し、 // 奇数業なら、この関数は Divider widget (区切り線)を生成します。 // ただし、小さなデバイスではこの線は見えづらいかもしれません。 itemBuilder: (context, i) { // 1 pixel の区切り線を各行の前に表示します。 if (i.isOdd) return Divider(); // "i ~/ 2" という文法は、 i を 2で割って整数を返します。 // 例えば、 i が 1, 2, 3, 4, 5 なら結果は 0, 1, 1, 2, 2 となります。 // これは ListView 内の実際の単語数を計算します。つまり区切り線の数を引きます。 final index = i ~/ 2; // もし利用可能な単語群の最後に到達したら... if (index >= _suggestions.length) { // ...最後まで来たら、更に10単語生成し、suggestions list に追加します。 _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); } }
_buildSuggestions()
関数は_buildRow()
を1単語ごとに呼びます。この関数は単語をListTile
に配置します。ListTile
は次のステップでリストの row (行) をより魅力的にしてくれます。 -
_buildRow()
メソッドをRandomRowrdsState
に追加します。class RandomWordsState extends State<RandomWords> { ... Widget _buildRow(WordPair pair) { return ListTile( title: Text( pair.asPascalCase, style: _biggerFont, ), ); } }
-
RandomWordsState
のbuild()
メソッドを_buildSuggestions()
を使うように編集します。単語生成ライブラリを直接呼ばないようにします。(Scaffold は Material Design の基本的なレイアウトを提供します。)class RandomWordsState extends State<RandomWords> { ... @override Widget build(BuildContext context) { // final wordPair = WordPair.random(); // ★削除 // return Text(wordPair.asPascalCase); // ★削除 return Scaffold ( appBar: AppBar( title: Text('Startup Name Generator'), ), body: _buildSuggestions(), ); } ... }
-
MyApp
の build method も更新しましょう。home をRandomWords
widget にするだけです。もともとの記述を以下のように変更します。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Startup Name Generator', home: RandomWords(), ); } }
-
App をリスタートさせましょう。どれだけスクロールしても単語がリストに表示されることが確認できるでしょう。
iOS。
Android