【Flutter】GitHubリポジトリを検索するサンプルアプリ作った

potatotipsのLT用にサンプルアプリを作ったので紹介です。
この記事で使っている画像は[発表スライド](https://speakerdeck.com/rkowase/try-flutter)から抜粋してます。

## 環境
- Flutter: beta-1
- Dart: 1系

## アプリ
入力前|検索結果
:-:|:-:
![](https://qiita-image-store.s3.amazonaws.com/0/123122/680e9082-b0a7-998b-6272-663b3b520b3f.png)|![](https://qiita-image-store.s3.amazonaws.com/0/123122/9faf7e26-3324-fa30-011e-16d17f2ba8af.png)

## コード
コードは https://github.com/rkowase/flutter-sample に置きました。

## サンプルアプリを簡単に解説

### main.dart

#### UI部分
サンプルアプリのコンテンツ領域（body部分）の表示要素としてはTextField, Button, ListViewなのでListViewのchildrenにそれぞれのWidgetを配置していきます。
検索ボタン押下時の処理やListViewの各要素の生成処理は後述します。

<img width="635" alt="Screen Shot 2018-03-21 at 17.40.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/123122/a9259a02-0e30-c94c-371a-12e1daf63482.png">


```dart:main.dart
return new Scaffold(
  appBar: new AppBar(
    title: new Text(widget.title),
  ),
  body: new Center(
    child: new ListView(
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      children: <Widget>[
        new TextField(
          decoration: const InputDecoration(
            hintText: 'Flutter',
            labelText: 'Query',
          ),
          maxLines: 1,
          controller: _controller,
        ),
        new Container(
          padding: const EdgeInsets.all(20.0),
          child: new RaisedButton(
            child: const Text('Search'),
            onPressed: _search,
          ),
        ),
        new Container(
          height: 500.0,
          child: new ListView(
            key: _listViewKey,
            itemExtent: 50.0,
            children: _createWidgets(_items),
          ),
        ),
      ],
    ),
  ),
);
```

#### 処理部分

以下の`_search()`メソッドは検索ボタン押下時の処理で、GithubClientから取得してきた結果を`_item`に入れて`setState()`でUI再描画（再構築）してます。

```dart:main.dart
void _search() {
  var client = new GithubClient();
  client.get(_controller.text).then((result) {
    setState(() {
      _items = result;
    });
  });
}
```

以下の`_createWidgets()`メソッドはList Widgetの各要素であるListTileを生成しています。
アバター画像に関しては`NetworkImage`を使って非同期に簡単に取得できます。

<img width="705" alt="Screen Shot 2018-03-21 at 17.41.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/123122/e32cae57-791e-67ad-43c1-15c126cda0fa.png">


```dart:main.dart
Iterable<Widget> _createWidgets(List<GithubRepo> items) {
  var ret = new List<Widget>();
  if (items == null) {
    return ret;
  }
  items.forEach((item) {
    ret.add(
        new ListTile(
          leading: new CircleAvatar(
            backgroundImage: new NetworkImage(item.avatarUrl),
          ),
          title: new Text('${item.name} / ⭐ ${item.starCount}'),
        )
    );
  });
  return ret;
}
```

### API Client

async/awaitが使えるので**シュッ**と書けます。

```dart:api/github_api_client.dart
class GithubClient {
  Future<List<GithubRepo>> get(String query) async {
    var url = 'https://api.github.com/search/repositories?sort=stars&q=';
    var httpClient = new HttpClient();

    try {
      var request = await httpClient.getUrl(Uri.parse(url + query));
      var response = await request.close();

      if (response.statusCode != HttpStatus.OK) {
        return null;
      }

      var json = await response.transform(UTF8.decoder).join();
      return GithubRepo.fromJson(json);

    } catch (exception) {
      return null;
    }
  }
}
```

### Model

今回はリポジトリ名、Star数、アバターを表示したかったので以下のようになりました。
`fromJson()`メソッドでGitHub APIのレスポンスをパースしてます。

```dart:model/github_repo.dart
class GithubRepo {
  var name;
  var starCount;
  var avatarUrl;

  static fromJson(json) {
    var data = JSON.decode(json);
    List<Map<String, Object>> itemList = data['items'];

    return itemList.map((item) {
      var repo = new GithubRepo();
      Map<String, Object> owner = item['owner'];
      repo.avatarUrl = owner['avatar_url'];
      repo.name = item['full_name'];
      repo.starCount = item['stargazers_count'];
      return repo;
    }).toList();
  }
}
```

## 感想
- 環境構築は非常に簡単でAndroidStudioをすでに使っていれば数分でHello Worldを動かすことができました。
    - VSCodeでもそんな変わらないと思います。
- Flutterはドキュメントが非常に充実してるので https://flutter.io/ を見るのが分かりやすいです。
- 今回作ったサンプルアプリは色々調べながらですが1日くらいで出来ました。
    - Hot Reload最高
- Androidだと一覧を表示するためにAdapterを用意しないといけなかったりなにかとやることが多いですが、Flutterだと簡単に書けてかなり楽でした。
- 公式Widgetが充実しているので知れば知るほどできることが増えて早く開発できそうです。
- Nativeとの連携部分はまだ試せていないのでハードウェア（カメラなど）を使うアプリとかも作ってみたいです。

## 補足
- 通信中のインジケータ表示やエラー時（通信失敗など）の処理は入れていません。
- 固定文字列（URLなど）はconstとかにしたほうがいいですがリファクタリングしてません。
- Viewはもっとうまく作る方法がある気がします。（こういう書き方のほうがいいとかありましたら教えてください！）
    - （追記）画面内の要素を上から順に配置するために `ListView` を使っていますが、今回の用途だと `Column` を使ったほうがよかったです。
    - （追記）あとリポジトリ一覧の `ListView` の親要素は `Container` ではなく `Expanded` にしたほうがよかったです。

## おまけ

### エラー画面のカラーコード

- Background: 7C160E
- Text: FFFE7F
<img width="703" alt="Screen Shot 2018-03-21 at 17.42.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/123122/34a43bc4-0806-1611-de59-74c00b4ec2b5.png">

### Hello Worldを簡単に解説

Hello Worldという文字列を表示するだけでView再描画の必要がないので`StatelessWidget`を継承しています。
画面全体の入れ物にあたる`Scaffold`の中に画面上部の`appBar`と`body`があり、それぞれにWidgetを入れていきます。
<img width="577" alt="Screen Shot 2018-03-21 at 17.39.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/123122/b425d486-6414-620c-2f4b-30b4bdafd2b9.png">


## リンク
potatotipsで「try! Flutter」という発表をしました - Rui Kowase's blog
https://rkowase.hatenablog.com/entry/2018/03/18/154816

try! Flutter // Speaker Deck
https://speakerdeck.com/rkowase/try-flutter