Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

巷で流行りのflutterを触ってみたついでに、以前から気になっていたopen-api-generatorを使って、HTTP通信行う雑なiOSアプリを作ってみた。
flutterこの書籍を読みつつキャッチアップ、open-api-generatorは、この記事を参考にした。

今回作ったアプリの構成

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

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

アプリ実装

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

以下が今回使用するRESTの仕様定義yml
設定値の詳細についてはこの記事を参考にした

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をインストール

brew install openapi-generator

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

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

サーバ側のコードを作成

$ 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側も自動生成。

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

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

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

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),
            ),
          ),
        ],
      ),
    );
  }
}

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

サーバ/クライアントを起動して、下のような画面がシミュレータに表示されれば成功。
スクリーンショット 2019-04-01 0.57.26.png

適当に名前、年齢を入力してaddボタンを押して、リロードボタンを押すと
スクリーンショット 2019-04-01 0.59.33.png

クソアプリの完成。

flutter+golang+open-api-generatorの所感

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

感想

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

soichiro0311
新人エンジニア。 aws,golang,code-design,java,agile,typescript,rust。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした