この記事は Androidその2 Advent Calendar 2016 の11日目です。
はじめに
この記事 を読んで、
(ほー、Dartって死んだと思ってたけどまだ開発続いてたのか。ほー、Flutterなんてものがあるのか)
とか思って実際に使ってみた。
ネタとしては、この本の内容をパクってQiitaのAPIから情報を取得してその内容を表示することにした。
Flutterとは
AndroidとiOSのアプリケーションを共通のコードで開発するためのFramework。
開発言語はDart。
公式サイト : https://flutter.io/
Github : https://github.com/flutter/flutter
Githubのレポジトリを見ていただければわかるが、結構活発に開発されている様子。
ちなみに、Dartは全く書いたことが無かった。
今回作ったもの
http://qiita.com/api/v2/items
から記事一覧を取得して表示するアプリケーション
コードはここにある。https://github.com/almichest/flutter-test
いろいろWIP。
- 本当は公開時点でCellをタップして記事の内容を表示、までやるつもりだった。
- ListViewのサイズが変
作業内容
1.セットアップ
私はMacで行ったが、Linuxでも多分同様にしていける。なお、Windowsは未対応らしい。
Flutter SDK
- 上記のGithubのレポジトリをclone
$ git clone https://github.com/flutter/flutter.git
- cloneしたflutterのディレクトリにPathを通す
$ export PATH=/your/path/to/flutter/bin:$PATH
- doctorを実行 (ちょっと時間がかかる)
$ flutter doctor
これだけ。とても簡単。
あとは、適当なAndroid端末を接続し、
レポジトリ内の適当なサンプルプロジェクトに移動して
$ flutter run
とすればサンプルアプリが動くはず。
IDE
公式では開発環境としてatomとIntellijを推奨している。
私はIntellijで開発した。
- Intellijをインストール(手順は割愛。私はCEを使っているが、特に問題なく動いている。)
- DartとFlutterのIntellij pluginをインストール
('dart', 'flutter' でそれぞれpluginを検索し、インストール & 再起動) - IntellijにFlutter SDKのパスを設定する
(Preferences
->Languages & Frameworks
->Flutter
から、上記の Flutter SDKのパスを設定)
誤ったパスを設定したりするとエラーが出るのですぐ気付く - IntellijにDartのパスを設定する (これは不要だったかも)
(Preferences
->Languages & Frameworks
->Dart
から、
Dart SDK Path:
に上記の Flutter SDK内の/bin/cache/dart-sdk
を設定)
これでアプリが作成できるようになるはず。
2. アプリを作る
0. エントリーポイント
これはサンプルを見ればすぐわかるが、main.dart
内の void main()
内で
runApp()
という関数を呼ぶ。
プロジェクトを作成したタイミングでテンプレートに入っているので、迷うことは無いはず。
1. APIクライアント
この辺はよくある
- http GET
- jsonをparse
- 対応するインスタンスを作成
の流れをdartでやっただけ。
コードは以下の通り。
(http://qiita.com/api/v2/items にしか対応していない。)
http GET で文字列を取得 (この例ではFuture<String>
のインスタンスが返る)
import 'dart:io';
import 'dart:async';
import 'dart:convert' show UTF8, LineSplitter;
class QiitaClient {
static const String _api_root = "http://qiita.com/api/";
Future<String> get() async {
var completer = new Completer();
var client = new HttpClient();
var request = await client.getUrl(Uri.parse(_api_root + "/v2/items"));
var response = await request.close();
var result = "";
await for (var contents in response.transform(UTF8.decoder).transform(const LineSplitter())) {
result += contents;
}
completer.complete(result);
return completer.future;
}
}
jsonの文字列をparse & アイテムのインスタンス作成
import 'package:firstapp/entity/qiita_item.dart';
import 'dart:convert' show JSON;
class QiitaItemsFactory {
static List<QiitaItem> create(String jsonString) {
List<Map<String, Object>> json = JSON.decode(jsonString);
return json.map((dic) {
var item = new QiitaItem();
item.url = dic['url'];
item.title = dic['title'];
item.imageUrl = dic['user']['profile_image_url'];
return item;
}).toList();
}
}
まぁ、どこかで見たようなものばかり。
2. UI
フレームワーク自体がMVVMを強く意識して作られている様子。
クラス図にするとこんな感じ。
このように、
- 一度描画したら以降再描画しないUI(ボタンとか、決まったテキストを表示するラベルとか) の作成には
StatelessWidget
を使う - 再描画が必要なUI(ScrollViewとか) の作成には
StatefulWidget
を使う
StatefulWidget
を使う際にはState
というクラスがViewModel的な役割を担っていて、
UIの状態は全てこの中に持たせるような設計になっている。
コードは以下の通り。 (StatefulWidgetを作成 / 更新 している部分だけ。)
全部 main.dartに書いているのはダサいがプロトなのでいいや、ということで。
class _QiitaItemsState extends State<QiitaApp> {
List<QiitaItem> _items;
Key _listViewKey = new Key('ListView');
@override
Widget build(BuildContext context) {
// このSearchButtonは自分で作ったクラス。レポジトリ参照。
var searchButton = new SearchButton();
searchButton.callback = (SearchButton button) {
var client = new QiitaClient();
client.get().then((result) {
_handleItemsString(result);
});
};
var listView = new ScrollableList(key: _listViewKey, itemExtent:70.0, children:_createWidgets(_items), );
// Viewのサイズを指定するには Container のインスタンスを作ってやらないといけないらしい
var container = new Container(height: 300.0, child: listView);
return new Material(
child: new Column(
children: <Widget>[
container,
searchButton
],
),
);
}
// API呼び出しが完了したら呼ばれるメソッド
void _handleItemsString(var jsonString) {
// これが呼ばれるとUIが再描画される
setState(() {
_items = QiitaItemsFactory.create(jsonString);
});
}
Iterable<Widget> _createWidgets(List<QiitaItem> items) {
var ret = new List<Widget>();
if(items == null) {
print('items is null');
return ret;
}
items.forEach((item) {
print(item.title);
ret.add(new Text(item.title));
});
return ret;
}
}
class QiitaApp extends StatefulWidget {
@override
_QiitaItemsState createState() {
return new _QiitaItemsState();
}
}
とりあえずこれで取得した記事一覧表示までは出来た。
上記のレポジトリをcloneして $ flutter run
すれば動くはず。
3. 作ってみて気付いたこと & 思ったこと
- どうも、
dart:mirrors
(reflectionのパッケージ) はflutterでは使えないようになっている様子。 - dartってjsとpythonを足して2で割った感じだなーと思った。 async/awaitはとても便利。
でもアクセス制御を_
のprefixでやる言語はやはりちょっと苦手。 - 作ったアプリは確かにAndroidでもiOSでも動いた。
ただ、全く同じコードなのに何故かiOSでは全角文字が文字化けしていて表示できなかった。 -
ScrollableList
(Androidで言うListView) のコンストラクタに、Viewの再利用とかを考えずに表示させたいViewを全て突っ込むワイルドなのがあるのがとても素敵だと思った。(上記のサンプルコード内でも実際に使っている。)
ただ、当然表示するセルが増えてくると辛いので、これとは別によくあるCallback形式で再利用するViewを作るインターフェースもある。 - アプリのサイズがやたら大きい。Androidでは、HelloWorldを表示するだけなのにapkのサイズが30MBとかだった。
恐らくdartのVMをアプリごとに作っているからだと思われる。 - dartはネット上の情報が少なくて辛かった。
流行っているプラットフォームに乗っかる開発の楽さを知った。 - 上記のdartのVMの恩恵か、コードを編集した際の再読込がとても速い。
$ flutter run
したコンソール内でr
を入力すると、アプリの再読込があっという間に終わる。 - この手のものにはよくある話で、お仕事で使うにはまだまだまだ辛そうだな、と思った。ただ、dartを勉強するきっかけとしては良いと思う。
終わりに
なんとなく使ってみたけど、気付いたらなくなってそうだな、と思った。
少なくとも、現時点で同じことをやるなら今いろいろな意味で話題のXamarinのほうがいいと思う。