この記事は 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のほうがいいと思う。