FlutterでQiitaのクライアントを作ってみた話

この記事は [Androidその2 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/android_second) の11日目です。

# はじめに
[この記事](http://jp.techcrunch.com/2016/10/26/20161026googles-dart-programming-language-returns-to-the-spotlight/) を読んで、
(ほー、Dartって死んだと思ってたけどまだ開発続いてたのか。ほー、Flutterなんてものがあるのか)
とか思って実際に使ってみた。
ネタとしては、[この本](https://www.amazon.co.jp/exec/obidos/ASIN/4865940391/hatena-blog-22/)の内容をパクって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
1. 上記のGithubのレポジトリをclone  
`$ git clone https://github.com/flutter/flutter.git`  
2. cloneしたflutterのディレクトリにPathを通す  
`$ export PATH=/your/path/to/flutter/bin:$PATH`
3. doctorを実行 (ちょっと時間がかかる)  
`$ flutter doctor`

これだけ。とても簡単。
あとは、適当なAndroid端末を接続し、
レポジトリ内の適当なサンプルプロジェクトに移動して
`$ flutter run`
とすればサンプルアプリが動くはず。

### IDE
公式では開発環境としてatomとIntellijを推奨している。
私はIntellijで開発した。

1. Intellijをインストール(手順は割愛。私はCEを使っているが、特に問題なく動いている。)
2. DartとFlutterのIntellij pluginをインストール  
('dart', 'flutter' でそれぞれpluginを検索し、インストール & 再起動)
3. IntellijにFlutter SDKのパスを設定する  
　(`Preferences` -> `Languages & Frameworks` -> `Flutter` から、上記の Flutter SDKのパスを設定)  
誤ったパスを設定したりするとエラーが出るのですぐ気付く
4. IntellijにDartのパスを設定する (これは不要だったかも)  
　(`Preferences` -> `Languages & Frameworks` -> `Dart` から、  
`Dart SDK Path:`に上記の Flutter SDK内の `/bin/cache/dart-sdk` を設定)  

これでアプリが作成できるようになるはず。

## 2. アプリを作る
### 0. エントリーポイント
これはサンプルを見ればすぐわかるが、`main.dart` 内の `void main()` 内で
`runApp()` という関数を呼ぶ。
プロジェクトを作成したタイミングでテンプレートに入っているので、迷うことは無いはず。

### 1. APIクライアント
この辺はよくある

1. http GET
2. jsonをparse
3. 対応するインスタンスを作成

の流れをdartでやっただけ。
コードは以下の通り。
(http://qiita.com/api/v2/items にしか対応していない。)

http GET で文字列を取得 (この例では`Future<String>`のインスタンスが返る)

``` qiita_api_client.dart
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 & アイテムのインスタンス作成

```qiita_items_factory.dart
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を強く意識して作られている様子。
クラス図にするとこんな感じ。
![クラス図0.png](https://qiita-image-store.s3.amazonaws.com/0/73290/a2e5fb14-08f4-0393-57e4-b90cb6e38dbe.png)
このように、 

* 一度描画したら以降再描画しないUI(ボタンとか、決まったテキストを表示するラベルとか) の作成には `StatelessWidget` を使う
* 再描画が必要なUI(ScrollViewとか) の作成には `StatefulWidget` を使う

`StatefulWidget`を使う際には`State` というクラスがViewModel的な役割を担っていて、
UIの状態は全てこの中に持たせるような設計になっている。

コードは以下の通り。 (StatefulWidgetを作成 / 更新 している部分だけ。)
全部 main.dartに書いているのはダサいがプロトなのでいいや、ということで。

```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のほうがいいと思う。