FlutterでQiitaのアプリを作ってみる その1

最近の案件ではwebばかりで久々にモバイルアプリ開発をやりたくなった
とはいえ作りたいものがあるわけでもないので、勉強がてら[Qiita API](https://qiita.com/api/v2/docs)を使って適当なアプリを作っていく過程を記録する

少し前にswift/kotlinでそれぞれのOSの開発をやっていたがFlutterは今回初めて使います。
間違えとかあれば教えてください

repository: 

https://github.com/t0yger/qiita-app


## 環境準備
[Flutter](https://docs.flutter.dev/get-started/install/macos)のSDKに関してはdocument通り。 `flutter doctor` でエラーが出ないところまでやる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/0cfc4189-109f-6abb-9e3d-e099026a6ddf.png)
久々にXcode落としたけど重いですね...
ひとまずflutterの準備は終わったのでアプリ作成
```sh
flutter create qiita_app
```
コマンドを打つだけ。難なく作成出来たので起動してみる
```sh
cd qiita_app
flutter run
```

macosかchromeで起動するか聞かれたのでとりあえずmacosを選び起動
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/928ea962-b350-f9e4-a808-54fcc144a394.png"  width="320" height="320">
macosのapplicationとして起動したっぽい
モバイルがやりたいのでiOSのsimulatorで起動したい

とりあえずsimulatorを起動
```sh
open -a Simulator
```
simulatorが起動したらもう一度アプリを起動してみる
今度は何で起動するか聞かれず自動的にiOSのsimulatorで起動した
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/40e4f1e2-f00b-41b0-eadc-ef422813775a.png" width="160" height="320">

Androidでも起動してみる
起動できるAVDのリストを出してみる
```sh
emulator -list-avds
```
`emulator command not found`となってしまった。PATHの設定がされてなかったので`~/Library/Android/sdk/emulator`をPATHに追加
もう一度実行
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/428da4c9-11a8-2d87-811f-58b359461336.png)
昔の案件で使っていたAVDが残ってたっぽいので`Pixel_2_API_28`で起動してみる
```sh
emulator -avd Pixel_2_API_28
```
無事emulatorが起動したのでもう一度flutterアプリを起動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/aba25994-422a-d7ee-d4fb-d367dee0a9f6.png)
iOSのsimulatorを起動しっぱなしだったから選択肢が出てきたのでAndoridを選択して起動
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/3ba509d8-faf8-c7b7-9b0e-4100771fb9dd.png" width="160" height="320" />

無事両OS起動した:tada:

editorに関してVSCodeが推されてるっぽいので無難に使ってみる
こちらもFlutterのdocumentにあるセットアップをそのままやる

## Qiita API叩いてみる
VSCodeを使うということでpluginの[REST Client](https://github.com/Huachao/vscode-restclient)を導入してみる
VSCodeのpluginで `REST` って打つと多分一番上に出る
このpluginをinstallし`qiita.http`ファイルを作成し以下を追加
```http
GET https://qiita.com/api/v2/items?page=1&per_page=20
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3077916/9b166fac-b4c1-9b0b-39e2-90f532aa53ef.png)
２００応答が返ってきました。tokenをつけないとAPIのリクエスト制限が60req/hourみたいなので乱発の注意

## 実装
さあさあ準備が終わったので実装をしていく
とりあえずFlutterからこのAPIを実行してみたい。
とりあえず何も考えず`lib/main.dart`にゴリゴリ書いていくことにする
Flutterのpackageに[http](https://pub.dev/packages/http)なるものがあるので使わせていただく
```sh
flutter pub add http
```
`pubspec.yaml`に差分が出た。恐らく`package.json`みたいなものなのだろう
これで`http`が使えるはず
READMEに書いてあるものをそのまま持ってきて`getArticles`関数を作ってみる
```dart
void getArticles() async {
  var url = Uri.https('example.com', 'whatsit/create');
  var response =
      await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
  print('Response status: ${response.statusCode}');
  print('Response body: ${response.body}');

  print(await http.read(Uri.https('example.com', 'foobar.txt')));
}
```
これを元にqiitaのAPIを叩くよう変更してみる
query parameterはどう書けばいいのか
`Uri.https`に飛んでみると丁寧にコメントが書いてあった
```dart
 /// Example:
 /// ```dart
 /// var uri = Uri.https('example.org', '/path', {'q': 'dart'});
 /// print(uri); // https://example.org/path?q=dart
```
なるほど、第３引数にオブジェクトで渡せば良さそう
```dart
void getArticles() async {
  var url = Uri.https('qiita.com', 'api/v2/items', {'page': 1, 'per_page': 20});
  var response = await http.get(url);
  print('Response status: ${response.statusCode}');
  print('Response body: ${response.body}');
}
```
こんな感じかな？この関数をボタンを押した際に呼び出してみる
ボタンを押した際の処理はどこにあるのかを探すと
```dart
floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
), 
```
この`onPressed`で実行してる`_incrementCounter`というのがそれっぽい
これを先ほど作った`getArticles`に置き換えてみる
置き換えるとwarningが出ちゃったのでとりあえず何も考えず従う
最終的にこう
```dart
floatingActionButton: const FloatingActionButton(
        onPressed: getArticles,
        tooltip: 'Increment',
        child: Icon(Icons.add),
) 
```
起動して叩いてみるが、叩けてなさそう
```
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: type 'int' is not a subtype of type 'Iterable<dynamic>'
#0      _Uri._makeQuery.<anonymous closure> (dart:core/uri.dart:2374:18)
#1      _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:625:13)
#2      _Uri._makeQuery (dart:core/uri.dart:2370:21)
#3      new _Uri (dart:core/uri.dart:1658:13)
#4      _Uri._makeHttpUri (dart:core/uri.dart:1797:12)
#5      new _Uri.https (dart:core/uri.dart:1693:12)
#6      getArticles (package:qiita_app/main.dart:9:17)
#7      _InkResponseState.handleTap (package:flutter/src/material/ink_well.dart:1096:21)
#8      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:253:24)
#9      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:627:11)
#10     BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:306:5)
#11     BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:276:7)
#12     GestureArenaManager<…>
```
なんだろう。query parameterでintを使ったのがダメだったのか？
stringにしてもう一度叩いてみる
```
flutter: Response status: 200
flutter: Response body: [{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:14\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h1\u003e\n\u003cp data-sourcepos=\"2:1-3:108\"\u003eRailsなどを中心に勉強中のエンジニア初心者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。\u003cbr\u003e\n間違っていることもあると思われるので、その際は指摘いただけると幸いです。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"5:1-5:7\"\u003e\n\u003cspan id=\"er図\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#er%E5%9B%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eER図\u003c/h1\u003e\n\u003cp data-sourcepos=\"7:1-7:85\"\u003eER図とは「DB上のテーブル同士の関連<…>
```
叩けた:tada:
長くなってしまったのでとりあえずここまで
次回はこのレスポンスを元に表示させてみたい

https://qiita.com/t0yger/items/9fe42fe04e5636ae9b3d