Flutter WebからFlaskで作ったAPIを呼び出す


[Flutter Web](https://flutter.dev/web)がGoogle I/O 2019でテクニカルプレビューとして発表され、これがあればAndroid、iOSだけでなくWebのフロントエンドもワンソースで作れると思いました。Webのフロントエンドを作るからには[webpack](https://webpack.js.org/)でビルドするものだと思いましたが、独自のビルドシステムでした。今回、独自のビルドシステムであることを鑑みて、Webのフロントエンドとして使う場合、サーバー側のAPI呼び出しをどうすれば良いか検証してみました。この検証ではサーバー側はPythonの[Flask](http://flask.pocoo.org/)で作成し、最終的には[Google App Engine Standard EnvironmentのPython3環境](https://cloud.google.com/appengine/docs/standard/python3/)にデプロイします。

# Flutter WebのHello Worldを動かす

[公式のGitHubレポジトリ](https://github.com/flutter/flutter_web)を使います。

こちらの案内の通りではHello Worldを動作させることができませんが、すでに先人たちが動作させる方法を発見し解説していますので、その通りに環境を構築してください。

[flutter webでexampleを動かすまで(flutterインストール済みを想定)](https://qiita.com/hiko1129/items/efc20792016a22a7c88a)
[Flutter Webを動かしたときにつまづいたこと](https://qiita.com/Nkzn/items/402a29fe9bb517ce78aa)

```sh
# examples/hello_world/ ディレクトリで
webdev serve
```

http://localhost:8080/ をブラウザで開くと、Hello Worldが表示されます。

# ２つのビルド方法

Flutter Webの開発環境には[2つのビルド方法](https://github.com/flutter/flutter_web#getting-stateless-hot-reload-with-webdev)があります。

## ホットリロード

`webdev serve --auto restart` コマンドでローカルで動くサーバを動かします。Dartファイルを保存すると0.3秒ぐらいでブラウザに反映されます。スマホアプリを作るのと同じ感覚で作れると思います。

## プロダクションビルド

`webdev build` コマンドでindex.htmlとmain.dart.jsがbuildディレクトリに作成されます。この2ファイルをWebサーバに設置すると、画面を表示できます。ビルド時間はテキストを1つ書き換えただけでも10秒以上かかります。

# スマホアプリを作る

まずFlutter Webで動かしたいアプリをスマホ向けに作ります。今回は名刺ビューワアプリを題材にします。
私が新人研修やHTTPクライアントライブラリの検証のために[JSONPlaceholder](https://jsonplaceholder.typicode.com/)を参考して作った架空の名刺情報JSONを返却するサービス[JJSONPlaceholder](https://github.com/tfandkusu/jjsonplaceholder)がありますので、それを使います。

名刺一枚のJSONを取得
GET https://jjsonplaceholder.appspot.com/cards/1

このようなものができました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/a826cb8e-1b67-cffd-12bb-f24b80605316.png" width="400">

ソースコードはこちらです。

```main.dart
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Web with Flask',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  //氏名
  String _name;

  //社名
  String _company;

  //所属
  String _division;

  //役職
  String _title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('名刺詳細')),
        body: Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: makeChildren(),
            )));
  }

  // paddingの中の要素たち
  List<Widget> makeChildren() {
    if (_name == null) {
      // 名刺が無いのでプログレスを表示
      return <Widget>[CircularProgressIndicator()];
    } else {
      // 名刺を表示
      return <Widget>[
        Text(_company, style: Theme.of(context).textTheme.title),
        Padding(
          padding: EdgeInsets.only(top: 8),
          child: Text("$_division $_title", style: Theme.of(context).textTheme.body1),
        ),
        Text(_name, style: Theme.of(context).textTheme.title)
      ];
    }
  }

  @override
  void initState() {
    super.initState();
    loadData();
  }

  // APIから取ってくる
  loadData() async {
    String url = 'https://jjsonplaceholder.appspot.com/cards/1';
    http.Response response = await http.get(url);
    var card = json.decode(utf8.decode(response.bodyBytes));
    // 画面を更新
    setState(() {
      _name = card['name'];
      _company = card['company'];
      _division = card['division'];
      _title = card['title'];
    });
  }
}
```
# Flutter Webリポジトリをフォーク

今回は[flutter_webのリポジトリをフォーク](https://github.com/tfandkusu/flutter_web/)して、フロントエンドはexample/hello_wordを書き換えて、サーバ側はexample/flaskディレクトリを作成して検証を進めました。

フロントエンド(Flutter)
[example/hello_word](https://github.com/tfandkusu/flutter_web/tree/master/examples/hello_world)

サーバー側(Python)
[example/flask] (https://github.com/tfandkusu/flutter_web/tree/master/examples/flask)

# Flutter Webに移植

[examples/hello_world/lib/main.dart](https://github.com/tfandkusu/flutter_web/blob/master/examples/hello_world/lib/main.dart)を先ほど作ったスマホアプリのそれに書き換えます。そして始めにimportしているパッケージを変更します。

変更前

```dart
import 'package:flutter/material.dart';
```

変更後

```dart
import 'package:flutter_web/material.dart';
```

このまま `webdev serve　--auto restart` コマンドで動かし、http://localhost:8080/ にブラウザでアクセスすると、HTTPリクエストがクロスオリジンになってしまうため、名刺情報の取得に失敗します。

![スクリーンショット 2019-06-03 1.04.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/cc1569f0-a0e6-fe62-6941-f47063a3d1a7.png)

次の節ではAPIサーバー側にCORS (Cross-Origin Resource Sharing) を設定して開発できる環境を整えます。

# 開発用にCORS (Cross-Origin Resource Sharing) を設定する

[Flaskで作ったAPIサーバー](https://github.com/tfandkusu/flutter_web/blob/master/examples/flask/main.py)はポート5000で起動するようにし、Access-Control-Allow-Originヘッダーを設定します。

```python
if __name__ == '__main__':
    # ローカルテスト環境
    @app.after_request
    def add_cross_origin_header(response):
        # 8080はFlutter Webのポート番号
        response.headers['Access-Control-Allow-Origin'] = 'http://localhost:8080'
        return response
    # Flaskはポート番号5000で起動
    app.run(host='127.0.0.1', port=5000, debug=True)
```

[Flutter側](https://github.com/tfandkusu/flutter_web/blob/master/examples/hello_world/lib/main.dart)のWebAPI呼び出しも http://localhost:5000/ に向けます。

```dart
String url = 'http://localhost:5000/cards/1';
http.Response response = await http.get(url);
```

これで開発時におけるホットリロードがフロントエンド側サーバ側双方で実現できるようになりました。

# 本番向けにデプロイする

Flutter側API呼び出しを同じホストに変更します。

```dart
String url = '/cards/1';
http.Response response = await http.get(url);
```

プロダクションビルドします。

```sh
# example/hello_word ディレクトリで
webdev build
```

buildディレクトリにindex.htmlとmain.dart.jsが生成されます。
[Flaskのプロジェクト](https://github.com/tfandkusu/flutter_web/tree/master/examples/flask)に移動し、それらをFlaskのルールに基づき設置します。

## index.html

[index.html](https://github.com/tfandkusu/flutter_web/blob/master/examples/flask/templates/index.html)はテンプレート扱いにして、ルートパスから配信されるようにします。

```main.py
@app.route('/')
def root():
    # index.htmlはtemplatesディレクトリに置く
    return render_template('index.html')
```
flaskでは静的ファイルのパスは/staticになるのでindex.htmlのscriptタグを編集します。

```index.html
<script defer src="/static/main.dart.js" type="application/javascript"></script>
```

## main.dart.js

静的ファイルとして配信されるようにします。

[static/main.dart.js](https://github.com/tfandkusu/flutter_web/tree/master/examples/flask/static) に設置します。


## デプロイ

http://localhost:5000 にアクセスして動作確認をして、問題なければデプロイします。

```sh
gcloud app deploy
```

私がデプロイしたURLは[こちら](https://flutter-dot-jjsonplaceholder.appspot.com/)です。