OpenAPI Generator + golang + Flutter でアプリ開発

1. はじめに

以下のような構成のスマホアプリを作ってみようと思います。

f:id:daigo-knowlbo:20181103213543p:plain

一般的な、HTTP経由でサーバと通信するタイプのスマホアプリです。

で、タイトルの通り「OpenAPI 3.0でWebAPIを定義」し「golangでWebAPIを実装」し「Flutterでスマホアプリを実装」する、ということをしたいと思います。
また、OpenAPI 3.0定義からサーバ及びクライアントコードを自動生成するために OpenAPI Generator を使用することとします。

github.com

今回利用した環境  

macOS Mojave 10.14
Flutter 0.8.2
go 1.10.3
openapi-generator 3.3.2

2. 準備

Flutter / go 環境は構築済みの前提とします。
openapi-generatorは以下のbrewコマンドでインストールします。

$ brew install openapi-generator

3. 作る!

大まかな流れは以下になります。

  • OpenAPI 3.0 specでAPI仕様を定義
  • OpenAPI Generatorでgolangのserverコードを自動生成
  • OpenAPI Generatorでdartのclientコードを自動生成
  • Flutterアプリを作成(dart clientを利用してgolang serverを呼び出す)

では順番に進めていこうと思います。
まずは、アプリ開発用のディレクトリを作成します。

$ mkdir oas3_go_flutter_exam
$ cd oas3_go_flutter_exam

3.1. OpenAPI 3.0 specを作成

OpenAPI定義yamlを作成します。

ファイル名は「employee_api.yml」とします。
idを指定してemployee情報を取得するWebAPIの定義になります。
Employeeは id / firstName / lastName / salary の属性を持ったオブジェクトとします。

openapi: "3.0.0"
info:
  version: 1.0.0
  title: oas3_go_flutter_exam Employee
  license:
    name: MIT
servers:
  - url: http://localhost:8080/api/
paths:
  /employee:
    get:
      summary: get employee information
      operationId: getEmployee
      tags:
        - employee
      parameters:
        - name: employeeId
          in: query
          description: query target employee id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: return employee information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Employee"
components:
  schemas:
    Employee:
      required:
        - id
        - firstName
        - lastName
        - salary
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        salary:
          type: integer
          format: int64

3.2. yamlの妥当性をチェック

定義した employee_api.yml にエラーがないか、以下のコマンドでチェックします。

$ openapi-generator validate -i employee_api.yml

Validating spec (employee_api.yml)
No validation issues detected.

3.3. golangのserverコードを自動生成

以下のコマンドでgolangのserverコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g go-server -o ./server

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/model_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/api_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/api/openapi.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/main.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/Dockerfile
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/routers.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/logger.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator/VERSION

モデルクラス(model_employee.go)やルート定義(routers.go)、APIのカラ定義(api_employee.go)など一通りのgo実装が自動生成されました。

3.4. goのWebAPIを実装

自動生成されたコードにオリジナルの実装を追加します。

api_employee.goファイル の GetEmployee() に Employeeオブジェクト を返却する実装を以下のように追加します。
(本来はDBアクセスなどを行った結果を返すと思います)

package openapi

import (
    "encoding/json"
    "net/http"
)

// GetEmployee - get employee information
func GetEmployee(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    w.WriteHeader(http.StatusOK)

    employeeID := r.URL.Query()["employeeId"]

    employee := Employee{
        Id:        employeeID[0],
        FirstName: "ryuichi " + employeeID[0],
        LastName:  "daigo " + employeeID[0],
        Salary:    12000000}
    json.NewEncoder(w).Encode(employee)
}

3.5. go serverを実行

WebAPIがうまく動作するか実行してみましょう。

$ go run main.go

curlでもブラウザでも何でも良いですが、以下のURLにGETを投げてみます。

http://localhost:8080/api/employee?employeeId=10

{"id":"10","firstName":"ryuichi 10","lastName":"daigo 10","salary":12000000}

golang serverのWebAPIが正しく動作していることを確認できました。

3.6. dartのclientコードを自動生成

以下のコマンドでdartのclientコードを自動生成します。

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

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/model/employee.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/Employee.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/api/employee_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/EmployeeApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator/VERSION

※ 上記のように「-DbrowserClient=false」オプションを付けないとFlutterでコンパイルできない dart:html に依存したコードが生成されてしまうので注意。

dartによりWebAPI呼び出しを行うコード、また送受信するデータのモデルクラスなど一通りの実装が自動生成されました。

3.7. Flutterアプリを作成

以下のコマンドでFlutterアプリを作成します。

 $ flutter create flutter_app

先程自動生成した dart client を利用して golang server にアクセスするために pubspec.yamldart client への依存定義を追加します。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

  openapi:
    path: ../client/

※ 上記定義の下2行

main.dartを修正します。
IDを入力し、ボタンを押すとWebAPI呼び出しが行われ、結果を画面にテキスト表示するようにします。

// main.dart

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

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter with OpenAPI Generator'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {

  String _employeeName = '';
  String _employeeId = '';

  void _callWebApi() {    
    var client = new EmployeeApi();
    var result = client.getEmployee(this._employeeId);
    result.then(
      (employee) => setState(() { this._employeeName = employee.firstName + ' ' + employee.lastName; } )
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text('pleas input employee id'),
            new TextField(
              onChanged: (v) => this._employeeId = v,
            ),
            new RaisedButton(
              child: new Text('call WebAPI'),
              onPressed: _callWebApi,
            ),
            new Text(
              this._employeeName,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
    );
  }
}

3.8. 実行

では実行してみます。

./serverディレクトリで以下のコマンドによりgolang serverを起動しておきます。

go run main.go

そしてFlutterアプリを起動します。

f:id:daigo-knowlbo:20181103211906p:plain

IDを入力し、call WebAPIボタンをタップします。

f:id:daigo-knowlbo:20181103212648p:plain

WebAPI呼び出しが行われEmployeeオブジェクトの取得&表示が行われました!

4. まとめ

OpenAPI Generator を使うとモデルを含めたテンプレート実装が一瞬のうちに生成されかなりいい感じに開発が進められるんじゃないかと思いました。サクサクっと必要な定形コードが自動生成されるので、ドメイン領域への集中力アップにも勿論効果的ですね。
OpenAPI 3.0によるAPI定義を明確に行った上で、サービスを実装していくスタイルも(まあSwagger時代から何年も行われていることではありますが)クリーンにプロジェクトが保たれていいと思います。

今回作ったコードは以下に置いておきました。

github.com

Flutter - json_serializableを理解するメモ

1. はじめに

Flutterでアプリ作ろうと思ったら多くの場合、サーバ側のWebAPIと通信することが想定されます。

HTTP越しの通信でデータの受け渡しをする、つまりjsonデータのやり取りが必要→アプリではjson文字列ではなくモデルクラスのようなオブジェクトでデータを扱いたい→サーバに送り返すときにはモデルオブジェクトを再びjsonに変換して送りたい。。。

つまり json <-> object の serialize / deserialize ですね。
公式ページだと「json_serializable」と「built_value」が紹介されていて、特別に新しい技術でもないので、既に色々な方がブログっております。

が、まあ自分用という意味でもメモブログしておきます。

今回使った環境は、、、
OS: Windows 10 x64
Flutterバージョンは以下の通り。

$ flutter --version

Flutter 0.9.4 • channel beta • https://github.com/flutter/flutter.git
Framework • revision f37c235c32 (5 weeks ago) • 2018-09-25 17:45:40 -0400
Engine • revision 74625aed32
Tools • Dart 2.1.0-dev.5.0.flutter-a2eb050044

2. json_serializableとは

Flutter(というかDart)で json文字列<->オブジェクト の serialize / deserialize を楽に実装するためのライブラリです。
では早速、使ってみようかと。

3. 使ってみよう

以下のようなロジックを実装してみます。

上記ロジックをFlutterアプリ内で実装し、print()関数でコンソールに出力します。
(なのでFlutterアプリですが、UI無視のサンプルとします)

3.1. flutterプロジェクトを作成

以下のコマンドで「json_example」という名前のFlutterプロジェクトを作成します。

$ flutter create json_example

3.2. 依存パッケージ追加

json_serializableを使うために「json_serializable」「json_annotation」「build_runner」の3つのパッケージを「pubspec.yaml」定義に追加します。

...省略...

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable: ^2.0.0
  build_runner: ^1.0.0

...省略...

各パッケージの概要は以下の通り:
json_serializable : json serialize / deserializeを行うパッケージ
json_annotation: json_serializableで使用されるアノテーションを定義するパッケージ
build_runner: 「json serialize / deserializeを行うためのコード」の自動生成を行うためのビルドランナー

以下のコマンドでパッケージを取得します。(VS Code+pluginやandroid Studio環境を使っていればIDEが自動で実行)

$ flutter packages get

3.3. モデルクラス作成

Employeeクラスを作成します。

// ./lib/employee.dart ファイル

import 'package:json_annotation/json_annotation.dart';

part 'employee.g.dart';

@JsonSerializable()
class Employee {
  Employee(this.id, this.firstName, this.lastName);

  int id;
  String firstName;
  String lastName;
  
  factory Employee.fromJson(Map<String, dynamic> json) => _$EmployeeFromJson(json);
  Map<String, dynamic> toJson() => _$EmployeeToJson(this);
}

モデルクラスは以下のような定型フォーマットで記述します。上記Employeeもこのフォーマットに従っています。
※xyzおよび【】部分が作成するモデルクラス名。
※別途メソッド定義を追加するのは自由。
※サンプルでは扱っていないが、アノテーションなども適時付けられる。

// ./lib/【xyz】.dart ファイル

import 'package:json_annotation/json_annotation.dart';

part '【xyz】.g.dart';

@JsonSerializable()
class 【Xyz】 {
  【Xyz】(【プロパティ定義を並べる】);

  【プロパティ定義を並べる】
  ...
  
  factory 【Xyz】.fromJson(Map<String, dynamic> json) => _$【Xyz】FromJson(json);
  Map<String, dynamic> toJson() => _$【Xyz】ToJson(this);
}

3.4. serialize / deserializeコードを自動生成

以下のコマンドを実行するとモデルクラスに対応した自動生成コードが生成されます。

$ flutter packages pub run build_runner build

※ 「flutter packages pub run build_runner watch」とすると
モデルクラスの変更を監視してファイル編集の度に自動生成が実行されます。

本サンプルでは「employee.dart」に対応した「employee.g.dart」ファイルが自動生成されます。
何に対して自動生成コードが作成されるかというと「@JsonSerializable()」アノテーションが付与されているクラスに対して生成されます。
自動生成された ./lib/employee.g.dart は以下の通りです。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'employee.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Employee _$EmployeeFromJson(Map<String, dynamic> json) {
  return Employee(json['id'] as int, json['firstName'] as String,
      json['lastName'] as String);
}

Map<String, dynamic> _$EmployeeToJson(Employee instance) => <String, dynamic>{
      'id': instance.id,
      'firstName': instance.firstName,
      'lastName': instance.lastName
    };

ポイント①$EmployeeFromJson() / $EmployeeToJson()

自動生成されたコードには、$EmployeeFromJson() / $EmployeeToJson() の2つのメソッドが定義されています。
中身をよく見ると、、、
$EmployeeFromJson()は、Mapオブジェクトから「id」「firstName」「lastName」の各要素を取り出してEmployeeオブジェクトを作成する実装(つまり、json文字列 -> Employeeの処理)。
$EmployeeToJson()は、Employeeオブジェクトから各プロパティ要素「id」「firstName」「lastName」を取り出してMapオブジェクトを作成する実装(つまり、Employee→Mapの処理)。
になっています。

ポイント②part of 'employee.dart';

自動生成コードの先頭部分を見てみると「part of 'employee.dart';」と記述されています。
「part of」はdart言語仕様のキーワードです。
簡単に言うと「自分は employee.dart の一部ですよ」と定義しています。

ポイント③もう一度 employee.dart を振り返る

前述の employee.dart を以下に再掲します。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'employee.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Employee _$EmployeeFromJson(Map<String, dynamic> json) {
  return Employee(json['id'] as int, json['firstName'] as String,
      json['lastName'] as String);
}

Map<String, dynamic> _$EmployeeToJson(Employee instance) => <String, dynamic>{
      'id': instance.id,
      'firstName': instance.firstName,
      'lastName': instance.lastName
    };
part 'employee.g.dart'; 定義

employee.dart では定型記述として「part 'employee.g.dart';」と定義していました。
dart言語の「part」キーワード定義であり、つまり「employee.g.dartは自分の子(パート)ですよ」という意味になります。

$EmployeeFromJson(json) / $Employee_ToJson(this)の利用

さらに、「Employee.fromJson()メソッドの定義実装で$EmployeeFromJson(json)」を呼び出し、「toJson()メソッド定義実装で$Employee_ToJson(this)」を呼び出していました。

つまり、モデルクラスは「自動生成されるコードに合わせた形」で定義していたわけです。
上記を理解するとコードのロジックがすべてクリアに見渡せるようになると思います。

3.5. Employeeクラスをserialize / deserializeしてみる

では main.dart からEmployeeクラスのserialize / deserializeを行ってみます。

// ./lib/main.dart

import 'package:flutter/material.dart';
import 'dart:convert';
import 'employee.dart';

void main() {
  String orgJson = '{"id":1000,"firstName":"ryuichi","lastName":"daigo"}';
  print('オリジナルJSON文字列: $orgJson');
  
  var mapEmployee = json.decode(orgJson);
  var employee = Employee.fromJson(mapEmployee);
  print('JSON→Employeeデシリアライズしたオブジェクト: id: ${employee.id}, firstName: ${employee.firstName}, lastName: ${employee.lastName}');

  var serializedJson = json.encode(employee.toJson());
  print('再度Employee→JSONシリアライズした文字列: $serializedJson');

  runApp(new MyApp());
}

...省略...

実行結果(コンソール出力)は以下の通り。

$ flutter run

...省略...
I/flutter (28581): オリジナルJSON文字列: {"id":1000,"firstName":"ryuichi","lastName":"daigo"}
I/flutter (28581): JSON→Employeeデシリアライズしたオブジェクト: id: 1000, firstName: ryuichi, lastName: daigo
I/flutter (28581): 再度Employee→JSONシリアライズした文字列: {"id":1000,"firstName":"ryuichi","lastName":"daigo"}
...省略...

3. まとめ

json_serializableについては、初見では、なんか良く分からん定型的な形式でモデルクラスを定義し、ジェネレータを使うといい感じに json serialize / deserialize 可能なパッケージというモヤモヤ感を持っていました。
ただ上述ように、ジェネレートされた僅かなコードを覗くことで、すべてが一本の線につながって気持ちよくなるかと思います。

最後に公式のリンクを4つ貼っておきます。

↓ Flutter公式のjson serialize説明 flutter.io

json_serializableパッケージ pub.dartlang.org

json_annotationパッケージ pub.dartlang.org

↓ build_runnerパッケージ pub.dartlang.org

FlutterのHello Worldを超深掘った話

1. はじめに

Flutter初学者の ryuichi111std です。
以前はXamarin派だったのですが、最近はFlutterに面白さを感じてチョロチョロと学習しています(別にXamarinが嫌いになったわけではないです)。
なので、「Flutterとはこうだ!」みたいな知識はまだ持っていないのですが、その中で理解した知識をまとめておこうと思います。

ということで、まずは画面(ページ)作成の概念についてまとめようと思ったのですが・・・Hello World実装に対して「なぜ?なぜ?病」を発症してしまったので、それについて記録しようかと。。。

(注意) なぜ?なぜ?を深掘った内容なので、とにかく動くものを作る知識が欲しいんだ、という方にはおすすめしない記事ですm(_ _)m

使用環境:Mac Book Pro(High Sierra) / Flutter 0.7.3 / Visual Studio Code

2. Hello World

公式サイトにも載っていますが、まずは「Hello World」を作ります。
以下のコマンドで hello_world プロジェクトを生成。

$ flutter create hello_world

Visual Studio Codeで hello_world を開きます。(Android Studioでもお好みで)
「lib/main.dart」に、結構、はじめに見るには重めのテンプレサンプルが生成されるので、実装をざっくり削除して以下の実装に書き換えます。

// lib/main.dart

import 'package:flutter/widgets.dart';

void main() {
  Text text = new Text(
    "Hello World", 
    textDirection: TextDirection.ltr,);
  Center center = new Center(child: text);
  runApp(center);
}

実行画面は以下のようになります。

f:id:daigo-knowlbo:20180915172241p:plain

2.1. main()関数

多くの開発言語・環境と同じく main()関数 がエントリーポイントになります。

2.2. Text / Centerの作成

Text と Center が「Hello Flutter」と画面に表示するためのUI要素の定義になります。
Flutterでは、これらは共に「Widget」と呼ばれます。

Textオブジェクトの生成には、第1引数で表示する文字列を、オプション引数 textDirection にはテキストの表示方向を指定します(ltr=左から右)。
サンプルとしてはtextDirectionが余分に感じますが、Textオブジェクトでは指定必須のオプションパラメータになります(指定しないと実行時エラーが発生)。

Centerオブジェクトは、名前の通り子要素をセンターに配置する役割を持ちます。コンストラクタで、childプロパティにTextオブジェクトを指定します。

※ 最小実装の Hello World であれば、Textオブジェクトのみで実装することが出来ますが、その場合、画面左上にちょろっとテキストが表示される残念な状態になります(iPhone Xのような角が丸いタイプの画面だと角に文字がはみ出た状態にもなります)。

2.3. runApp()関数

runApp()関数は、引数に Widget オブジェクトを取り、そのWidgetをルートWidgetとして、それを画面いっぱいに満たすようにしてFlutterアプリケーションを実行します。

※ runApp()の定義は https://docs.flutter.io/flutter/widgets/runApp.html で確認することが出来ます。

2.4. import 'package:flutter/widgets.dart'

外部で定義されたオブジェクトを利用するためのインポート文です。
公式の HelloWorld だと「import 'package:flutter/material.dart'」が行われています。
ただ、最低限のimportでは本サンプルで使用した「import 'package:flutter/widgets.dart'」になります。
Text / Center / TextDirection / runApp を利用するために必要なimportなのですが・・・

2.4.1. Textクラス

textは、Flutterの実装では「flutter/packages/flutter/lib/src/widgets/text.dart」で実装されています。

https://github.com/flutter/flutter/blob/1ad538e454c77496fbd068b9e8b5f8b61c2f6d96/packages/flutter/lib/src/widgets/text.dart#L214

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/text.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/widgets.dart

Text.dartは、「class Text extends StatelessWidget { }」という形で Textクラス を実装しています。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/text.dart

2.4.2. Centerクラス

Centerは、Flutterの実装では「Futter/packages/flutter/lib/src/widgets/basic.dart」に実装されています。

https://github.com/flutter/flutter/blob/1ad538e454c77496fbd068b9e8b5f8b61c2f6d96/packages/flutter/lib/src/widgets/basic.dart#L1541

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/basic.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/widgets.dart

Basic.dartは、「class Center extends Align { }」という形で Centerクラス を実装しています。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/basic.dart

ちなみに Centerクラスの基本クラス Align は、同じくbasic.dartファイル内で「class Align extends SingleChildRenderObjectWidget { }」と定義されています。
(更に深掘ると Center <- Align <- SingleChildRenderObjectWidget(framework.dart) <- RenderObjectWidget(framework.dart) <- Widget(framework.dart) というクラス継承構造になっています。)

2.4.3. TextDirection列挙

TextDirectionは、Flutterの実装ではFlutter.engine側の engine/lib/ui/text.dart で実装されています(enum TextDirection { })。

https://github.com/flutter/engine/blob/master/lib/ui/text.dart

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/basic.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/1ad538e454c77496fbd068b9e8b5f8b61c2f6d96/packages/flutter/lib/src/widgets/basic.dart#L1541

basic.dartは「export 'package:flutter/painting.dart';」という実装を持ち、painting.dartは「export 'src/painting/basic_types.dart';」という実装を持ちます。更にbasic_types.dartは「import 'dart:ui' show TextDirection;」という実装を持ちます。
おお、繋がった。dart:ui → engine/lib/ui/text.dart

2.4.4. runApp()

runApp()は、Flutterの実装では「Futter/packages/flutter/lib/src/widgets/binding.dart」に実装されています。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/binding.dart

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/binding.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/widgets.dart

3. まとめ

ということで・・・Text / Center / TextDirection / runApp を使うために必要な import は package:flutter/material.dart であり、1行のimport文から各種必要なクラスの読み込み(export)が行われているFlutter内の実装を明確に理解することが出来ました。
5分で流し読んでしまう Hello World ですが、ちょっとした なぜ?なぜ? からえらい深掘りになったというお話です。。。
でも、本質部分を理解することで、もっと大きな問題に出会ったときに、その解決方法を自力で導き出せると思うので、まあ知っていて損はないかと^^

Flutterはリファレンスもしっかりしていて、

https://docs.flutter.io/index.html

オープンソースなので、何か疑問点やうまく動作しない部分が合ったとき、ある程度内部動作を調べることも出来て、

flutter:
https://github.com/flutter/flutter

engine:
https://github.com/flutter/engine

今後もいろいろリサーチしていこうかと思います。

絶対にくじけないFlutter開発環境構築(VS Code)

1. はじめに

ごめんなさい。タイトル負けしている内容なので先に謝っておきますm( )m

Flutterも大分成熟したようで正式版リリースが待ち遠しいですね。
日本でも、もう既にプロダクトに採用されている事例もあるようでアーリーアダプターではない私も手を付け始めています。

新しい言語、新しいフレームワークなどに手を付ける場合、一番つまずく可能性が高いのは入り口の部分ですよね。
そこをクリアすれば、その技術のテーストが理解でき、感覚的に前に進めるものです。
ということで、この投稿では「何も入っていない、まっさらな MacBook にFlutter開発環境を構築するまで」の軌跡を たらたら ペタペタ 記述&貼り付けしていきます。

今回使った環境: macOS High Sierra(初期インストール直後)

2. コツ?

Flutterの開発環境構築は 公式サイトの説明 及び 診断コマンド(flutter doctor) が非常に優秀で、指示されるままにコマンドを実行していれば比較的容易に開発環境構築完了まで行きつけるのでは、と思います。
「flutter doctor」コマンドは「叩くべきコマンド」や「ダウンロードすべきインストーラのあるURL」を丁寧に教えてくれます。
とはいえ、やることは段階的にいろいろあるので躓くことはあるかと。。。
基本的には、最終的に以下が抑えられればイケるはず!

  • XCodeiOSアプリがビルド&実行できる状態になっている
  • Android SDKがインストールされandroidアプリがビルド&実行できる状態になっている(簡単に言うとAndroid Studio環境が整っていればOK)
  • その他 Flutter が要求する諸々・・・

3. まっさらなMacにFlutter開発環境を構築するぞ!

皆さんの各環境毎に、各モジュール類がどれだけ既にインストールされているかによってやるべきことは変わってくるのですが、ここではまっさらなMac(High Sierra)に対してFlutterをインスールした作業をペタペタ貼っていきます。(なので、最短距離のインストール手順ではないです。本当に履歴をペタペタという感じ)

3.1. Flutterをダウンロード

公式サイトからzipファイルをダウンロードします。

flutter.io

「Get the Flutter SDK」項目の下の「flutter_macos_v0.7.3-beta.zip 」です。

3.2. Flutterのzipを解凍(配置)

ダウンロードしたzipを任意のディレクトリに解凍します。
Flutterのbin自体が含まれているので、いわゆるインストール先になります。
そういう意味で適切なディレクトリに置いてください。
ここでは公式サイトの例と同じく ~/develop に解凍します。
解凍したファイルは以下のような状態です。

$ cd ~/develop/flutter
$ ls -la

[beta]
total 168
drwxr-xr-x@ 24 daigo  staff    768  9  9 11:49 .
drwxr-xr-x  50 daigo  staff   1600  9  9 09:58 ..
-rw-r--r--@  1 daigo  staff   6148  9  9 11:51 .DS_Store
-rw-r--r--@  1 daigo  staff   6214  9  5 11:47 .cirrus.yml
drwxr-xr-x@ 14 daigo  staff    448  9  8 18:01 .git
-rw-r--r--@  1 daigo  staff    516  9  5 11:47 .gitattributes
drwxr-xr-x@  5 daigo  staff    160  9  5 11:47 .github
-rw-r--r--@  1 daigo  staff   1566  9  5 11:47 .gitignore
drwxr-xr-x@  6 daigo  staff    192  9  5 11:50 .idea
drwxr-xr-x@  4 daigo  staff    128  9  5 11:47 .pub-cache
-rw-r--r--@  1 daigo  staff   1004  9  5 11:47 AUTHORS
-rw-r--r--@  1 daigo  staff  13316  9  5 11:47 CONTRIBUTING.md
-rw-r--r--@  1 daigo  staff   1520  9  5 11:47 LICENSE
-rw-r--r--@  1 daigo  staff   1107  9  5 11:47 PATENTS
-rw-r--r--@  1 daigo  staff   6706  9  5 11:47 README.md
-rw-r--r--@  1 daigo  staff   7046  9  5 11:47 analysis_options.yaml
-rw-r--r--@  1 daigo  staff    350  9  5 11:47 appveyor.yml
drwxr-xr-x@  6 daigo  staff    192  9  5 11:47 bin
drwxr-xr-x@ 14 daigo  staff    448  9  9 11:51 dev
drwxr-xr-x@ 12 daigo  staff    384  9  5 11:47 examples
-rw-r--r--@  1 daigo  staff   1749  9  5 11:47 flutter_console.bat
-rw-r--r--@  1 daigo  staff    296  9  5 11:47 flutter_root.iml
drwxr-xr-x@ 12 daigo  staff    384  9  9 11:50 packages
-rw-r--r--@  1 daigo  staff      5  9  9 19:36 version

3.3. Flutterにパスを通す

~/.bashrcにFlutterへのパスを追記しておきます(zshの方は ~/.zshrcへ)。

~/.bashrc ファイル

export PATH=~/develop/flutter/bin:$PATH

flutterコマンドの動作確認をします。「source ~/.bashrc」するかターミナル新規オープンして、以下のコマンドを実行します。

$ flutter --version

あと、初期状態のMacだと ~/.bashrc が読み込まれない設定になっているので、その場合は以下を参考に ~/.bash_profile ファイルを記述します。

blog.ruedap.com

3.4. flutter doctorで診断する

flutter doctor コマンドを実行します。
「Flutter / Android toolchain / iOS / Android Studio / Connected devices」という5つのカテゴリー毎に、Flutter開発環境を構築する上で
「何が足りていないのか?」
「どうすれば解決できるのか?」
を事細かに診断してレポートしてくれます。
以下がコマンドの実行結果です。

$ flutter doctor

Building flutter tool...
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.io/setup/#android-setup for detailed instructions).
      If Android SDK has been installed to a custom location, set $ANDROID_HOME to that location.
[✗] iOS toolchain - develop for iOS devices
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    ✗ Brew not installed; use this to install tools for iOS device development.
      Download brew at https://brew.sh/.
[✗] Android Studio (not installed)
[!] Connected devices
    ! No devices available

! Doctor found issues in 4 categories.

よく見ると「具体的に実行すべきコマンド」「ダウンロードすべき物のURL」がすべて出力されています。
ということで指摘された問題点を順番に解決していきます。

3.5. gitも入ってないとこれも言われるかも・・・

gitも入っていないと思うので、たしか以下の画面が出てくるかも(この点、記憶が曖昧・・・)。

f:id:daigo-knowlbo:20180909224115p:plain

↑が出たら、インストールボタンを押してgitをインストールします。

3.6. Android toolchainの問題を解決

Android SDKないよ。Android Studiohttps://developer.android.com/studio/index.html からダウンロードしてインストールして。一回起動すればSDKコンポーネントのインストールをサポートしてくれるよ。」と言っています。
支持に従ってAndroid Studioをダウンロード&インストールします。

f:id:daigo-knowlbo:20180909224241p:plain

支持に従ってインストールして、新規にプロジェクト(MyApplication)を作成すると、以下のような画面になるかと。

f:id:daigo-knowlbo:20180909224323p:plain

右上に「Gradle project sync failed...」と表示され、右下に「Failed to find Tools revisions 27.0.3」と表示されています。
すぐ下の「Install BuildTools 27.0.3 and sync project」(青い文字)をクリックして、不足しているツールをインストールします。

f:id:daigo-knowlbo:20180909224452p:plain

で、実行します(緑の▶ボタン)。

どのデバイスで実行するかの選択画面が表示されます。

f:id:daigo-knowlbo:20180909224652p:plain

物理デバイス繋いでないし、emulatorも作ってないので、左下の「Create New Virtual Device」をクリックします。

適当にNexus6にして

f:id:daigo-knowlbo:20180909224758p:plain

Nougat x86をダウンロードして、、、Finish。

f:id:daigo-knowlbo:20180909224813p:plain f:id:daigo-knowlbo:20180909224905p:plain

出来たっぽい。
f:id:daigo-knowlbo:20180909224837p:plain

作ったemulatorを選択してOK。

f:id:daigo-knowlbo:20180909224924p:plain

「Instant Run」のインストールを促される。要らないと思うけど、ここでは一応入れました。

f:id:daigo-knowlbo:20180909225146p:plain

動いた!

f:id:daigo-knowlbo:20180909225157p:plain

いい感じにAndroid開発環境が出来たっぽいので、もう一回 flutter doctor してみる。

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):

[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.13.6 17G65, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK 28.0.2)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[✗] iOS toolchain - develop for iOS devices
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    ✗ Brew not installed; use this to install tools for iOS device development.
      Download brew at https://brew.sh/.
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] Connected devices
    ! No devices available

! Doctor found issues in 3 categories.

Androidは、まだダメ出しされました。→「Some Android licenses not accepted.」
実行すべきコマンドの指示も出ているので、素直に実行。

$ flutter doctor --android-licenses

Warning: File /Users/daigosora/.android/repositories.cfg could not be loaded.

5 of 6 SDK package licenses not accepted. 100% Computing updates...     

Review licenses that have not been accepted (y/N)? y

・・・省略・・・こんな感じで accepted y/N を何回か聞かれるので yエンター、yエンターする(ちゃんと読んで承諾してね)。

しつこいけど、ここで flutter doctor すると以下のようにAndroid toolchainがOKとなります。

$ flutter doctor

[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.2)
[✗] iOS toolchain - develop for iOS devices
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    ✗ Brew not installed; use this to install tools for iOS device development.
      Download brew at https://brew.sh/.
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] Connected devices
    ! No devices available

! Doctor found issues in 2 categories.

3.7. iOS toolchainの問題を解決

次はiOSの問題の解決を。
doctorは、「XCodeをインストールして!ダウンロードするかAppストアからインストールしてね」と言っているので、Appストアを開きXCodeをインストールします。

f:id:daigo-knowlbo:20180909225447p:plain

指示ではインストール後に「sudo xcode-select --switch 」しろと書いてあるけど、ここではXCode起動していOSアプリがSimulatorで動くことを確認します(コマンドじゃなくて、これでもOK)。

XCodeを起動します。
「Create a new Xcode project」をクリックします。

f:id:daigo-knowlbo:20180909225526p:plain

とりあえず「Single View App」を選択。

f:id:daigo-knowlbo:20180909225810p:plain

こんな設定で。

f:id:daigo-knowlbo:20180909225747p:plain

そのまま実行します(▶ボタンクリック)。

f:id:daigo-knowlbo:20180909230012p:plain

OK!!(XCodeのSimple View Appはで生成したアプリはラベルも何もない状態なので分かりにくいけど真っ白な画面が正常状態です)

(1) brewをインストール

そうそう、もう1つ指摘されていました。
Brew not installe」
初期状態Macから始めているのでbrewも入っていません。
指示された「https://brew.sh/」をブラウザで開くと、brewインストールコマンドが表示されるます。
そのコマンドをそのままターミナルにコピー&Enterします。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

(2) 再度 flutter doctor

再度、flutter doctor してみます。

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.2)
[!] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
    ✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To install:
        brew install cocoapods
        pod setup
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] Connected devices
    ! No devices available

! Doctor found issues in 2 categories.

iOS toolchain はまだクリアしませんねー。
まあ、実行すべきコマンドは丁寧に指示してくれています。
これらは単純にターミナルにコピーして実行するだけです。

$ brew install --HEAD libimobiledevice
...
$ brew install ideviceinstaller
...
$ brew install ios-deploy
...
$ brew install cocoapods
...
$ pod setup

3.8. 結構いい感じのはず。再度 flutter doctor

しつこいけど 再度 flutter doctor を実行してみます。

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.2)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] Connected devices
    ! No devices available

! Doctor found issues in 1 category.

いい感じですね。
Android Studioに関してはFlutterプラグインが入ってないと言われてますが、私はエディタ(IDE)としてVisual Studio Codeを使うつもりなのでこれは無視して構いません。Android StudioでFlutter開発を行うつもりならプラグインをインストールする必要があります。
「Connected devices」については、この段階では、実機を接続していない+emulator/simulatorも起動していないので、No devices availableと表示されているだけです。
Android Studioセットアップ時にAndroidアプリがemulatorで起動することは確認していますし、iOSについても同様にXCodeで作成したiOSアプリがSimulatorで動作するのを確認済みなのでこの点は問題ありません。

4. Visual Studio Codeをインストール

ここでは、Flutter開発のエディタ(IDE)としてVisual Studio Codeを利用します。
ということでVS Codeをダウンロード&インストール!

code.visualstudio.com

上記から、zipファイルをダウンロードし、解凍したアプリを「アプリケーション」にヒョイって持っていく感じでインストールします。

4.1. Flutter拡張機能プラグイン)をインストール

VS Codeの左の「拡張機能」アイコンをクリックします。

f:id:daigo-knowlbo:20180909231054p:plain

「Flutter」で検索。そのまま「Flutter」という名前の拡張機能が出てくるので、インストールします。VS Codeは再起動させます。

f:id:daigo-knowlbo:20180909231141p:plain

これで、開発環境構築完了です!!

5. Hello Flutterを作ってみる

では、Hello Flutterプロジェクトを作成して iOS / Android で動作するのか試してみましょう。

ターミナルを開き、プロジェクトを作成したい任意のディレクトリに移動します。
ここでは ~/workspace としました。(go 1.10みたいに環境ディレクトリ縛りは無いのでご自由な場所で)

ディレクトリを作って。

$ cd ~/
$ mkdir workspace
$ cd workspace
$ pwd

/Users/daigo/workspace

「flutter create 【プロジェクト名】」コマンドでFlutterアプリの雛形が作成されます。

$ flutter create helloflutter

Creating project helloflutter...

  helloflutter/ios/Runner.xcworkspace/contents.xcworkspacedata (created)

  helloflutter/ios/Runner/Info.plist (created)

...省略...

Wrote 65 files.

[✓] Flutter is fully installed. (Channel beta, v0.7.3, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✓] Android toolchain - develop for Android devices is fully installed. (Android SDK 28.0.2)
[✓] iOS toolchain - develop for iOS devices is fully installed. (Xcode 9.4.1)
[✓] Android Studio is fully installed. (version 3.1)
[!] Connected devices is partially installed; more components are available.

Run "flutter doctor" for information about installing additional components.

All done! In order to run your application, type:
  $ cd helloflutter
  $ flutter run
Your main program file is lib/main.dart in the helloflutter directory.

5.1. Visual Studio Codeで開く

「ファイル→開く」で「~/workspace/helloflutter」を開きます。
./lib/main.dart ファイルがアプリのメインコード(エントリーポイント)になります。

f:id:daigo-knowlbo:20180909231338p:plain

ステータスバー(下の水色のバー)の右の方に No Devices と表示されているので、クリックします。 すると、上のコマンドパレットに、事前に作成済みの「Nexus 6」と「iOS Simulator」が表示されます。

f:id:daigo-knowlbo:20180909231357p:plain

まず「iOS Simulator」を選択します。するとiOS Simulatorが起動してきます。

f:id:daigo-knowlbo:20180909231410p:plain

続けてVS Codeメニュー「デバッグデバッグの開始」を選択します(もしくはF5クリック)。

f:id:daigo-knowlbo:20180909231429p:plain

サンプルが起動しました。
VS Code上部の、「赤い■ボタン」を押すとデバッグが終了します。

次にステータスバー右の「iPhone X(iosSimulator)」をクリックし、「Nexus 6」を選択します(表示されないときは、一旦 iOS Simulator を終了してみてください。もしくはAndroid Emulatorを事前に起動しておいてください)。
するとAndroid Emulatorが起動してきます。
VS Codeメニュー「デバッグデバッグの開始」を選択します(もしくはF5クリック)。

f:id:daigo-knowlbo:20180909231506p:plain

androidでも無事に実行出来ました!

ブレークポイントもHot Reloadも効くよ

この状態で、
Visual Studio Codeブレークポイントを付ければもちろん止まります。
更にデバッグ実行状態でコードを修正して保存すれば「Hot Reload」も動作します(コード修正が即座に実行中アプリに反映)。

6. まとめ

ということで、必要以上に(?)長々とFlutter開発環境構築手順について書いてきました。
皆さんの環境毎に追加でインストールすべきモジュールは異なります。
それらは親切にFlutter doctorが教えてくれもします。
本投稿では、まっさらなMac環境からFlutter開発環境を構築するまでの手順(というか軌跡というか?)をまとめました。
ここに記述させていただいた内容で、何かに躓いてしまった方の助けになる情報が1つでもあれば幸いです。

goa v2.0.0-wip 事始め

1. golangはじめました(趣味で)

3〜4週間前から趣味でgolangを始めてみました。
ということでやっと基礎学習も出来つつあり、色々試し始められたので、たまにブログっていければと思います。
唐突ですが goa というものに興味を持って使ってみたので、その辺のお話を取り上げたいと思います。

また、本投稿の動作確認に使った環境は以下のとおりです。
* macOS High Sierra 10.13.6
* go 1.10.3
* $GOPATH=/Users/daigo/go

2. goaとは

goaとは、APIを作る際 DSLによりデザインファイルを記述すると そこから自動で実装の雛形やドキュメント(swaggerファイル)を生成してくれるマイクロサービス用フレームワークです。
私もそうだったのですが、DSLって聞くと、また新しい記法を覚えなくちゃいけないのかぁ・・・と思いますが、goaのDSLgolangで記述することができるので、敷居はだいぶ低くなっていると思います。まあ、golangによるgoaの記法を覚える必要はありますが(汗
ということで、goaよりも以前からある swagger からの流れと同じく、以下のような問題の解決を図るフレームワークとなります。

  • いきなりAPI書くんじゃなくてきちんと設計しようよ
  • でもドキュメント(設計書)書くのめんどいよね(トラディショナルカンパニーのExcel設計書とかクソだよね)
  • バックエンドAPI開発者とフロントエンド開発者が分離したりしてくると仕様の齟齬は避けたいよね
  • ドキュメント書いてもコードのバージョンアップにきちんと追随したいよね(労力最小限で)

v1とv2

golang本体もそうですが、周辺フレームワークのバージョンアップもまだまだかなり激しいですよね。
goaのバージョンは2018/8/19時点で、「stableバージョン=v1.4.0」「次のメジャーバージョン=v2.0.0-wip」です。
v2はまだwipな状態ですが、github上で以下のように記述されている通り、既に結構作り込まれているのではないかと思います。

goa v2 is currently in beta: it is robust enough to be used in production but there may still be breaking changes before the final release. If you're new to goa then you may want to consider starting with v2.

v1.x系とv2.0ではDSLの記法、ツールコマンドの使い方が変わっているので改めて学習する必要があります。とはいえ、向かっている思想は同じなのでスムーズに移行できるのではないかと思います。(既存コードのMigrationは難しい?その観点では調べてないので?はてな?です)

goaのgihhubは以下(v1.x)で↓↓↓

github.com

その中にv2ブランチが切られています。↓↓↓

github.com

3. goa v2をインストール

goa v2のインストールは以下のコマンドでいけます。

$ go get -u goa.design/goa/...

インストールの確認は「goa version」コマンドで行えます。

$ goa version
goa version v2.0.0-wip

4. goa v2を使ってみる

公式github上のドキュメントトップでも説明されているレベルの「はじめてのgoa v2」的なサンプルを作っていこうと思います。
サンプル仕様はDBもセッションも使わず完全ステートレスなWebAPIとします。

ガチャサービスとして以下の2つのメソッドを実装します。
* ガチャを回す(GETリクエスト、URLパラメータでガチャタイプをIntで指定)
 → GET http://localhost:8080/{type}
 → ガチャ結果はStringで返却
* チートガチャを回す(POSTリクエスト)
 → POST http://localhost:8080/ POSTデータ=secret_key(string)
 → ガチャ結果はStringで返却

4.1. プロジェクトの作成

gachaプロジェクトを作成します。
(ひとまずdepも使わないし、ディレクトリ掘るだけ)

$ cd $GOPATH/src
$ mkdir gacha
$ cd gacha

結局、この段階で ↓ にいる感じです。

$ pwd
/Users/daigo/go/src/helloGoa/gacha

4.2. designファイルの定義

goaではdesignファイルを用意して、そこにgolangAPI仕様を記述します。

$ mkdir design
$ vi design/design.go
// design/design.go

package design

import . "goa.design/goa/http/design"
import . "goa.design/goa/http/dsl"

var _ = API("gacha", func() {
    Title("gacha service")
    Description("サンプルのガチャサービスです。")
    Server("http://localhost:8080")
})

var _ = Service("gachasvc", func() {
    Method("pon", func() {
        Description("ガチャを回します")
        Payload(func() {
            Attribute("type", Int, "gacha type")
        })
        Result(String)
        HTTP(func() {
            GET("/pon/{type}")
        })
    })

    Method("cheat_pon", func() {
        Description("チートガチャを回します")
        Payload(func() {
            Attribute("secret_key", String, "秘密の鍵")
        })
        Result(String)
        HTTP(func() {
            POST("/pon/cheat/")
        })
    })
})

API

APIはトップレベDSLです。
APIのタイトルやホスト名バージョン等の記述を行います。
指定できる仕様の詳細は公式Docにありますが、ここでは省略します。

Service / Method

サービスDSLは、メソッドを定義します。
なんとなく眺めると理解できると思いますが、「pon」と「cheart_pon」の2つのメソッドが定義されています。
descriptionは、そのままメソッドの説明ですね。
Payloadは、メソッドが受け取るペイロード(引数)の定義です。サンプルではそれぞれのメソッドが1つづつ受け取っていますが、複数の場合はAttribute()定義を並べて定義します。
Resultは、メソッドの結果の戻り値の型定義になります。
HTTPは、メソッドをHTTPでホストすること、そしてHTTP Verb(GET/POST等)とルーティングURLを定義しています。
Payload/ResultとHTTPが分離されているところが重要で、「メソッド名/INの引数/OUTの戻り値の定義」と@トランスポートのプロトコル」は分離して考えることが出来ます。例えば、HTTPではなくgRPCでホストする定義に簡単に書き換えることが出来ます(今日段階ではgRPCはgoa v2で対応していない?)。

4.3. コード自動生成

では、goaのDSL定義(design.go)からコードの自動生成を行います。
「goa gen gacha/design」コマンドを実行します。
gacha/designの部分は $GOPATH/src からの相対パスでdesign.goが定義されているパスを指定します。

$ goa gen gacha/design                                                                                                               [~/go/src/gacha]

gen/gachasvc/client.go
gen/gachasvc/endpoints.go
gen/gachasvc/service.go
gen/http/cli/cli.go
gen/http/gachasvc/client/cli.go
gen/http/gachasvc/client/client.go
gen/http/gachasvc/client/encode_decode.go
gen/http/gachasvc/client/paths.go
gen/http/gachasvc/client/types.go
gen/http/gachasvc/server/encode_decode.go
gen/http/gachasvc/server/paths.go
gen/http/gachasvc/server/server.go
gen/http/gachasvc/server/types.go
gen/http/openapi.json
gen/http/openapi.yaml

15ファイルほど自動生成されました。
サービスエンドポイントやHTTPトランスポートレイヤの実装などが自動生成されています。
swaggerファイル(openapi.json / openapi.yaml)も自動生成されています。

openapi.jsonは以下のような定義になっています。

# 自動生成された openapi.json
{
  "swagger": "2.0",
  "info": {
    "title": "gacha service",
    "description": "サンプルのガチャサービスです。",
    "version": ""
  },
  "host": "localhost:8080",
  "paths": {
    "/pon/cheat": {
      "post": {
        "tags": [
          "gachasvc"
        ],
        "summary": "cheat_pon gachasvc",
        "description": "チートガチャを回します",
        "operationId": "gachasvc#cheat_pon",
        "parameters": [
          {
            "name": "CheatPonRequestBody",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/GachasvcCheatPonRequestBody"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK response.",
            "schema": {
              "type": "string"
            }
          }
        },
        "schemes": [
          "http"
        ]
      }
    },
    "/pon/{type}": {
      "get": {
        "tags": [
          "gachasvc"
        ],
        "summary": "pon gachasvc",
        "description": "ガチャを回します",
        "operationId": "gachasvc#pon",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "description": "ガチャタイプ",
            "required": true,
            "type": "integer"
          }
        ],
        "responses": {
          "200": {
            "description": "OK response.",
            "schema": {
              "type": "string"
            }
          }
        },
        "schemes": [
          "http"
        ]
      }
    }
  },
  "definitions": {
    "GachasvcCheatPonRequestBody": {
      "title": "GachasvcCheatPonRequestBody",
      "type": "object",
      "properties": {
        "secret_key": {
          "type": "string",
          "description": "秘密の鍵",
          "example": "Qui iste laboriosam quis ducimus fugiat qui."
        }
      },
      "example": {
        "secret_key": "Accusamus iste."
      }
    }
  }
}

「goa gen」コマンドではサービス自体のドメインロジックの実装部や、サーバとしてホストするロジックが生成されていません。
これらを実装する出発点となる実装の生成は「goa example gacha/design」コマンドで行うことが出来ます。

$ goa example gacha/design

cmd/gacha_cli/main.go
cmd/gacha_svc/main.go
gachasvc.go

3つのgoファイルが生成されました。

cmd/gacha_cli/main.go

APIメソッドを呼び出すテスト用CLIコードです。

cmd/gacha_svc/main.go

HTTPでサービスをホストする実装コードです。ログ出力等含めた実装になるので、ここからカスタマイズして自分の実装に持っていってもいいと思います。

gachasvc.go

ガチャサービスの実装雛形です。 ここに自分のドメインロジックの実装を行います。

// 自動生成された gachasvc.go

package gacha

import (
    "context"
    gachasvc "gacha/gen/gachasvc"
    "log"
)

// gachasvc service example implementation.
// The example methods log the requests and return zero values.
type gachasvcSvc struct {
    logger *log.Logger
}

// NewGachasvc returns the gachasvc service implementation.
func NewGachasvc(logger *log.Logger) gachasvc.Service {
    return &gachasvcSvc{logger}
}

// ガチャを回します
func (s *gachasvcSvc) Pon(ctx context.Context, p *gachasvc.PonPayload) (res string, err error) {
    s.logger.Print("gachasvc.pon")
    return
}

// チートガチャを回します
func (s *gachasvcSvc) CheatPon(ctx context.Context, p *gachasvc.CheatPonPayload) (res string, err error) {
    s.logger.Print("gachasvc.cheat_pon")
    return
}

上記の Pon() / CheatPoin() の中身を実装すればいいですね。
自動生成されたコードでは、ログ出力と空文字のreturnが実装されています。

4.4. ドメインロジックの実装

では、Pon() / CheatPoin()の2つのメソッドに対する、面白みのない実装は以下のとおりです。
(ベタに読めるように、べた書きしています。)
乱数を発生させて確率で「SS激レア・Sレア・レア」の結果文字列を返却しています。
パラメータは *gachasvc.PonPayload ポインタ型として引数で受け取れるので * p.Type みたいに参照することが出来ます。(ポインタなので * を忘れずに)

// gachasvc.goに実装を追加

// ガチャを回します
func (s *gachasvcSvc) Pon(ctx context.Context, p *gachasvc.PonPayload) (res string, err error) {
    s.logger.Print("gachasvc.pon")

    rand.Seed(time.Now().UnixNano())
    r := rand.Intn(10)

    if *p.Type == 0 { // ノーマルガチャ
        if r == 9 {
            return "SS激レア", nil
        } else if r > 6 {
            return "Sレア", nil
        }
    } else if *p.Type == 1 { // 高確率ガチャ
        if r == 7 {
            return "SS激レア", nil
        } else if r > 3 {
            return "Sレア", nil
        }
    }
    return "レア", nil
}

// チートガチャを回します
func (s *gachasvcSvc) CheatPon(ctx context.Context, p *gachasvc.CheatPonPayload) (res string, err error) {
    s.logger.Print("gachasvc.cheat_pon")

    if *p.SecretKey == "神の手" {
        return "SS激レア", nil
    }

    // 不正チート。垢BAN処理!!!!

    return "ノーマル", nil
}

4.5. ビルド&実行

ではビルドして実行しましょう。
まず、プロジェクトルート($GOPATH/src/gacha)で「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha
$ go build

サーバ(サービス)の実行

次にサーバ(サービス)実装をビルドします。
$GOPATH/src/gacha/cmd/gacha_svcディレクトリで「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha/cmd/gacha_svc
$ go build

lsすると gacha_svc が生成されたのを確認できます。

$ ls -la 
total 19528
drwxr-xr-x  4 daigo  staff      128  8 19 12:58 .
drwxr-xr-x  4 daigo  staff      128  8 19 12:01 ..
-rwxr-xr-x  1 daigo  staff  9990876  8 19 13:10 gacha_svc
-rw-r--r--  1 daigo  staff     4073  8 19 12:01 main.go

サービスを実行します。

$ ./gacha_svc

[gacha] 13:13:09 method "Pon" mounted on GET /pon/{type}
[gacha] 13:13:09 method "CheatPon" mounted on POST /pon/cheat
[gacha] 13:13:09 listening on :8080

OK。8080ポートでリスニングしてくれています。

クライント(CLI)の実行

ブラウザやPostmanなどからサービスを叩くことも出来ますが、goa example がサーバ(サービス)を叩くクライントCLIも自動生成してくれています。
もう1つターミナルを開いて、「$GOPATH/src/gacha/cmd/gacha_cliディレクトリで「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha/cmd/gacha_cli
$ go build

lsすると gacha_cli が生成されたのを確認できます。

$ ls -la 
total 18000
drwxr-xr-x  4 daigo  staff      128  8 19 12:58 .
drwxr-xr-x  4 daigo  staff      128  8 19 12:01 ..
-rwxr-xr-x  1 daigo  staff  9211836  8 19 13:17 gacha_cli
-rw-r--r--  1 daigo  staff     2285  8 19 12:01 main.go

「./gacha_cli --help」で使い方が表示されます。

./gacha_cli is a command line client for the gacha API.

Usage:
    ./gacha_cli [-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags]

    -url URL:    specify service URL (http://localhost:8080)
    -timeout:    maximum number of seconds to wait for response (30)
    -verbose|-v: print request and response details (false)

Commands:
    gachasvc (pon|cheat-pon)
    
Additional help:
    ./gacha_cli SERVICE [ENDPOINT] --help

Example:
    ./gacha_cli gachasvc pon --type 2753046654881133159

/pon/{type}への GET リクエストは以下の通り。

$ ./gacha_cli gachasvc pon --type 0

"Sレア"

/pon/cheat/への POST リクエストは以下の通り。

$ ./gacha_cli gachasvc cheat-pon --body '{
     "secret_key": "神の手"         
  }'
 
"SS激レア"

5. まとめ

ということで goa の基本的な利用の流れの紹介でした。
記事中では十分に紹介しきれていませんが、改めてgoaを使うメリットは以下のようなものがあります。

  • なるべく少ないコストで、API設計ドキュメントを作成・メンテする事ができる(goa DSL -> swagger doc)
  • APIメソッドの入力パラメータ仕様に基づいてgoa側(+生成されたコード)がバリデーション+パラメータエンティティオブジェクトへの割当を行ってくれる
  • つまり開発者は、ドメイン領域へのコーディングに集中することができる

ということで、本ブログでは初めてgolang関連を取り上げましたが、私の所属会社ではメインのRubyに続いてgolangが最近元気なようなので、引き続きgolangに(趣味で)取り組んで行こうと思うので引き続きブログっていきたいと思います。

(Durable Functions)「第22回 Azureもくもく会」に参加+LTした話

久しぶりに kingkino@マンダム (@kingkinoko) on Twitterさん主催のAzureもくもく会に参加しました。
で、LTもさせていただきました。

1. 何を もくもく+LT したの?

「Durable Functionsの基礎学習」とその発表でした。
(何かを作り上げた!みたいなものではないのだけれど、昔から技術のバックグラウンドを調べたりするの好きなので)

今年に入ってから、仕事では .NET/Azure を離れ、Java/AWS の世界に移っていましたが、ぽろぽろと趣味で.NET/Azureは やっていました。
で、先日のGlobal Azure Boot Camp 2018に参加してAzure界隈の方々とお話しさせていただいたら「Durable Functions」がホットだということで。
GW終わり辺りからドキュメントを読み、もくもく+LTをさせていただいたという感じです。

2. 発表資料

発表資料は↓↓↓です。

speakerdeck.com

なのですが、「学んだこと」「伝えたかった事」のメインはDemoをさせていただいた部分でした。
加えて、「内容がLT・プレゼン向きじゃない」+「私の説明が決して上手くない」ということで、伝わりにくかったかと思います。

ということでDemoした部分について、ブログらせて頂こうかと・・・

3. 何を学んだのか?何をDemoしたのか?

MS公式のDurable Functionsの資料を読んだ結果、以下のドキュメントがDurable Functionsが Durable である所以的な部分を指しているような気がしました。

docs.microsoft.com

しかし、
「ドキュメントに記述されているルールに従って実装すれば、いい感じにDurable Functionsは動くのだろうけど、何故上手く動くのかが腑に落ちない」
という感覚を持ち、その もやもや を払拭するための技術探索を行いました。
つまり、以下のような内容が、話としては理解するけれど、具体的にそうなる根拠となるバックエンドの知識が欲しい、と。。。

・オーケストレーター関数
 決定論的である必要がある。
 複数回実行されても結果が同じでなければならない。
  →現在日付の取得・GUIDのランダム生成の実行はNG。
  →DateTime.Now ではなく DurableOrchestrationContext.​Current​Utc​Date​Time を使う
・アクティビティ関数
 非決定論的操作が可能。
 ある責務を持ったドメイン ファンクションはここで定義。
  →つまり「DBアクセス」などの処理はアクティビティ関数に実装
・イベントソーシング(パターン)で実装されている。
・Azure Storage(キュー・テーブル)に実行履歴をチェックポイントする事で信頼性を得ている。
・awaitが呼び出されるとオーケストレーター関数をゼロから再実行する。

4. Demo内容

Durable Functionsがどのようなフローで実行されるのか?Azure Storageを使ってどのようにDurableに実行されるのか?(直訳すれば "丈夫に" "恒久的に"ですね)
ほぼVSテンプレが吐き出す以下のコードで検証します(Http TriggerによるDurable Functionsコード)。
カスタムしたのは 19 / 20行目 を追加したぐらいでしょうか。

01: using System;
02: using System.Collections.Generic;
03: using System.Net.Http;
04: using System.Threading.Tasks;
05: using Microsoft.Azure.WebJobs;
06: using Microsoft.Azure.WebJobs.Extensions.Http;
07: using Microsoft.Azure.WebJobs.Host;
08: 
09: namespace FunctionApp2
10: {
11:   public static class Function2
12:   {
13:     [FunctionName("Function2")]
14:     public static async Task<List<string>> RunOrchestrator(
15:       [OrchestrationTrigger] DurableOrchestrationContext context)
16:     {
17:       var outputs = new List<string>();
18: 
19:       var currentUtc = context.CurrentUtcDateTime;
20:       var current = DateTime.Now; // ※本当はオーケストレーター関数で実行しちゃいけないやつ
21: 
22:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Tokyo"));
23: 
24:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Seattle"));
25: 
26:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "London"));
27: 
28:       return outputs;
29:     }
30: 
31:     [FunctionName("Function2_Hello")]
32:     public static string SayHello([ActivityTrigger] string name, TraceWriter log)
33:     {
34:       log.Info($"Saying hello to {name}.");
35:       return $"Hello {name}!";
36:     }
37: 
38:     [FunctionName("Function2_HttpStart")]
39:     public static async Task<HttpResponseMessage> HttpStart(
40:       [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
41:       [OrchestrationClient]DurableOrchestrationClient starter,
42:       TraceWriter log)
43:     {
44:       // Function input comes from the request content.
45:       string instanceId = await starter.StartNewAsync("Function2", null);
46: 
47:       log.Info($"Started orchestration with ID = '{instanceId}'.");
48: 
49:       return starter.CreateCheckStatusResponse(req, instanceId);
50:     }
51:   }

ブレークポイントは張りまくります。

f:id:daigo-knowlbo:20180512223119p:plain

テストではローカルAzure Storage(Emu)を使いますが、中身を完全に空にしておきます。

f:id:daigo-knowlbo:20180512223410p:plain

4.1. 実行

(1) デバッグ実行

プロジェクトをデバッグ実行します。
以下のような感じでローカルでAzure Functionsが実行されます。

f:id:daigo-knowlbo:20180512232907p:plain

ここで、Azure Storage ExplorerでStorageの確認を行います。

f:id:daigo-knowlbo:20180512223725p:plain

「Blob Contaners」「Queues」「Tables」に色々作成されています(中身は空)。

※上記Storage項目のそれぞれの説明はここらへんに詳細が書かれています。構成により調整が可能。

(2) HTTPトリガーをキック

HTTPトリガーの受け口である「http://localhost:7071/api/Function2_HttpStart」をポストマンでキックします。

f:id:daigo-knowlbo:20180512224654p:plain

当然、FunctionsのHTTPトリガーである「HttpStart()」が呼び出されます。

f:id:daigo-knowlbo:20180512224751p:plain

「続行(F5)」を行います。
すると、コードの実装の通り「StartNewAsync("Function2")」でFunction2オーケストレーター関数が呼び出されます。
ということで、以下の画面のように Function2オーケストレーター関数 でブレークします。

f:id:daigo-knowlbo:20180512225248p:plain

再び「続行(F5)」して「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」まで飛んだ状態が以下です。

f:id:daigo-knowlbo:20180512233112p:plain

日付取得結果は以下の通りでした。

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:30:46

今度は「ステップオーバー(F10)」します。
CallActivityAsync()により Function2_Hello が呼び出され、以下の場所でブレークします。

f:id:daigo-knowlbo:20180512225917p:plain

この状態でAzure Storage Explorerにて「DurableFunctionsHubHistoryテーブル」を確認すると以下のようレコードが作成されています。

f:id:daigo-knowlbo:20180512233151p:plain

※要するに「Function2オーケストレーター関数→Function2_Helloアクティビティ関数の呼び出しの履歴の保存」がAzure Storageに対して行われたということです。

で、「続行(F5)」しちゃいます。
結果、ブレークするのはココ↓↓↓↓↓。

f:id:daigo-knowlbo:20180512230343p:plain

そう、次のアクティビティ関数呼び出しの個所ではなく、オーケストレーター関数の頭のブレークに飛びます。

また、「続行(F5)」しちゃいましょう。
こんな感じになりますね↓↓↓。

f:id:daigo-knowlbo:20180512235526p:plain

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:32:31

DateTime.Nowは実行した時間そのものが取得されているのに対し、context.CurrentUtcDateTime値は「初回実行時と同じ値」が取得されています!
何度実行しても(何度再生されても)結果が同じ、つまり「決定論的動作」をしています。オーケストレーター関数の条件を満たしている!

次に「ステップオーバー(F10)」すると、先程1度実行済みの「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」が実行されるはず・・・
では「ステップオーバー(F10)」してみます。
Function2_Helloアクティビティ関数は呼び出されず、次のアクティビティ関数呼び出し箇所「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Seattle"));」に移りました。

f:id:daigo-knowlbo:20180512231219p:plain

さらに「ステップオーバー(F10)」してみます。
今度は、Function2_Helloアクティビティ関数が呼び出されました(引数nameはもちろんSeattle)。

f:id:daigo-knowlbo:20180512231409p:plain

ここで再びAzure Storage Explorerを見てみましょう。
レコードが増えましたね。Function2_Helloアクティビティが2回呼ばれた記録が行われています。

f:id:daigo-knowlbo:20180512233357p:plain

また、Result列に「Hello Tokyo!」と、1つ目のアクティビティ関数の処理結果が保存されています。

f:id:daigo-knowlbo:20180512233518p:plain

ここでちょっといたずら的にTokyoをOsakaにUpdateしてしまいます。

f:id:daigo-knowlbo:20180512235935p:plain

再び「続行(F5)」しちゃいましょう。
想像通り、オーケストレーター関数のトップに帰ってきます。

f:id:daigo-knowlbo:20180512233721p:plain

F5を連打し、Durable Functionsの処理をすべて終了させます。

結果として処理の履歴はDurableFunctionsHubHistoryテーブルに以下のように出力されました。

f:id:daigo-knowlbo:20180513000117p:plain

DurableFunctionsHubInstancesテーブルを見ると、処理結果が保存されています。

f:id:daigo-knowlbo:20180513000203p:plain

ポストマンでstatusQueryGetUriをリクエストしてみると・・・

f:id:daigo-knowlbo:20180513000415p:plain

先程いたずらでAzure Storageデータを変更した「Osaka」の文字が。
つまり、ロジックをメモリ上で実行して終わりではなく、Azure Storageに処理状態を永続化していることが理解できました。

5. まとめ

長々と分かりにくい検証を行いましたが以下のことが明確に理解できたのではないかと思います。

  • Durable Functionsは、アクティビティ関数の実行履歴を細かくAzure Storageに保存している。
  • Durable Functionsは、オーケストレーター関数で定義されたアクティビティ関数を高い信頼性で実行するためにawaitのタイミングでAzure Storageに状態を保存し、自らをリプレイしてすべてのアクティビティ関数をワークフローとして実行している。

Durable Functionsの実装については以下のgithubで公開されているので、更にソースレベルで探索ができるのではないかと思います。

github.com

また、勉強会等でもkingkino@マンダム (@kingkinoko) on Twitterさんや(「🍖・ω・)「🍺 (@yu_ka1984) on Twitterさん などDurable Functionsマスターがいらっしゃるので、色々伺えるのではないかと思います。

Syncfusion SfCalendarを使う(Xamarin Forms)

1. はじめに

Xamarin Formsで、Syncfusion SfCalendarを使って以下のようなサンプル実装を行いました。

f:id:daigo-knowlbo:20180328013715p:plain:w300
f:id:daigo-knowlbo:20180328020349p:plain:w300

  • 月カレンダー形式で予定を表示する
  • 日をクリックすると、対象日のスケジュール詳細(件名)が表示される
  • スワイプもしくはボタンクリックで前後の月に移動できる
  • Prismを使ってMVVMアーキテクチャとする

2. 実装手順

2.1. VS 2017でプロジェクト作成

Visual Studio 2017でPrism Blank App(Xamarin.Forms)プロジェクトを作成。
プロジェクト名は「UseSfCalendarWithPrism」としました。

f:id:daigo-knowlbo:20180328014042p:plain

2.2. Nugetパッケージ管理でSyncfusion SfCalendarを追加

XamarinNugetパッケージ管理で「Syncfusion.Xamarin.SfCalendar」をインストール。

f:id:daigo-knowlbo:20180328014001p:plain

2.3. 実装

で、実装は以下に置きました。。。。

github.com

以下にサンプル実装のポイントを・・・

ポイント1 カレンダーの月変更イベントをViewModelにバインド

カレンダーの月変更イベントは SfCalendar.MonthChanged です。
MVVMとしているのでイベントをViewMode(MainPageViewModel)で受け取りたいです。
その為、Prismの「EventToCommandBehavior」を利用して、イベントをView→ViewModelにコマンドとして伝播させています。

[MainPage.xaml]
<ContentPage ...省略
                        xmlns:b="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" />
<SfCal:SfCalendar x:Name="calendar" 
                  ShowInlineEvents="True"
                  MinDate="{Binding MinDate}"
                  MaxDate="{Binding MaxDate}"
                  BlackoutDates="{Binding BlackoutDates}"
                  DataSource="{Binding CalendarEventCollection}"
                  SelectionMode="{Binding SelectionMode, Mode=OneWay,Converter={StaticResource SelectionModeConverter}}">
    <SfCal:SfCalendar.Behaviors>
        <b:EventToCommandBehavior EventName="MonthChanged" 
                              Command="{Binding MonthChangedCommand}"
                              EventArgsParameterPath="args.CurrentValue" />
    </SfCal:SfCalendar.Behaviors>
</SfCal:SfCalendar>
[MainPageViewModel.cs]

public class MainPageViewModel : ViewModelBase
{
  ...省略
  public ICommand MonthChangedCommand => new Command<DateTime>((currentDate) =>
  {
    this.UpdateEvents(currentDate);
  });
}

ポイント2 カレンダーにバインドするイベントコレクション作成

SfCalendarに表示するイベントは「SfCalendar.DataSourceプロパティ」に設定しますが、PrismでViewModelにバインドしているので「MainPageViewModel.CalendarEventCollectionプロパティ」にデータバインドしています。
カレンダーの月変更イベントに対して当該月+前後一週間のイベントをバインドデータに設定しています。

[MainPageViewModel.cs]
  public class MainPageViewModel : ViewModelBase
  {
    
    /// <summary>
    /// イベントを更新します。
    /// </summary>
    /// <remarks>
    /// 今月のイベントを表示するために、対象月+前後一週間のイベントをコレクションに設定します。
    /// 1ヶ月のカレンダーの前後に 前月・次月 の日付が表示されるため、前後1週間のイベントを設定します。
    /// </remarks>
    /// <param name="calendarDate"></param>
    private void UpdateEvents(DateTime calendarDate)
    {
      // バインド対象のthis.CalendarEventCollectionを直接、繰り返しAdd()するとパフォーマンスが著しく落ちるのでテンポラリにデータコレクションを用意して差し替える
      CalendarEventCollection newCalendarEventCollection = new CalendarEventCollection();

      DateTime dt = new DateTime(calendarDate.Year, calendarDate.Month, 1);
      int thisMonthLastDay = dt.AddMonths(1).AddDays(-1).Day;
      for (int i = -7; i < thisMonthLastDay+7; i++)
      { // イベントはサンプルなので適当に2日に1回散歩と仕事、毎日のランチを設定
        DateTime eventDt = dt.AddDays(i);

        if (eventDt.Day % 2 == 1)
        {
          //this.calendarEventCollection.Add(
          newCalendarEventCollection.Add(
          new CalendarInlineEvent()
          {
            Subject = $"{eventDt.Day}日 散歩",
            StartTime = eventDt.AddHours(10),
            EndTime = eventDt.AddHours(11),
            Color = Color.Green
          });
        }
        //this.calendarEventCollection.Add(
        newCalendarEventCollection.Add(
          new CalendarInlineEvent()
          {
            Subject = $"{eventDt.Day}日 ランチ",
            StartTime = eventDt.AddHours(12),
            EndTime = eventDt.AddHours(13),
            Color = Color.Orange
          });
        if(eventDt.Day % 2 == 0)
        {
          //this.calendarEventCollection.Add(
          newCalendarEventCollection.Add(
            new CalendarInlineEvent()
            {
              Subject = $"{eventDt.Day}日 仕事",
              StartTime = eventDt.AddHours(10),
              EndTime = eventDt.AddHours(19),
              Color = Color.Blue
            });
        }
      }
      this.CalendarEventCollection.Clear();
      this.CalendarEventCollection = newCalendarEventCollection;
    }
  }

つまずいた点

2018/3/28現在版 Syncfusion SfCalendar(android)に不具合がありました。
イベントを設定しているのに画面上で、対象日をクリックすると「No Appointments」と表示されてしまう不具合でした。
twitterでつぶやいたところSyncfusionの方よりリプライを頂き、即座にパッチアッセンブリを頂き不具合の修正を確認できました。
アップデートとしては2018年3月末の「2018 Vol 1 SP1」で対応されるとのことです。

SfCalendarへの要望

表示形式が「YearViewとMonthView」の2つなのですが、Weeklyとかandroidのカレンダーみたいな形式とか、色々なバリエーションがあればもっと嬉しいなぁ、と思いました。

※今日のブログ、雑ですね。。。まあ、動くソースをgithubに置いたので、「おかしいぞ!分からんぞ!」と思った方はコメント頂ければと思いますm( )m