LoginSignup
118
95

More than 5 years have passed since last update.

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

Posted at

この記事は 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

  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
このように、

  • 一度描画したら以降再描画しない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のほうがいいと思う。

118
95
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
118
95