pco2699’s blog

学んだコード・技術について、保存しておく場所

flutterとFirebase Cloud Messagingを連携させて通知専用アプリをサクッとつくる

こんにちは、この記事はflutter advent calendar 2018 9日目の記事です。

TL;DR

  • 全然ネイティブアプリ作ったこと無いわしが、flutterでサクッと通知を受け取れるアプリを作った
  • クオリティや保守性はさておき通知の実装できたので実装のポイントなど
  • flutter いいよおいいよお

なぜつくることになったか

現在、私はcall.jpというサービスの立ち上げ、運営を手伝っています。 call.jpはWebページから、チューターや店員さんを呼べる呼び出しサービスです。

現在、ジーズアカデミーTOKYOなどで試験運用中です。

gsacademy.tokyo

以下の通り、通知受取がPCにしかありませんでした。

f:id:pco2699:20181209171558j:plain
call.jpの概要図

なので、サクッと通知を受け取れるアプリを作ることになりました。

アプリの要件

要件は以下の通りです。

  • とにかくシンプルに通知受け取れればOK
  • アプリ自体はバックグラウンドで動かす前提
  • iOS/Android両方ともサクッと対応できるとよい(チューターはiOS/Androidばらばらのため)

上記の要件を検討するにあたって、React Nativeなど検討しましたが以下からflutterがいいのでは、という話になりました。

  • flutterがGoogle製なので、同じくGoogleで現在利用しているfirebaseと相性がよい
  • 通知機能も問題なく両プラットフォームで実装できる
  • Dartもそんなにとっつきにくくなさそう(JavaとかJavascriptとかSwitftやってればできそう)
  • なんか最近、Twitter界隈でもよく見かけてイケてる感じがするw

なので、とりあえずflutterで簡単な通知アプリを実装してみることにしました。

つくったもの

名前の通り「通知を受け取るだけのアプリ」です。 ↓が実際につくったものです。

Firebase Cloud Messagingで通知を受け取る際のポイント

Firebase Cloud Messaingでflutterで通知を受け取る際のポイントは以下です。(すげぇ簡単です。)

  1. FirebaseMessagingのライブラリのインポート&アプリでFirebaseの設定
  2. アプリのログイン時、起動時など任意のタイミングで通知許可の設定を行う(iOSのみ)
  3. 通知受信時の挙動を定義する。
  4. トピックのサブスクライブを行う。

FirebaseMessagingのライブラリのインポート&アプリでFirebaseの設定

なにわともはれ、Firebase Messagingのライブラリのインポート&アプリでFirebaseの設定を行います。 Firebaseの設定は、Firebase公式のとこから各OSに合わせて、それに従います。 flutterの場合、プラットフォームに合わせて、各フォルダ下(ios, android)でfirebaseの設定を行います。

ライブラリのインポートはpubspec.yamlに以下を追記します。

// pubspec.yaml
dependencies:
  firebase_messaging: ^2.0.0 // ver番号は現在の状況に合わせて変えてください

アプリのログイン時、起動時など任意のタイミングで通知許可の設定を行う

ここからdartファイルに手を入れていきます。 自分が通知設定を行いたい画面のStateのinitState()に通知許可の設定を入れます。

自分は、立ち上げ時に通知許可の設定を出したかったので、HomeScreenに入れてます。

// home_screen.dart
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

// HomeScreenの宣言
class HomeScreen extends StatefulWidget {
  @override
  State createState() => new HomeScreenState();
}

// HomeScreenのStateの宣言
class HomeScreenState extends State<HomeScreen> {
  final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging();
 @override
  void initState(){
    super.initState();
    // ここで通知許可の設定を行う
    _firebaseMessaging.requestNotificationPermissions(
        const IosNotificationSettings(sound: true, badge: true, alert: true));
    _firebaseMessaging.onIosSettingsRegistered
        .listen((IosNotificationSettings settings) {
      print("Settings registered: $settings");
    });
 }

通知受信時の挙動を定義する

同様にinitState()の中で、通知受信時の挙動を定義します。 私の場合は、ダイアログが出ればよいので、buildDialog()という関数を定義して ダイアログを出すようにしています。

// home_screen.dart
  // ダイアログを出す関数の設定
  void _buildDialog(BuildContext context, String message) {
    showDialog(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return new AlertDialog(
            content: new Text("$message"),
            actions: <Widget>[
              new FlatButton(
                child: const Text('CLOSE'),
                onPressed: () {
                  Navigator.pop(context, false);
                },
              ),
            ],
          );
        }
    );
 @override
  void initState(){
    super.initState();
    // ここで通知許可の設定を行う
    _firebaseMessaging.requestNotificationPermissions(
        const IosNotificationSettings(sound: true, badge: true, alert: true));
    _firebaseMessaging.onIosSettingsRegistered
        .listen((IosNotificationSettings settings) {
      print("Settings registered: $settings");
    });
    // ここで通知受信時の挙動を設定しています。
    _firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        print("onMessage: $message");
        _buildDialog(context, "onMessage");
      },
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
        _buildDialog(context, "onLaunch");
      },
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
        _buildDialog(context, "onResume");
      },
    );
 }

ちなみに、onMessage, onLaunch, onResumeってなんぞやと思っていると思いますので調べてみました。 flutterのfirebase_messagingライブラリのREADMEにばっちし載っています。

pub.dartlang.org

以下私の日本語訳

アプリ - フォアグラウンド アプリ - バックグラウンド アプリ - 停止中
Notification on Android onMessage システムトレイに通知が格納される、もし通知をおしてアプリを開くとonResumeが呼ばれる(click_action: FLUTTER_NOTIFICATION_CLICKの設定が必要) システムトレイに通知が格納される、もし通知をおしてアプリを開くとonLaunchが呼ばれる(click_action: FLUTTER_NOTIFICATION_CLICKの設定が必要)
Notification on iOS onMessage システムトレイに通知が格納される、もし通知をおしてアプリを開くとonResumeが呼ばれる システムトレイに通知が格納される、もし通知をおしてアプリを開くとonLaunchが呼ばれる
Data Message on Android onMessage バックグラウンドにいる間はonMessageが呼ばれる プラグインではサポートされていない。メッセージはロストする。
Data Message on iOS onMessage メッセージはFCMによって保存されフォアグラウンドに戻ったときにonMessageが呼ばれる メッセージはFCMによって保存されフォアグラウンドに戻ったときにonMessageが呼ばれる

まさかのDataMessage on Androidでアプリが停止中だとメッセージがロストする、という。。。(自分も今、初めて気づいた...)

トピックをサブスクライブ

最後にトピックをサブスクライブします。 今回は、ボタンを押してサブスクライブしています。

  void _onChanged1() {
    _firebaseMessaging.subscribeToTopic("/topics/gsacademy");
    _buildDialog(context, '通知の受信を開始します');
  }

ちなみに、トピック以外にも端末者IDや通知トークンでも通知は受信可能です。

さいごに

flutterで動かしてますが、今の所、機能がシンプルなゆえ、基本問題なく、動いています。いやあflutter便利。

Firebase Cloud Messaging周りについては、以下のfirebase user group user meetupの資料にて詳しく扱っていますのでよろしければ参考にしてみてください。

speakerdeck.com

Go言語で今日傘が必要か教えてくれる傘APIをつくってみた ~Mockテストもしっかりやるよ~

こんにちは、この記事はgo advent calendar 2018 9日目の記事です

Go言語初心者といっても過言ではなく他のGoアドベントカレンダーの方々の記事を見てると。とても恐れ多いですが 初心者なりにアウトプットしたいと思っています!!

ちなみに、↓のネタもあったんですが、全く受けなかったのでやめておきましたw

モチベーション

  • Go言語でちょっとしたCLIツール(tailコマンドコピーみたいなやつ)は作ったことあるけど、Web系のAPI作ったこと無い
  • せっかくだから、なんか欲しいものつくりたい
  • 最近、手ぶらで出かけるのにハマってるんだけど、雨によく打たれて困るから、朝に傘もっていくか知りたい -> それ作ろう

というわけで今回のネタに決まりましたー!!!

APIの方針

  • GoのAPIフレームワーク(Goaとか)はいろいろあるらしいけど、超簡単なAPIなのでとりあえず標準パッケージのみでつくる
  • API自体の設計は超シンプルにする
  • せっかくなので、モックかましAPIのテストも書いてみる

せっかくなので、GraphQLとかgRPC とかでやってみる説もあったんですが
今後LINE BOTとかに組み込みやすくしたいので
ノウハウが溜まっているJSON/REST APIにしておきます。(ほんとは時間なかっただけ

APIをつくっていくよ編

API自体は非常にシンプルでして、ほぼほぼOpenWeatherMap APIのラップに近いです。 /v1を投げると以下のようなシンプルなjsonが返ってきます。

{
    "is_umbrella_required": false, // 傘いるかフラグ
    "rain_volume": 0 // 向こう24時間での予測降水量
}

OpenWeatherMap APIにリクエストを投げる

天気を取得するAPIとしてOpenWeatherMapAPIを使います。
このAPIは天気関連のプロダクトを作る際には非常によく使われているAPIなので
基本的な登録の仕方やAPIの内容詳細については、説明を割愛します。

今回はこちらの「5 day / 3 hour forecast」を使います。 f:id:pco2699:20181209122117p:plain

構造体でjsonマッピングを設定

まずはAPIの仕様書に基づいて Goで構造体を定義します。

type WeatherData struct {
    Cod string `json:"cod"`
    Message float64 `json:"message"`
    Count int `json:"count"`
    List []WeatherList `json:"list"`
}

// 以下、WeatherListの内容など...(長いので省略)

これって全部マッピングする必要無いよな...!?とか思ったんですが
よくわからなかったんで、とりあえず全部jsonを構造体にマッピングしました。

たぶん、必要なデータだけjson:"hogehoge"的な感じでやればマッピングされると思います。

ApiClient interfaceの定義

今回はMockテストをしたいので、interfaceでAPIコールを行う機能を定義します。
以下の通りです。

package WeatherApiClient

import "umbrellaApi/WeatherData"

type ApiClient interface {
    Fetch() WeatherData.WeatherData
}

ちなみに、自分は普段JavaでSpringを書いているのですが
このコードを書いたことでデフォでinterfaceに依存させているのか よく理解できました。

ApiClientの実装を行う

上記のinterfaceの実装を行っていきます。 まず構造体で接続情報を定義していきます。

type WeatherApiClient struct {
    RequestUrl string //リクエスト先のURL
    Place      string //OpenWeatherMapのリクエストパラメータ①
    Format     string //リクエストパラメータ②
    Count      string //③
    Lang       string //④
}

コンストラクタを定義します。 ここで具体的な接続情報を入れていきます。

// WeatherApiClient.go
func NewWeatherApiClient() ApiClient {
    return &WeatherApiClient{
        "https://api.openweathermap.org/data/2.5/forecast",
        "Tokyo,jp",
        "json",
        "8", // 3時間毎×8 -> 24時間後までの予測を取得する
        "ja",
    }
}

Fetchの処理の中身を書いていきます。 ここで、OpenWeatherMapAPIをコールしてjsonをFetchした上で、WeatherDataの構造体につめて返してます。

// WeatherApiClient.go
func (w *WeatherApiClient) Fetch() WeatherData.WeatherData {
    values := url.Values{}
    apiKey := os.Getenv("API_KEY")
    values.Add("APPID", apiKey)
    values.Add("q", w.Place)
    values.Add("mode", w.Format)
    values.Add("cnt", w.Count)
    values.Add("lang", w.Lang)
    res, err := http.Get(w.RequestUrl + "?" + values.Encode())
    if err != nil {
        log.Fatal("Failed to get request url")
    }

    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    var wd WeatherData.WeatherData
    err = json.Unmarshal(body, &wd)
    if err != nil {
        log.Fatal("Failed to fetched unmarshal json")
    }

    return wd
}

APIサーバーを立てる

OpenWeatherMap APIにコールする部分は完成したので
今度は、APIサーバー部を書きます。

Serverの定義

テストしやすいようにAPIサーバーの処理は
構造体で定義しておき、その中にinterfaceであるAPIClientを入れておきます。

APIサーバーのルーティングはシンプルなのでhttp.handleFuncで定義します。

handleApiでは、OpenWeatherMap APIで取得した情報をもとに
今後、24時間の予測降水量を積算して、少しでも今後雨が降るようだったら
傘を持っていくフラグをONにしています。 それをmapに詰めてjson形式にして返しています。

// Server.go
func (s *Server) Start() {
    http.HandleFunc("/v1", s.handleApi)
    log.Fatal(http.ListenAndServe(":3000", nil))
}

func (s *Server) handleApi(w http.ResponseWriter, r *http.Request) {
    wd := s.ApiClient.Fetch()
    rainVolume := 0.0
    for _, values := range wd.List {
        rainVolume += values.Rain.Volume
    }

    isUmbrellaRequired := false
    if rainVolume > 0 {
        isUmbrellaRequired = true
    }
    if err := json.NewEncoder(w).Encode(map[string]interface{}{"isUmbrellaRequired": isUmbrellaRequired, "rainVolume": rainVolume}); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

mainを書く

あとは、mainで書いたServerを呼び出せば OKです。 .envファイルを読み込めるようにjoho/godotenv/autoloadを読み込んでます。 これでルートディレクトリにある.envを勝手に読み込んでくれます。(便利)

// main.go
import (
    "umbrellaApi/Server"
    "umbrellaApi/WeatherApiClient"

    _ "github.com/joho/godotenv/autoload"
)

func main() {
    wac := WeatherApiClient.NewWeatherApiClient()
    server := &Server.Server{
        wac,
    }
    server.Start()
}

Mockかましてテストするよ編

さて、すごい簡単なAPIにもかかわらずinterfaceなどが登場して、いろいろめんどくさい感じになってますが あれもこれも、テストをしやすくするための工夫です。

それでは、以下の重要な部分について、それぞれテストを書いていきましょう。

  • OpenWeatherMap APIにリクエスト投げる部分
  • APIの傘持っていく判定を行う部分

今回のコードですと、それぞれ依存することなくテストを書くことができます。

OpenWeatherMap APIにリクエスト投げる部分

WeatherMapApiClientに対して、テストを書いていきます。
今回は、利用する部分のチェックしか行ってません。

// WeatherMapApiClient_test.go
package WeatherApiClient

import (
    "testing"
)

func TestWeatherApiClient_Fetch(t *testing.T) {
    wac := NewWeatherApiClient("{API_KEYの情報を入れる}")
    wd := wac.Fetch()

    if wd.Cod.String() != "200" {
        t.Errorf("got: %v\nwant: %v", wd.Cod, "200")
    }
    for _, values := range wd.List {
        if &values.Rain.Volume == nil {
            t.Errorf("got: %v\nwant: %v", values.Rain.Volume, "Number")
        }
    }
}

APIの傘を持っていく判定を行う部分

まずは、mockのApiClientを差し込むことで、毎回OpenWeatherMap APIにアクセスせずとも
テストを行えるようにします。

type testApiClient struct {}

func (t testApiClient) Fetch() WeatherData.WeatherData {
    wd := WeatherData.WeatherData{
        // テストデータを詰める
    }
    return wd
}

こいつをServerに押し込んでやることで、Serverでテストが行えるようになります。
Serverのテストはhttptest.NewServerで行います。

func TestServer(t *testing.T) {
    server := &Server{
        testApiClient{},
    }
    ts := httptest.NewServer(http.HandlerFunc(server.handleApi))
    defer ts.Close()

    res, err := http.Get(ts.URL)
    if err != nil {
        t.Fatalf("%v", err)
    }

    data, err := ioutil.ReadAll(res.Body)
    if err != nil {
        t.Fatalf("%v", err)
    }
    var apiData ApiData
    err = json.Unmarshal(data, &apiData)
    if err != nil {
        t.Fatalf("%v", err)
    }
    if apiData.IsUmbrellaRequired != true {
        t.Errorf("got: %v\nwant: %v", apiData.IsUmbrellaRequired, true)
    }

    if apiData.RainVolume != 0 {
        t.Errorf("got: %v\nwant: %v", apiData.RainVolume, true)
    }

}

今回のコード

今回はコードは以下からご覧いただけます。
指摘やコメントなど大歓迎です m( )m

github.com

Kyash/LINE PAYで結婚式二次会をキャッシュレス対応させてみた話

結婚式のネタを擦りまくってますが笑
結婚式二次会で事前集金を使ってみたところ、結構、面白かったので、まとめておきます。 (珍しく技術ネタじゃなくて決済系ポエムです。)

事前集金をしようと思ったきっかけ

以前、IoT BBQという企画をやったんですが
集まったのがエンジニアということもあって LINE PAY/Kyashで事前支払い のみ!にしたんですね。 gsacademy-kai.connpass.com

これがまぁ、よかった!
具体的には、こういうメリットがありました。

  • 事前集金なので、材料とか飲み物代をわざわざ幹事が立て替える必要ない
  • おつりを準備したりする必要もない
  • 参加者側も、手ぶらで来れる

なんで、結婚式の二次会も、使ったほうが絶対いいよね、となり 奥さんと相談した後に、実際に事前集金することにしました。

事前集金の方法

今回は、前回のBBQと違って、ITに疎い人もいると考えられるので 以下の方式にしました。

  • メールで一斉に事前集金を案内
  • Kyash/LINE PAY両方で選べる
  • 決済が難しい場合は、当日現金払いも可能
  • 決済金額は7000円

やってみた結果

まずは結果として、それぞれのサービスで何人が払ったのか数字を纏めておきます!

  • LINE PAY (58人/92人)
  • Kyash (26人/92人)
  • 現金 (54人/92人)

現金 > Kyash > LINE PAY という結果になりました。 要因分析は下に書いていきます。

なぜこのような結果となったのか

考察として、以下の2つの切り口があると思っています。

  • なぜ現金派が多かったのか
  • LINE PAYよりKyashでの支払いが多かったのか

なぜ現金派が多かったのか

理由はわかりやすいと思いますが「それぞれの決済アプリ導入にまだハードルがある」からだと思います。

前回のIoT BBQは、IoT BBQということもあり、アーリーアダプター派のエンジニアが集っていたため すべてKyashとLINE PAYで集金することができました。

しかし、今回は、全然、アーリーアダプターではない方が半数以上、占めていたためこのような結果になりました。

実際にあった声としてはこんなものがありました。

  • Kyashを導入しようと思ったが、そもそもOSのバージョンが古すぎてアプリが入らなかった

  • LINE PAYを使いたいが、自分の口座が住信SBIネット銀行のため、お金が入れられない。

  • 単純にめんどくさい

ただ、現金の方がいい、とか現金で払うべき、という声はほぼなく、皆さん仕方がなく、めんどくさくて、という方が多かったイメージです。

なぜLINE PAYよりKyashでの支払いが多かったのか

これは単純に、参加者というより、事前支払いの広報の仕方のせいです。すみませんw

LINE PAYだと、「メールで一斉に通知」というのができないので ひとりづつぽちぽち、送金依頼をする必要があります。 これが結構つらいし、いきなり友達に送金依頼すると「なんなのあれ?」という返答が多く こちらとしても送金依頼しづらかったです。

一方、Kyashは送金リンクという送金できるリンクがあり、こちらが異常に便利です。
↓のような感じで、送金リンクをメールに貼っ付ければ、はいおわり。

f:id:pco2699:20181117162109p:plain

現状だと、大人数の集金がKyashが向いている と言えそうです。

まとめ

  • 結婚式の二次会でKyash/LINE Payで事前支払いをやってみたところ、「およそ4割の参加者」が事前支払いしてくれた。

  • 現金派の人は、むしろキャッシュレスで払いたいが、環境要因で支払えないことがけっこうあった。

  • 現状、大人数の集金はKyashが向いている

Fintech業界に身を置く、私としてもやはりKyashや LINE PAY はどんどん広がっていってほしいと思ってるので
がんがん「支払いKyash or LINE PAYでいいっすか?」というキャッシュレステロをどんどん敢行していきたい所存でございます。

flutter meetup tokyo #5 に参加してきた

flutter meetup tokyo #5に「ブログ参加枠」で参加してきました!
勉強会をブログにまとめるの初めてなので、拙いところがあってもご容赦ください...

LT① flutterで個人リリースしてみた / shogo.yamada

speakerdeck.com

内容ざっくりまとめ

  • Group Albumというアプリをflutterでリリースしました!
    • こういった勉強会で写真を共有できるようなアプリがほしかったので、flutterでつくった
  • よかったこと
    • とりあえずマテリアルデザインなので、デザイナー不要
    • 開発スピードが早かった!(3ヶ月ぐらいでリリースできた)
  • つらかったこと
    1. flutterだけでは実装できなかった処理があった、結構 SwiftとKotlinも書いた!
      • 画像の一覧表示などの画像表示周り
    2. 基本的に英語の記事しかない

感想

LT② Redux in action

内容ざっくりまとめ

  • flutterはプロトタイピングで使われるけど、要求は代わるので、スコープはしばしば大きくなるしコードのメンテナビリティも下がっちまうよねー。
  • シンプルなカウンターアプリでExample
  • incrementCounterという処理を複数のページで共有するには?
    • MyHomePage -> MySecondPageで渡すという形であれば問題なさそう、ただそれが並列になると?
  • flutter_reduxがよい github.com

  • 実際の実装の説明をコードベースでしてました

感想

LT③ flutter engineことはじめ

scrapbox.io

内容ざっくりまとめ

  • flutter engineとは?
    • flutterはDartから各ネイティブアプリへのつなぎ込みを行っている!
    • Skiaがコアエンジン
    • Skia自体はC++で書かれているので、C++が書ける人はぜひコミットを!
  • Embedder
    • 各ネイティブのハードウェアAPIのラップを行っている?
  • flutterにおけるスレッド管理
  • 各タスクランナーの処理の内容
  • flutter engineへのコントリビューション方法 -> ただ、テストでフェイルしちゃう...

LT④ 画像取得とpermission

speakerdeck.com

内容ざっくりまとめ

  • flutterで画像を取得する機能を作りたい
  • それ自体は、flutterのimage_pickerプラグイン自体でできるよねー
  • ただ、 iOSだとPermissionを考慮する必要がある。
  • Permission自体を確認するOSSはすでにある。

github.com

LT⑤ flutterのライフサイクル

speakerdeck.com

内容のざっくりまとめ

  • flutterのライフサイクル
  • iOSとかAndroidにもあるライフサイクルをまとめてみたよ
  • ざっくりライフサイクル
    • resume
    • suspending
  • AndroidiOSとflutterでライフサイクルの比較 (スライド参照)

LT⑥ flutterを広めるために技術同人誌を出した話

speakerdeck.com

内容

  • flutterめっちゃいいから、社内で広めたいよね
  • でも、社内で広めるには目標ほしいけど、プロダクションは遠いよね...
  • じゃあ技術同人誌を作ってしまおう
  • 結果完売!(パチパチ)
  • ここがよかった
    • 深い理解
    • 絶対的な締切
  • ここが課題
    • 締切をすぎると燃え尽きる...
    • 良くも悪くも限られた範囲

LT⑦ Flutter + Flutter + Bitrise + DeployGateでAndroid/iOS用テストアプリを配信してみた話

speakerdeck.com

ざっくり内容まとめ

  • スタディプラスでWebView使うようなアプリをさっくり作るサーバサイドで業務用件がでてきた -> それならflutterでやっちゃおう!!
  • flutterでiOSアプリでやるんだったらBitriseおすすめ!
  • Bitriseで無料のiOSのビルドできるよ
  • GithubでPushしたらBitriseでビルド、DeployGateで捗る!!

感想

LT⑧ FlutterでWebviewをいい感じにする

speakerdeck.com

ざっくり内容まとめ

  • flutter web viewで flutter_webview_pluginが一番ひっかかる
  • ただもうちょっといい感じにしたい -> もどれるようにしたいとか
  • やったこと
    • 戻れるように
    • 読み込めるように
  • 苦労点
    • レイアウトを右によせるだけで苦労した...

github.com

感想

LT⑨ FlutterでiOSでin-house配信した話

ざっくり内容

  • iPadワコムタブレットとBLE通信するアプリをflutterで作った
  • タブレットの通信部はSDKを使わず自分でBluetoothのパケットを解析して自作
  • FlutterBlueというBLEライブラリがある(驚き)
  • バグが多いがユーザコードで回避できる
  • async/awaitが書けるのはSwiftより全然らく!
  • PDFを開くライブラリもなかったので自作した(驚愕)
  • せっかくなのでflutterのplugin化した
  • よいところ
    • ListView.builder
    • async/await
    • Hot Reload
  • 悲しいところ
    • いっぱい

感想

Vue.js + Firebaseだけでアプリを作るときにクライアント側にFirebase API Keyをおいてしまって問題ないのか?

背景

Vue.js + Firebase だけWebサービス作れるやーん!っていう言説が非常に多い気がする昨今(私も同意です)
ちょっと前までは、「APIキーはサーバサイドにおいておくのがセキュリティの鉄則!」だった気がします。

で、Vue.js + Firebaseでサーバレスでアプリをつくるとなると、APIキーはクライアントに置かれることになります。
そこらへんって本当に大丈夫なの、って疑問に思ってたので調べてみました。

結論

結論から先に述べると、「問題なし」
Realtime Databaseとかの権限設定が適切に設定されている前提ならば 特に問題ない

調べたもの

やはりあるよねStackOverFlow

stackoverflow.com

内容

質問(意訳)

Firebaseのキーって見てる人全員にさらされちゃってるけど、それでいいんかい?  
このキーの目的ってなんなんだよ?公開されちゃってていいんかいー!

回答(意訳)

APIキーはただ、Firebaseプロジェクトの居場所を特定するだけのもので、知ったところで
別にリスクは無いよ。
データベースのURLみたいなもんさ...?

↓のディスカッションも見てみるとよいとのことです。
(Realtime Databaseの権限設定などのディスカッションがされてます。) stackoverflow.com

Vue.js + Vue-Material + Firebase でハッカブルな結婚式二次会フォームを爆速で実装する


こんにちは、実はこの度、結婚することになりまして、結婚式をあげる運びとなりました。(パチパチ)
せっかくなので、なにかエンジニアリング的なことを結婚式でやってみようと思いまして
とりあえず 結婚式の二次会受付フォーム を自作することにしました!
Vue.jsとFirebaseで自分でも思ったより爆速に仕上がったので、やりかたをまとめておきます。

つくったもの

f:id:pco2699:20180819192802g:plain

利用技術

  • Vue.js (2.5.2)
  • Vue Material (1.0.0 beta-10.2)
  • Firebase Cloud Firestore
  • Firebase Hosting
  • Firebase Cloud Functions(フォームの結果出力で利用、今回は説明は省略)

Typescriptも利用しようと思ったんですが
Vue.jsとTypescriptはいまのところ相性が良くなさそうで、普通のjsでやることにしました。 (今後のVerUpで良くなりそう。)

つくりかた

vue-cliの導入

なにわともあれ、Vue.jsを使う場合は、vue-cliから作るのが一番、爆速です。 vue-cliはNodeやnpm、yarnなどを利用しますがそこらへんの導入の仕方は、他のページなどを参照してください。
今回、私はyarnを利用して必要なパッケージを導入していきます。

$ yarn global add vue-cli

Vue プロジェクトの作成

先程の vue-cliでVueのProjectを作成しましょう

$ vue init webpack WeddingForm

色々聞かれると思うので、答えてプロジェクトを作りましょう。
ちなみに、Project Nameは大文字×らしいので、wedding_formとかにしておきます。
ESLintとかテストとかは、適当に選んでおきます。 (自分は、全部ONにしましたが、テストとか全く書かなかったので、不要だと思いました。)
最初のパッケージインストールはvue-cli側で勝手にやってくれます。

必要なパッケージ類の導入

できたページに必要なパッケージを導入していきましょう

# firebase, vue-materialなどの導入
$ cd WeddingForm
$ yarn add firebase vue-material
# バリデーション用パッケージ類の導入
$ yarn add vuelidate
# かな自動入力の際に使うパッケージの導入
$ yarn add historykana

とりあえずプロジェクトを立ち上げてみる

これだけで、とりあえずVueのサンプルプロジェクトができたので
早速立ち上げてみましょう。 かっこいいVueのロゴが現れます!

$ yarn run dev

f:id:pco2699:20180818002047p:plain

Vue-materialとかfirebaseを使えるようにする

パッケージを導入しただけだとVue-matrialとかfirebaseが使えないので セットアップしていきましょう。
基本的には[プロジェクトルート]/src/main.jsに設定は記入していきます。

# Vue-materialについて、必要な部品をインポート
import { MdButton, MdToolbar, MdField, MdRadio MdSnackbar, MdProgress } from 'vue-material/dist/components'

# firebase
import firebase from 'firebase/app'
import 'firebase/firestore'

# cssをインポート
import 'vue-material/dist/vue-material.min.css'

# importしたvue-materialの部品を読み込み
Vue.use(MdButton)
Vue.use(MdToolbar)
Vue.use(MdField)
Vue.use(MdRadio)
Vue.use(MdSnackbar)
Vue.use(MdProgress)

今回は、必要な部品のみ読み込むようにしてます。
ここで読み込むことで、全ての.vueファイルのテンプレート部でHTMLのコンポーネントとして上記の部品が使えるようになります。
ただJs上とHTML上だとMdButton -> <md-button></md-button> といった形で、記法が少々、違うので注意。

全部の部品を読み込む方法は↓です。(パフォーマンスが悪くなるのでおすすめしません。)

import Vue from 'vue'
import VueMaterial from 'vue-material'
import 'vue-material/dist/vue-material.min.css'

Vue.use(VueMaterial)

上記の読み込んだMdButtonなどのコンポーネント類の使い方の詳細は↓に載ってます。
App - Vue Material

フォーム部の設定

navbarとか基本的なvue-materialの使い方は、他のページに譲るとして
今回は、一番苦労したフォーム部のを重点的に説明していきます。

基本的な使い方編

src/componentsにWForm.vueのようなvueファイルを作成して、 そこにフォームの基本的なデザインとVueで使うモデルを定義していきましょう。


HTML部(抜粋)

<template>
  <form class="Wform" @submit.prevent="validate">
    <div class="md-layout md-gutter">
      <div class="md-layout-item md-medium-size-50">
        <md-field>
          <label for="first_name"></label>
          <md-input id="first_name" name="first_name" v-model="form.firstName" @input="first_phonetic" @keyup="first_phonetic" required :disabled="sending">
          </md-input>
          <span class="md-error" v-if="!$v.form.firstName.required">必須項目です</span>
        </md-field>
      </div>
       <div class="md-layout-item md-medium-size-50">
        <md-field>
          <label for="first_name"></label>
          <md-input id="last_name" name="last_name" v-model="form.lastName" @input="last_phonetic" @keyup="last_phonetic" required :disabled="sending">
          </md-input>
          <span class="md-error" v-if="!$v.form.lastName.required">必須項目です</span>
       </md-field>
      </div>
    </div>
    <md-button class="md-raised md-primary" type="submit" :disabled="sending">送信</md-button>
    <md-progress-bar md-mode="indeterminate" v-if="sending" />
    <md-snackbar :md-active.sync="userSaved">登録ありがとうございました!</md-snackbar>
  </form>
</template>

以下、コードの詳細です。

横並びレイアウト

<div class="md-layout md-gutter">

今回は姓と名で横並びのレイアウトにしたいので、md-layoutというclassを付与します。
ここらへんはvue-materialのcss内に定義されてるのでBootstrapっぽい要領でレイアウトしてくれます。
md-layoutで横並びのレイアウトのコンテナ的なものを作ります。
md-gutterは横並びのレイアウトのパターンで、横並びに一定の溝(gutter)をつけてレイアウトします。

ここらへんのレイアウトもvue-materialのレイアウトを参考にしてます。

インプット部品

          <md-input id="last_name" name="last_name" v-model="form.lastName" @input="last_phonetic" @keyup="last_phonetic" required :disabled="sending">
          </md-input>

md-inputでテキストを入力する部品を定義出来ます。v-modelで後で定義するモデルと紐付けるようにしましょう。 @input@keyupで姓を入力したときに同時にふりがなを入力する関数と紐付けています。 また、送信中に、フォームの入力等を避けるため、sendingという状態を持っており、sendingがtrueのときはdisabledになるようにします。

送信部分

    <md-button class="md-raised md-primary" type="submit" :disabled="sending">送信</md-button>
    <md-progress-bar md-mode="indeterminate" v-if="sending" />
    <md-snackbar :md-active.sync="userSaved">登録ありがとうございました!</md-snackbar>

ボタンはmd-raisedという属性をつけておくことで、画面から浮いた (raised)のようなデザインにすることができます。
md-progress-barはボタン押下後に、表示されるローディングバーなので、sending状態のみ出るようにします。
md-snackbarは、登録成功時に画面下に「登録されました」と表示されるバーです。
これは:md-actice.sync="userSaved"とつけておき、データが登録されたタイミングでuserSavedをtrueにすることで 本スナックバーが表示されるようにします。


スクリプト部(抜粋)

<script>
import { db } from '../main'

export default {
  name: 'WForm',
  data () {
     return {
       form: {
        lastName: '',
        firstName: '',
        lastNamePhonetic: '',
        firstNamePhonetic: '',
        presence: true,
        message: '',
        contact: ''
       },
       first_history: [],
       last_history: [],
       userSaved: false,
       sending: false
     }
   },
   methods: {
    validate () {
      this.$v.$touch()
      if (!this.$v.$invalid) {
        this.sending = true
        db.collection('presences').add(this.form).then(() => {
          this.userSaved = true
          this.sending = false
          this.clearForm()
        }).catch(() => {
          this.sending = false
        })
      }
    },
    clearForm () {
      this.$v.$reset()
      for (let field in this.form) {
        if (this.form.hasOwnProperty(field)) {
          this.form[field] = ''
        }
      }
    }
   }
}
</script>

以下、コードの詳細です。 フォームに紐づくモデルの設定

  data () {
     return {
       form: {
        lastName: '',
        firstName: '',
        lastNamePhonetic: '',
        firstNamePhonetic: '',
        presence: true,
        message: '',
        contact: ''
       },

フォームで用いるモデルをdataで定義していきます。
dataは関数で返す必要があるのでdata(): {return ...}といった形で記載してます。
(コンポーネントを同ページで何回も利用したときに、値がコピーされずに同じモデルに紐付いちゃうから関数で返す必要がある、という理解です。↓参照)

コンポーネントの基本 — Vue.js

そして各フォームに紐づく、モデルのデータはすべてformの子に定義しておきます。
こうすることで、firebaseの登録時にthis.formだけで、フォームのデータを送信することが出来て、後から項目が増えても、コードに手を入れる部分が減ります。

その他の機能で用いるモデルの定義

       first_history: [],
       last_history: [],
       userSaved: false,
       sending: false

その他のフォームには紐付かないものの、機能として利用するモデルはformの外に定義します。
それぞれ以下のような使い方をしています。

first_history, last_history: かな入力機能で利用(first:姓, last:名で利用、ホントはもっとスマートにできそう...)
userSaved: 情報が保存されたかどうかを示すフラグ
sending: 情報を送信中かどうかを示すフラグ

関数の定義
フォームで必要な関数を定義していきます。

    validate () {
      this.$v.$touch()
      if (!this.$v.$invalid) {
        this.sending = true
        db.collection('presences').add(this.form).then(() => {
          this.userSaved = true
          this.sending = false
          this.clearForm()
        }).catch(() => {
          this.sending = false
        })
      }
    },

validateと書いてますが、validateしてfirebaseに登録する処理を行っています。
送信開始時にsending=trueとして、値が無事に追加される or エラーとなったらsending=falseで値を元に戻します。
db.collection('presences').add(this.form)でfirestoreに値を追加します。
.thenで正常系の処理、.catchでエラー時の処理を記載します。 今回はエラー時はあまり考慮しないので、sendingフラグを元に戻すだけにしてます。(ここらへんはもっと改善したいですね。)

clearForm () {
      this.$v.$reset()
      for (let field in this.form) {
        if (this.form.hasOwnProperty(field)) {
          this.form[field] = ''
        }
      }
}

clearFormで、モデルの状態をリセットして送信後にフォームがリセットされるようにします。
(でないと連打して登録しまくる人がいそうなので...)

かな自動入力編

今回、名前を入力する際に同時にかな入力されるようにしてあります。 主に以下を参考にしつつ実装しました。

qiita.com

HTML部の定義

<md-input id="last_name" name="last_name" v-model="form.lastName" @input="last_phonetic" @keyup="last_phonetic" required :disabled="sending"></md-input>

@input, @keyup時に後述するmethodを呼び出すようにフォーム側で指定します。
前述の記事だと@inputだけつけてるんですが、かながうまく入らなかったので@keyup時にも指定しています。

モデル・スクリプト部の定義

data() {
   return {
      first_history: [],
      last_history: []
   }
}

姓・名それぞれのかなを管理するためのモデルを定義しています。

methods{
    first_phonetic () {
      const input = document.getElementById('first_name').value
      this.first_history.push(input)
      this.form.firstNamePhonetic = historykana(this.first_history)
    },
    last_phonetic() {
      // 上と同じなので省略
    }
}

前述の記事で、 関数の引数からフォームの入力値をとってきているんですが、それがうまくいかなく
document.getElementById('first_name')で無理やり値を持ってきています。
で、とってきた値を上記で定義したモデルにpushしていって、そのhisotryをhistorykanaにつっこめば、かな自動入力ができます。

バリデーション編

フォームなのでバリデーションは欠かせません。今回はvuelidateというVueのバリデーション用ライブラリを使いました。 monterail.github.io

HTML部の定義

        <md-field :class="getValidationClass('firstName')">
          <label for="first_name"></label>
          <md-input id="first_name" name="first_name" v-model="form.firstName" @input="first_phonetic" @keyup="first_phonetic" required :disabled="sending"></md-input>
          <span class="md-error" v-if="!$v.form.firstName.required">必須項目です</span>
        </md-field>

バリデーションでポイントになるのが、md-inputの下の<span>クラスです。
あとで、説明しますが、モデルの$vの下に、バリデーションの結果が入るので、そこをチェックして
falseの場合、バリデーションがエラーなので、エラーとして表示します。

モデル・スクリプト部の実装

  validations: {
    form: {
      lastName: {
        required
      },
      firstName: {
        required
      },
      firstNamePhonetic: {
        required
      },
      lastNamePhonetic: {
        required
      },
      presence: {
        required
      },
      contact: {
        email,
        required
      }
    }

まず、どのフォームのどの部品をバリデーションするかを定義します。
dataとかmethodsと並列で、validationsというモデルを定義すればOKです。

    validate () {
      this.$v.$touch()
      if (!this.$v.$invalid) {
        this.sending = true
        db.collection('presences').add(this.form).then(() => {
          this.userSaved = true
          this.sending = false
          this.clearForm()
        }).catch(() => {
          this.sending = false
        })
      }
    },

上記では、this.$v.$touch()ですべてのフォーム部品をユーザが触った状態にします。
(デフォルト値のままで、ユーザが触ってない部品があったりすると、その時点でバリデーションエラーになってしまうため)
そして、$v.$invalidでフォーム全体のバリデーションがOKかの値が入っているので、これをチェックすることでデータ送信可能か確認します。

テーマ色の設定

最後に、テーマ色が青色で超Googleっぽくて味気ないので別の色に変えておきましょう。
vue-materialのテーマ色をカスタマイズできるようにするには、まずscssを使えるようにする必要があります。

scssを使えるようにする設定
scssを使えるようにするには、webpackの設定を変更します。 build/webpack.base.conf.jsのmodule.rulesに以下を追加します。

  module: {
    rules: [
      // もともと書かれている設定は省略
      {
        test: /\.scss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'sass-loader'
       ]
    ]
  }

次にutilsで設定されているcssLoaderの設定を変更してscssをコメントアウトします。 build/utils.jsの以下の部分をコメントアウトします。 (ここに気づくのに結構時間がかかった...)

exports.cssLoaders = function (options) {
  //省略
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    // ↓を省略する!
    // scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

テーマ設定用scssファイルの作成・インポート
こうすることでようやくscssをインポートできるようになります。
では、scssを作成してインポートしましょう。

src/assets/cssというディレクトリを作ってtheme.scssというファイルを以下の内容で作成しましょう

@import "~vue-material/dist/theme/engine"; // Import the theme engine

@include md-register-theme("default", (
  primary: md-get-palette-color(pink, A200), // The primary color of your application
  accent: md-get-palette-color(blue, A200) // The accent or secondary color
));

@import "~vue-material/dist/theme/all"; // Apply the theme

md-register-themeで、primaryやaccentの色を指定してます。
今回は、結婚式らしくメインの色はピンク色、アクセントで青を設定してます。

これをVue.js側で読み込めば完了です。
src/main.jsに以下のimport文を追加します 。

import './assets/css/theme.scss'

Github

github.com

その他

ここまでは実際のものの作り方でしたが、firebase-hostingをつかって実際に本番にデプロイもしたり、functionsを使って、入力された情報をcsvで吐き出したりしています。
このfirebase-hostingが鬼楽で、超絶よかったので、また別記事に纏めたいと思います〜

grep + sedコマンドでlogから華麗に時刻を抜き出す

問い合わせ調査でせっかくなんで、grep + sedでフフフーンと華麗にログ調査をしようとしたら華麗にハマったのでメモ。

やりたかったこと

hoge 2018-08-10 07:31:31 hage

↑単純にこいつから時刻を抜き出したかった。

試したコマンド

$ echo "hoge 2018-08-10 07:31:31 hage" | sed -e 's/^.+\([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\).+$/\1/'

結果

hoge 2018-08-10 07:31:31 hage

なんでや!!!ってずっとなってました。

原因

Linuxコマンドだと{}にも\をつけてエスケープする必要があるんですよねー。
というわけで正解は↓

$ echo "hoge 2018-08-10 07:31:31 hage" | sed -e 's/^.+\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\).+$/\1/'

結果

2018-08-10 07:31:31

ちなみに

hoge (2018-08-10 07:31:31) hage

こんな感じで()をマッチングさせたい場合は、\(や\)を利用します。
(一個目のスラッシュがLinuxコマンドとしてのエスケープ、二個目のスラッシュが正規表現としてのエスケープ。ややこしや)

$ echo "hoge 2018-08-10 07:31:31 hage" | sed -e 's/^.+\(\\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\\)\).+$/\1/'

参考サイト

qiita.com