flutterをやるよ
環境は以下の通りです。
MacOS Catalina
セットアップ
flutter本体を落とす
install
上記からOSに対応したflutterを落としてきます。
パスを通します。落としてきたflutterの置き場所はpwd使っているので、.zshrcがある場所と同階層に。
export PATH=$PATH:`pwd`/flutter/bin
再度、.zshrcを読み込ませます。
$ source ./.zshrc
エディタの用意(VScodeを筆者は選択)
VScode上で下記のプラグインをインストールします。
- Flutter
- Dart
- Awesome Flutter Snippets
iOSとandroidのSDKを用意
XcodeとAndroid Studioをダウンロードしてきます。
最後に
下記のコマンドを打って何も問題がないと表示されたら、完了です。仮にエラーが出たとしても、このdoctorコマンドで出るエラーは何が足りないかを明瞭に示してくれるので、ググったりしながらてんやわんやになる心配はないです。
$ flutter doctor
:
:
• No issues found!
他のflutterコマンド
flutterのバージョンアップ用のコマンド
$ flutter upgrade
flutterのリリースチャンネルを確認したり、引数にチャンネルを指定すれば、そのバージョンを見るようになる。基本的にstable(安定版)に向いていれば問題ない。
$ flutter channel
Flutterに関して
メインで編集するファイルは下記の通り
- pubspec.yaml
このファイルによってflutterアプリのasset(configuration fileやjsonやimageを含む)や、依存関係を管理する。パッケージ管理ファイルになる。
$ flutter pub get
上記のコマンドを使うことでパッケージを引っ張ってくることができる。パッケージを取得させたら、main.dartファイルの方でimport宣言をさせることで使用できる
下記のようになる。最上段に書いとくのが基本。
import 'package:flutter/material.dart'; //FlutterのマテリアルデザインによるUIウィジェットが入っているパッケージ
import 'package:english_words/english_words.dart';
- lib/main.dart
アプリの本体になるメインソースコードを記述するファイル。widget単位で機能を実装することになる。widgetは後述。
flutterの構造について
widgetって言葉がいやってほどに出てくるので〇〇widgetはなんじゃったか?みたいにごっちゃにならないようにしていきたい。
取り敢えずここからはチュートリアルで作成した**「無限スクロールランダム名前生成ファボ機能でリスト作成アプリ」**のソースコードを例にして解読していきます。このアプリ自体に関しては下記の公式サイトから作れます。
Widget
UIを設計する際にはこのWidgetが最小単位となって形成される。WidgetはUIに直結する機能でも、バックエンドでの処理でも実装できる。widgetはstatlessかstatefullのどちらかを継承している形で作成される。故に、widgetはstatelesswidgetとstatefullwidgetの2種類存在する。
widgetは基本的に下記のような性質を持つ
- Widgetクラスは必ず他のWidgetを返すbuildメソッドをもつ
- buildメソッドはWidgetの代わりにテキストを返すこともできる
- Widgetの入れ子でflutterは構成される
WidgetTree
ベースとなるwidgetの中にwidgetを組み込んで更にその中にまた別のwidgetを組み込むといった階層構造になる。これをwidget treeと呼ぶ。
StatelessWidget
状態を持たないwidget。つまり、静的なUIを提供することを意味し、タッチしたり、スライドさせたり、その他動作するような機能を持たないwidgetのこと。
- StatelessWidgetを継承した1つのクラスで構成されている
- StatelessWidgetはbuildメソッドを持つ
- Widgetもしくはテキストを返す
- returnするWidgetはStatefulWidget(MaterialAppとか)などになる
上記のような性質をstatlesswidgetは持っている。
StatelessWidget
|
MaterialApp
|- title アプリのタイトルに相当
|- home アプリ起動直後に表示されるページに相当
statelesswidgetのソースコードは以下の通り
void main() => runApp(MyApp()); //main関数がrunApp関数を実行し、MyAppウィジェットを呼んでいるここはパッケージのimport宣言の次に来る部分でwidgetではない。アプリの始まりになる部分
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
primaryColor: Colors.white,
),
home: RandomWords()
);
}
}
コードを上から解読していたものを箇条書きに擦ると下記のようになります。
-
extendsを使ってサブクラスを継承させています。
-
@overrideメソッドは先祖クラスでも定義されているが、現在のクラスで何か他のことを行うために再定義されていることを意味する。
-
buildメソッドを使って返り値に設定されている内容をwidgetとしてビルド
-
buildメソッドの引数には、BuildContextとcontextが渡される
-
BuildContextは、build内の処理を実行し、widgetとして生成し、contextによって、widgetがツリー内のどこに位置するかを意味
今回のbuildはMaterialAppというクラスから生成されたwidgetが返るようになっています。
MaterialAppの中にもいくつかプロパティが存在し、そのプロパティが直接表示されるものを決定づけています。titleや、themeなどは直感的に分かると思います。home部分が何やら別のクラスのwidgetを呼んでそうですが、ここで呼ばれた widgetが後述のstatefulwidgetになります。
StatefulWidget
statelessが静的ならこっちは動的な機能を持ったwidgetという認識でいいと思います。実際に触って挙動があるアプリこそ、アプリの本懐。故にここからがようやくアプリがアプリらしくなるようなコードの解析になっていきます。
Statefullwidgetは2つのクラスを用いて機能します。
StatefulWidgetクラスと、Stateクラスです。
StatefulWidgetクラスはStateクラスをインスタンスとして生成しwidgetを呼び出す際に必要になるクラスです。先のStatelessWidgetのhomeで書かれていたRandomwords()がこれにあたります。そして、2つ目のStateクラスこそ、アプリの挙動を定義する本格的な中身のコードになります。
statefullwidgetクラスの特徴は下記の通り
- Widgetにstateを継承させて拡張している
- StatefulWidgetはbuildメソッドを持たない
- createStateメソッドを持ち、これがStateクラスを返す
- stateが変化したときに再描画を指示するメソッドが存在
- StatefulWidget自体はstateクラスを返すようになっているだけのシンプル構造
- 複雑な処理等はStateクラスで持つため、ここのコードが長くなる
stateクラスの特徴は下記の通り
- Stateクラスはbuildメソッドをもつ
- Widgetを返すときに定義したメソッドをアクションとして組み込める
- returnできるWidgetはいくつも種類があり、TextやImage、TextInput、Scaffold等
2つのクラスに分離している理由は色々あるようですが、Stateにbuildメソッド置いた方がバグ回避できるとかみたいです。
Putting the build function on State rather than StatefulWidget also helps avoid a category of bugs related to closures implicitly capturing this.
実際のStatefulWidgetを記述したものが下記になります。ここから2つのクラスを見ていきます。
class RandomWordsState extends State<RandomWords> {
final List<WordPair> _suggestions = <WordPair>[];
final Set<WordPair> _saved = Set<WordPair>();
final TextStyle _biggerFont = TextStyle(fontSize: 18.0);
void _pushSaved() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map(
(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final List<Widget> divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('無限スクロールランダム名前生成ファボ機能でリスト作成アプリ'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
});
}
Widget _buildRow(WordPair pair) {
final bool alreadySaved = _saved.contains(pair);
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
}
class RandomWords extends StatefulWidget {
@override
RandomWordsState createState() => new RandomWordsState();
}
さっとStateクラスを見て、「ほほぅ、これがwidget treeじゃな」ってなるのではないでしょうか?全ての動的処理はstateで行われるので、非常にコードが長いです。しかし、小さいwidgetがいくつも並んで長いコードを形成しているように書いてあるので、細かく分解して読むように習慣づけていきます。
StatefulWidgetクラス
先に短いこっちのクラスから読解します。
class RandomWords extends StatefulWidget {
@override
RandomWordsState createState() => new RandomWordsState();
}
createState()で実際にStateクラスの中身、つまりwidget treeを返しています。newはStateクラスをインスタンスとして生成する役割を持っています。
Stateクラス
いよいよアプリの実際の挙動を決める部分のコードを読んでいきます。
class RandomWordsState extends State<RandomWords> {
final List<WordPair> _suggestions = <WordPair>[];
final Set<WordPair> _saved = Set<WordPair>();
final TextStyle _biggerFont = TextStyle(fontSize: 18.0);
void _pushSaved() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map(
(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final List<Widget> divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('無限スクロールランダム名前生成ファボ機能でリスト作成アプリ'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
});
}
Widget _buildRow(WordPair pair) {
final bool alreadySaved = _saved.contains(pair);
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
}
StateクラスはState<>の中でStatefulWidgetクラス名(ソースコードではRandomWords)を宣言することで初めてStateクラスとして定義されます。
final List<WordPair> _suggestions = <WordPair>[];
final Set<WordPair> _saved = Set<WordPair>();
final TextStyle _biggerFont = TextStyle(fontSize: 18.0);
まず、上記のコードから読み取れることを列挙します。
定数定義
最初に3つ、finalを使って定数定義をしています。
finalは変数修飾の1つ。
finalが指定された変数は、プログラム開始後のある時点で一回だけ初期化され、初期化以降は、代入などを通じて変更されない/できないことが保証される(再代入不可)。なお、finalな変数が「指す先」のメモリ領域の内容が変更されることについての制約はない。
[Dartの変数定義時の修飾static/final/const、そしてconst constructorについて]
(https://qiita.com/uehaj/items/7c07f019e05a743d1022)
ということです。
型宣言
ここでの定数定義には、型宣言も同時にされています。Dartは動的型付が可能ですが、型宣言をすることもできます。宣言の種類はprimitive、dynamic variables、genericsが存在し、genericsを使った宣言をfinalかconstの定数宣言時にする場合、これの直後に型宣言をすることができます。
データ型だけでなく、クラスとかも型宣言の1つとしてカウントされることですね。Dartは全てのデータをオブジェクトとして扱うから、クラスから生成されたオブジェクトも型として扱えるってことなんでしょうね、多分。
カンマやセミコロン
セミコロンは関数を呼び出した行末につけることが必須になっている。
カンマに関しては、引数をいくつもつなげる時に使うが、引数の最後にもくっついている時があるが、これは、あってもなくても良いが、引数を後から追加する時によく忘れがちなので、先につけておくといい、くらいの気持ちで書いておく。
final List<WordPair> _suggestions = <Wordpair>[];
上記みたいなやつですね。ジェネリクスの書き方で、Listって書いてあります。<>の中身はこれ、english_wordパッケージに用意されているクラスですね。
void main() {
// primitives
int provocation = 114514;
double ugly = 23.19;
bool justice = true;
List kakugo = ["JO", "JO", "GW"];
Map stand_users = {"Giorno Giovana":"Gold Experience", "Guido Mista ":"Sex Pistols"};
// dynamic variables
var pig = "Polpo";
var count = [3,2,1,"GO!!"];
// generics
List<int> add_num = [1,3,5,7,9];
Map<int, String> star_dust = {0:"Kujou", 1:"Kakyouin", 2:"Avdol"};
// final and const
final String stand = "Star Platinum";
const String stand = "Hierophant Green";
}
メンバ変数のアクセス制御
rubyのようにpublicやprivateを置いてアクセス制御するのではなく、Dartでは、_を用いることで、private変数として扱い、アクセス制御をします。
Dart Collections
rubyでいうところの配列やハッシュを総称してDartではコレクションズと呼んでいるようです。
Dart Collections -- maps, lists,queues, sets
取り敢えず、初期段階で見て取れるDartの基本文法はこんなもんだと思います。
void _pushSaved() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map(
(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final List<Widget> divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}
続いて上記のコードを読んでいきます。ここはリストアイコンが作成されてそこをタップすると新しい画面へ遷移し、遷移先での画面を表示するところまでの機能になります。
void
voidを先行して宣言されたメソッドは返り値を返さなくなる。オブジェクトの操作がそのメソッド内部だけで完結していることを明示してくれる感じですかね。
この辺から英語の方のドキュメントも説明あんまり詳しくないんですよね・・・もうチョッチ細かく書いて欲しいもんです。「Hey,add this code quickly. Got it? OK! Next!!」みたいなやっつけ感のあるノリで書いてあるんです。
_pushsavedを書くことでリストアイコンが出てきます。
Navigatorを使って、画面遷移をできるようにします。今回のNavigatorでは進む遷移をしたいので、pushメソッドを使っています。戻る遷移ではpopメソッドを用います。
pushの中には、遷移先での表示するものが詳しく書いてあり、それをcontextとしてNavigatorに渡している・・・ような感じだと思います。
void _pushSaved() {
Navigator.of(context).push(
上記部分まで読みました。
更に詳しく書いてある部分を見ていきます。
MaterialPageRouteで全画面ダイアログでのモーダルを出すことができます。
builderを使って、実際に表示させる部分をビルドしています。
Iterableを使って型宣言しているのは、mapメソッド自体が繰り返しを行うものだから・・・だと思います、多分。
Maps, and their keys and values, can be iterated. The order of iteration is defined by the individual type of map.
そして、その繰り返される中身は、ListTyleオブジェクトだよってのが読んでわかりやすくなっている配慮でしょうね・・・多分。
ListTyleを使ってリストを作成しています。asPascalcaseで名前が2つくっついた文字列が返る、以前に定義した_biggerFontの文字サイズで出てきます。ここまでがtiles定数になりますね。
MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map(
(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
上記まで読みました。
続いて、2つめの定数定義を読んでいきます。ListTileに対してチェーンメソッドさせています。toListで繰り返されるリストを作成し、その作成されたリストが、divideTilesによって1pixelの境目のあるタイル状にしてくれています。タイルの中身は先に定義した定数tilesが中身として入っていることがわかります。このメソッドはbuildされたcontextも必要なので、contextも一緒に引数に入っています。
final List<Widget> divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
上記まで読みました。
次の部分でリストアイコンをタップする遷移先の機能に関しては終わりになります。
returnされているScaffoldによってここまで読んできたものが、アプリの表示されるオブジェクトとなってMaterialPageRouteの返り値になります。
Scaffoldはキーワード引数として用意されているプロパティに必要なウィジェットを組み込むことで、マテリアルデザイン準拠のアプリケーションの文字通り土台(Scaffold)になってくれます。ここではappBarとbodyのプロパティが用意され、それぞれAppBarとListViewというウィジェットクラスを呼んでいます。ListViewの中に先に定義したdividedが入っているのもわかると思います。
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}
上記まで読みました。ようやく、これで遷移ページを作成できたことになります。次はメインページを作成している部分を読んでいきます。
ここからアプリ立ち上げた時の画面を出しているウィジェットの部分のコードを見ていきます。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('無限スクロールランダム名前生成ファボ機能でリスト作成アプリ'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
ここは、先ほどと同じようにScaffoldで作っています。IconButtonでは、前に作成されたリストボタンをボタンとして機能させています。タップされた時点で、_pushSavedの中の処理が始まります。
また、bodyで呼んでいるのは次のウィジェットです。
Widget _buildSuggestions() {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
});
}
上記のウィジェットは無限スクロールする機能を実現させています。
Listview.builderでリストを作成し、itemBuilderは、ListViewのコールバック関数として呼ばれ、引数にcontextと、無名関数のiがあります。このiはitemBuilderが呼ばれるたびに、0から加算されていく繰り返しを作り出します。そして、繰り返される処理は、「iが奇数の場合、リストタイルの間の区切れを作り、偶数である場合、_suggestions.length、つまり、最初に定義した0配列異常である場合、10個の単語のペアを作るようにしていて、returenとして、もう1つのウィジェットである_buildRowを返す」と言うものになる。
1つ疑問なのは、この作りによってユーザーがスクロールすることで自動でリストが無限生成されると言うことですね。itemBuilderが画面の操作処理を拾ってるのかな・・・ここだけ謎かつ、ここ1番謎にしちゃダメなとこだと思う・・・誰か教えてください。
_buildSuggestionsで呼ばれているこのコードの最後のウィジェット、_buildRowを見ていきます。
Widget _buildRow(WordPair pair) {
final bool alreadySaved = _saved.contains(pair);
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
最初に定義した連想配列の_savedにvalueがあるかを確認してtrueのbool値を持つようにします。(containはvalueがあった時のtrueしか返さず、falseは返さない)
ListTileを作るのは遷移ページとやることは同じですが、次のハートアイコンを作るところがこのウィジェットの違うところです。
trailingプロパティでIconを選択し、alreadySavedを条件として使い、条件式を?で作っています。1つは trueの時に、アイコンを全塗り仕様のものを、falseの時にはアイコンのボーダーしかないものを出すようにし、カラーもそれに合わせて切り替わるように条件分岐してあります。ボーダーの時はカラー指定をせず、nullにしてありデフォルトの灰色が出るようになっています。
onTapでユーザーの画面タッチした時のイベント発生処理を書くことができます。setStateは今書いているウィジェット自体がstatefulWidgetなので、ステータスの更新が必要であるから使われています。(setStateを使わないStatefulWidgetもあるそうですが、それはまた触れた折にアウトプットしていきたいと思います)
SetstateのあるStatefulWidgetとないStatefulWidget
取り敢えず、ここではタップされた際に、valueがある際にはそれを削除し、なければ、追加するようにして、ファボ機能を実現しています。
以上、読解終わり。
まとめ
今回のコードのウィジェットツリーを可視化すると下記のような感じになると思います。
MyApp(StatelessWidget)
|
RandomWords(StatefulWidget)
|
RandomWordsState(StateWidget)
|- _pushSaved 遷移した後の画面
|- Widget build アプリの初期画面を出すScaffold
|- Widget _buildSuggestions 無限スクロールモデル
|- Widget _buildRow リストの中身生成とステータス管理
ウィジェットの積み重ねでアプリが作られていくことはよく理解できました。多少Dartの書き方も知れました。後は作りたいと思った物に対して、メソッドを探してくる感じになるんでしょうかね・・・多分。