flutter + golang + openAPIで雑アプリ開発

巷で流行りの`flutter`を触ってみたついでに、以前から気になっていた`open-api-generator`を使って、HTTP通信行う雑なiOSアプリを作ってみた。
`flutter`は[この書籍](https://www.amazon.co.jp/dp/4798055832/ref=asc_df_47980558322612983/?tag=jpgo-22&creative=9303&creativeASIN=4798055832&linkCode=df0&hvadid=295719984664&hvpos=1o1&hvnetw=g&hvrand=10888086438959720715&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1028851&hvtargid=pla-526160326963&th=1&psc=1)を読みつつキャッチアップ、`open-api-generator`は、[この記事](https://ky-yk-d.hatenablog.com/entry/2019/01/13/234108)を参考にした。

## 今回作ったアプリの構成
ユーザを登録する機能を実装。照会の実装はUX考えるとちょっとしんどくなったので、画面に`json`を表示するだけというクソ仕様で実装。

- クライアント: `flutter`
  - ユーザ情報(名前,年齢)を入力して登録
  - 登録した情報をサーバから取得して`json`を表示
- サーバ: `golang`
  - オンメモリに登録されたユーザ情報を保存しておく


## アプリ実装

まずは`REST`の仕様を定めた`yml`ファイルを作成する。
ここから`open-api-generator`を使用して、クライアント・サーバの`REST`の実装を自動生成する。
`open-api-generator` で自動生成できる言語は[ここ](https://github.com/OpenAPITools/openapi-generator)を確認。結構豊富にある。

以下が今回使用するRESTの仕様定義`yml`
設定値の詳細については[この記事](https://qiita.com/teinen_qiita/items/e440ca7b1b52ec918f1b)を参考にした

```user-management-api.yml 
openapi: "3.0.0"
info:
  version: 1.0.0
  title: go_flutter_exam User
  license:
    name: MIT
servers:
  - url: http://localhost:8080/api/
paths:
  /users:
    get:
      summary: get all user information
      tags:
        - users
      parameters: []
      responses:
        '200':
          description: return user information
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
  /user/add:
    post:
      summary: Create a new User
      tags:
        - addUser
      parameters: []
      requestBody: # リクエストボディ
        description: user to create
        content:
          application/json:
            schema: # POSTするオブジェクト
              $ref: '#/components/schemas/User'
            example:
              name: Michel Roe 
              age: 23
      responses:
        '201':
          description: CREATED

components:
  schemas:
    User:
      required:
        - name
        - age
      properties:
        id:
          type: string
        name:
          type: string
        age:
          type: integer
          format: int64

```

次に`open-api-generator`をインストール

```bash
brew install openapi-generator
```

作成した`yml`の妥当性を検証

```bash
$ openapi-generator validate -i user-management-api.yml 
Validating spec (user-management-api.yml)
No validation issues detected.
```

サーバ側のコードを作成

```bash
$ openapi-generator generate -i user-management-api.yml -g go-server -o ./server
```

サーバ側のコードを自動生成すると、

- HTTPメソッドごとの関数の`.go`ファイル
- APIで使うDTOの`.go`ファイル
- routing処理,webサーバ起動処理の`.go`ファイル
- `main.go`

が生成される。

で、HTTPメソッドごとの関数の`.go`ファイルにリクエストを受け取ってどうハンドリングするかのロジックを書いていく。

- ユーザ登録POSTリクエストハンドリング処理実装

```api_add_user.go
package openapi

 import (
	 "net/http"
	 "github.com/mholt/binding"
	 "strconv"
 )
 
 // UserAddPost - Create a new User
 func UserAddPost(w http.ResponseWriter, r *http.Request) {
	 w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	 w.WriteHeader(http.StatusOK)
	 user := new (User)
	 userMap := GetUserMap()
	 binding.Bind(r, user)
	 user.Id = strconv.Itoa(len(userMap))
	 userMap[int64(len(userMap))] = user 
 }
 
 func (user *User) FieldMap(req *http.Request) binding.FieldMap {
	 return binding.FieldMap{
		 &user.Name: "name",
		 &user.Age:   "age",
	 }
 }
```
- 全ユーザ情報取得GETリクエストハンドリング処理実装

```api_users.go
 package openapi

 import (
	 "encoding/json"
	 "net/http"
 )
 
 // UsersGet - get all user information
 func UsersGet(w http.ResponseWriter, r *http.Request) {
	 w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	 w.WriteHeader(http.StatusOK)
	 userMap := GetUserMap()
	 values := []*User{}
	 for _, v := range userMap {
		 values = append(values, v)
	 }
	 
	 json.NewEncoder(w).Encode(values)
 }
 ```

- ユーザ情報DTO。今回はここにインメモリMapとMapの取得処理も実装

```model_user.go
package openapi

var inMemoryUserMap = map[int64]*User{}

type User struct {

	Id string `json:"id,omitempty"`

	Name string `json:"name"`

	Age int64 `json:"age"`
}

func GetUserMap() map[int64]*User {
    if inMemoryUserMap == nil {
        inMemoryUserMap = map[int64]*User{}
    }
    return inMemoryUserMap
}
```

サーバ側はこれで完成。0から自分でルーティング書くよりずいぶん早く実装完了した。
好感触。


続いてclient側も自動生成。

```bash
$ openapi-generator generate -i user-management-api.yml -g dart -DbrowserClient=false -o ./client
```

クライアント側もサーバ側とリクエストを投げる/受け取るという部分に違いはあるものの、同じようなファイルが生成される。

あとはクライアント側のUI部分を実装するだけ。
とりあえずflutterのスターターアプリを作成。

```bash
flutter create flutter-golang-exam_app
```
自動生成されたflutterのパッケージをimportできるように上のコマンドで作成したプロジェクトの`pubspec.yml`の依存に自動生成されたパッケージを追加。

```pubspec.yml
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  openapi: ## ←これが自動生成されたパッケージ
    path: ../../client/
dev_dependencies:
  flutter_test:
    sdk: flutter
```


で、`main.dart`を以下のように実装。

```main.dart
import 'package:flutter/material.dart';
import 'package:openapi/api.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.red,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _userName ='';
  int _userAge = 0;

  List<User> _displayUsers = new List();

  void _addUser() {
    var api = new AddUserApi();
    User user = new User(this._userName,this._userAge);
    api.userAddPost(user: user);
  }

  void _getUser() {
    var api = new UsersApi();
    Future<List<User>> users = api.usersGet();
    users.then((content) =>  setState(() {
        this._displayUsers = content;
    }));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new TextField(
              decoration: new InputDecoration(labelText: "Enter register user name "),
              onChanged: (v) => this._userName = v,
            ),
            new TextField(
              decoration: new InputDecoration(labelText: "Enter register user age"),
              keyboardType: TextInputType.number,
              onChanged: (v) => this._userAge = int.parse(v),
            ),
            Text(
              '$_displayUsers',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        verticalDirection: VerticalDirection.up, // childrenの先頭を下に配置
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            onPressed: _addUser,
            tooltip: 'User Register',
            child: Icon(Icons.add),
          ),
          Container( // 余白のためContainerでラップ
            margin: EdgeInsets.only(bottom: 16.0), 
            child: FloatingActionButton(
              onPressed: _getUser,
              tooltip: 'All User Get',
              child: Icon(Icons.autorenew),
            ),
          ),
        ],
      ),
    );
  }
}
```

これでクライアントも実装完了。

サーバ/クライアントを起動して、下のような画面がシミュレータに表示されれば成功。
<img width="383" alt="スクリーンショット 2019-04-01 0.57.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/301386/5fb0598b-4843-8825-593c-8374a46829d6.png">

適当に名前、年齢を入力してaddボタンを押して、リロードボタンを押すと
<img width="390" alt="スクリーンショット 2019-04-01 0.59.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/301386/22aa89db-a198-de5e-6cfd-b180a6fd2f82.png">

クソアプリの完成。

## `flutter+golang+open-api-generator`の所感
- 実装はとても楽チンになった！`routing`部分とか自前でやるとそれなりに詰まったかもなあと思いながら自動生成されたコードを見ながら感じたので。(初心者だからかもしれないけど。。)
- サーバ側は自動生成されたものを手で修正せざるを得ないのでは？と思うところも多々あったので、自動生成されたものを一切触らずにサーバ側実装を行う方法を考えねばなあと感じた。この点が一番引っかかった。
- 個人的には`java`やってるので、`flutter`の方が`swift`よりも読みやすいなと思った。UIは[flutterStudio](https://flutterstudio.app/)使えば、もっと楽にできるのかなと思って触ってみたけど、コード見ながらじゃないとどのパーツを置いていけば良いかわからなくなったので、コード書くほうが良いやって思った。

## 感想
`openAPI`は自動テストコードをメンテコスト低く作成できるかもしれないなと思って、前から目を付けていたけどやはり良い。
でもやっぱ自動生成されたものを手動更新せずに実装する方法は考えないとちょっと使えない。
クライアント側の自動生成コードはモデルのコンストラクタくらいしか追加してないので、サーバ側の自動テストを作る目的ならすぐに導入できそう。
`flutter`も学習コスト高い感じはしなかったので、今後も継続勉強していこうと思った。今度はマトモなアプリを掲載できるようにw