Continuity is The Father of Success

Androidアプリとかゲームとか。毎日続けてるものについて。

Flutter for WebをBitriseで自動デプロイしてみた

はじめに

この記事はFlutter Advent Calendar 2019 (#2) 20日目の記事です。 その1 も合わせて今年もFlutterの盛り上がりを感じますね!

やってみること

昨年のアドベンドカレンダーでは、ビルドスクリプトとかCI/CDについて書いてました。 CodeMagic が登場し、かなり慌てたことを覚えています。

blog.dr1009.com

1年経ったのでなにかしら新しいことに挑戦したい……不安定なものを触りたい……破壊的な変更に巻き込まれたい……でも調べるのが大変すぎるのはつらい……ということで、Flutter for WebのCI/CDに挑戦したいと思います。 よろしくお願いします。

開発環境

Flutterは先日リリースされた 1.12.13 を利用します。 Webもベータに格上げされたので、ちょうどいい感じです。

~/workspace » flutter --version
Flutter 1.12.13+hotfix.5 • channel stable •
https://github.com/flutter/flutter.git
Framework • revision 27321ebbad (7 days ago) • 2019-12-10 18:15:01 -0800
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0

Flutter for Webの準備

まずはプロジェクトを作成していきます。 Web版のプロジェクトを作る場合は、まず下記の公式チュートリアルを確認するのが一番早く、確実です。

flutter.dev

2019年12月18日現在(このブログを書いている日です)、まだstable版では使えないのでbetaチャンネルに変更します。 stable版に格上げされたら、下のコードは不要になります。 enable-web は残るような気もするけど。

$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web

完了したら flutter devices を呼び出してみます。 筆者の環境では、ドキュメント通りChromeが表示されました。

~/workspace »  flutter devices
2 connected devices:

Chrome     • chrome     • web-javascript • Google Chrome 79.0.3945.79
Web Server • web-server • web-javascript • Flutter Tools

続いて、Githubリポジトリを作成します。 今回のプロジェクト名は flutter_web_deploy としました。

github.com

すでに flutter run -d chrome するとChromeで動作を確認できます。 また flutter build web することで /build/web/ ディレクトリに index.html などが作成され、公開する準備が整っています

ここで終わってしまうとあまりにもあっさりしてるので、次はBitriseと組み合わせてみましょう。

Bitrise上でFlutter for Webをビルドする

お馴染み、BitriseにログインしてGithubリポジトリと連携していきます。

www.bitrise.io

f:id:D_R_1009:20191217235056p:plain

そのあとは先ほどのステップをそのままBitrise上で再現してみると。

f:id:D_R_1009:20191218011347p:plain
ビルド成功

見事、Bitrise上でFlutter for Webのビルドを実現できました。 プロジェクトをPublicにしてあるので、興味のある方は見てみてください。

app.bitrise.io

bitrise.yml は以下の通りです。

---
format_version: '8'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: flutter
trigger_map: []
workflows:
  deploy_web:
    steps:
    - activate-ssh-key@4.0.5:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4.0.17: {}
    - flutter-installer@0.11.0:
        inputs:
        - version: beta
        - is_update: 'false'
    - script@1.1.5:
        title: flutter config
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            flutter config --enable-web
    - script@1.1.5:
        title: Build flutter for web
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            flutter build web
    - create-zip@0.9.0:
        inputs:
        - source_path: build/web
        - destination: "$BITRISE_DEPLOY_DIR"
    - deploy-to-bitrise-io@1.9.4: {}
app:
  envs:
  - opts:
      is_expand: false
    BITRISE_FLUTTER_PROJECT_LOCATION: "."
  - opts:
      is_expand: false
    BITRISE_PROJECT_PATH: ios/Runner.xcworkspace
  - opts:
      is_expand: false
    BITRISE_SCHEME: Runner
  - opts:
      is_expand: false
    BITRISE_EXPORT_METHOD: app-store

これでGithubのmasterブランチへのプッシュや、タグの作成に応じてFlutter for Webのビルドができるようになりました! Android/iOS版のCI/CDと組み合わせて実行できます。(ただ、実行時間的に課金プランになりますが)

12月25日にmonoさんが「FlutterでモバイルとWeb両対応のアプリを作るノウハウ」を書いてくださる予定です。 ソースの共通化をしたら、CI/CDも共通化するニーズが高まる気がするので、今後はこの手の構成が増えるのではないかなーと思っています。

デプロイも自動化したい

おわろうかと思ったのですが続きます。

上記のステップでは「zipファイルをダウンロードし、温かみのある手動アップロード」作業が必要になっています。 令和元年もそろそろ終わってしまう昨今、もう一歩自動化を進めたいところですね?

ということで、Firebase Hostingを利用した自動化に進みましょう。

firebase.google.com

Firebase Hostingは、ご存知Firebaseが提供するWebホストプロバイダーです。


Introducing Firebase Hosting

ドキュメントはこちら。

firebase.google.com

今回はCI/CD用の設定をする必要があるので、最初に手元の環境でトークンを取得しておきます。 手順は次のドキュメントを確認してください。

firebase.google.com

こちらでゲットしたトークンを、BitriseのSercretsに保存します。 (筆者は FIREBASE_TOKEN として追加しました。後ほど変数名として利用します。)

続いて、ローカルのプロジェクトでFirebase Hostingの設定を行います。 以下、コマンド処理のイメージです。

$ cd workspace/firebase_web_deploy/
$ firebase login
$ firebase init

上記のステップを踏むと firebase.json ファイルと public ディレクトリが、 firebase_web_deploy の下に生成されます。

public ディレクトリに追加したファイルが firebase deploy することでアップロードされる、という理解でほぼ大丈夫です。もしもいろいろと設定したい場合には、コマンドの途中で変更したりjsonファイルをいじってみてください。

さて、Firebaseのプロジェクトを新規作成か既存への追加を行ったことかと思います。 このときに指定した Project ID を、トークンと同じようにBItriseの環境変数に定義しましょう。 (筆者は FIREBSSE_PROJECT_ID として定義しました。)

あとは public ディレクトリの中身をFlutter for Webの成果物で置き換え、アップロードするだけです。 先ほどのzip処理の次に、簡単なスクリプトを追加します。

#!/bin/bash
curl -sL firebase.tools | bash

cp -rf build/web/* public/

firebase deploy --project ${FIREBASE_PROJECT_ID} --token ${FIREBASE_TOKEN}

ステップをワクワクしながら走らせ、完了後にHosting先を見てみると……お馴染みのアプリが動いています!

flutter-web-deploy-demo.firebaseapp.com

f:id:D_R_1009:20191218025138p:plain
デプロイ完了!

おめでとうございます!アップロード処理まで自動化できました。 Webにはストア審査がないので、これで1日に二桁回数のリリースもできちゃいますね!

ステップを一つ追加しただけですが、定義したymlファイルは下のようになります。

workflows:
  deploy_web_firebase:
    steps:
    - activate-ssh-key@4.0.5:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4.0.17: {}
    - flutter-installer@0.11.0:
        inputs:
        - version: beta
        - is_update: 'false'
    - script@1.1.5:
        title: flutter config
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            flutter config --enable-web
    - script@1.1.5:
        title: Build flutter for web
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            flutter build web
    - create-zip@0.9.0:
        inputs:
        - source_path: build/web
        - destination: "$BITRISE_DEPLOY_DIR"
    - deploy-to-bitrise-io@1.9.4: {}
    - script@1.1.5:
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            curl -sL firebase.tools | bash

            cp -rf build/web/* public/

            firebase deploy --project ${FIREBASE_PROJECT_ID} --token ${FIREBASE_TOKEN}
        title: Firebase Hosting

Firebase Hostingをする場合でも、後ほどなんらかの事情でWebアプリのロールバックする可能性もあるため、zipファイルをダウンロード可能な状態にして保存しておくのが良さそうな気もします。 この辺りは、Webアプリエンジニアの方の知見をいただきたいところですね。

おまけ

Github Actionsも試してみたところ、さっくりデプロイできました。

f:id:D_R_1009:20191219001846p:plain

Create main.yml · koji-1009/flutter_web_deploy@d0dfd12 · GitHub

利用したワークフローは下の通りです。 これで意図通りに動くのがすごい。

name: CI

on: 
  push:
    branches: 
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Install flutter (beta channel)
      uses: subosito/flutter-action@v1
      with:
        channel: 'beta'
    - name: Enable Flutter for Web
      run: flutter config --enable-web
    - name: Build Flutter for Web
      run: flutter build web
    - name: copy resource
      run: cp -rf build/web/* public/
    - name: Firebase GitHub Action
      uses: pizzafox/firebase-action@1.0.7
      env:
        PROJECT_ID: "flutter-web-deploy-demo"
        FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
      with:
        args: deploy

FlutterとFirebase CLIのActionsがあるので、そちらを適宜利用しています。 Flutterのビルドコマンドが整理されているため、全体的にすっきりしてますね。

コードは flutter_web_deploy/main.yml at master · koji-1009/flutter_web_deploy · GitHub にあるので、よければご確認ください。 実行速度もBitriseとほぼ変わりません。WebだけならGithub Actionsだけで対応できちゃいそうです。

終わりに

BitriseとGithub Actionsを利用して、Flutter for WebのFirebase Hostingを利用したデプロイを試してみました。 ささっと環境構築して、ささっと配信環境を作って、ささっとアプリ開発していけるようになり嬉しい限りです。

ぱっと考えてみるとCircleCIでも同じことができるので、手の空いたときにymlファイルを書いてみたいなと思います。 2年連続でCIについて書いたから、来年はCircleCIでなにか書けるといい気もする……。

それでは、Flutterを遊び倒していきましょう! 明日は akio_r さんの記事です。お楽しみに〜。

Android Architecture Components Pagingを推したくて

Kotlin愛好会で談義してきた。

love-kotlin.connpass.com

speakerdeck.com

Kotlin愛好会にはサーバーサイドエンジニアの方も多く参加される & Paging自体があまり導入例を聞かないので、Paging紹介:Kotlinな話=7:3ぐらいにしてみました。

Pagingライブラリ

developer.android.com

Pagingライブラリは、大量のデータを読み込んでリスト表示したいときに利用するライブラリです。 「大量のデータ」は多くの場合API経由で読み込むサーバー上に保存されているデータになりますが、端末内のストレージに保存したデータでも構いません。

本業ではタイムラインの読み込みから導入して、今ではいろいろな追加読み込み処理を任せるようになっています。 大変便利。

tech.studyplus.co.jp

Pagingを導入すると何が嬉しいのか

RecyclerViewへ渡すデータ量の調整ができることはもちろん嬉しいのですが、Pagingを導入することによる最大のメリットは「RecyclerViewのスクロールリスナーを用意しなくて良くなる」ことだと思っています。 Pagingライブラリを利用するということは、ほぼ PagedListPagedListAdapter を利用したコードを書くことです。

developer.android.com

developer.android.com

PagedListAdapterPagedList を受け取り、PagedList の更新通知を RecyclerView へ渡す仕組みを持っています。 そして PagedListAdapterRecyclerView のスクロールにより追加読み込みが必要になったとき、追加読み込み処理を PagedList へ伝える仕組みも持っています。 このことにより ViewModelRepositoryRecyclerView.OnScrollListener を組み合わせるコードを、個々の開発者が書く必要がなくなります。 素早くスクロールしたケース、データの終点までスクロールしたときに追加読み込みをどうループさせないか、などの細かな実装を避けられるため、思っている以上に開発コストが下がるのが感じられるのでは、といったところです。

Paging導入の障害(?)

architecture-components-samplesの実装例を見ながら、そこそこ試行錯誤しながら書く必要があるのが大変かなーと思います。

github.com

PagingWithNetworkSample を時間をかけて読み込むことをお勧めします。 (DIがされていないサンプルになるため、LocatorとViewModelの接続がわかりにくいのですが、Dagger2などが導入済みプロジェクトであればもっと簡単に記述できます)

これといった必要条件もないのですが、参照できる実装例が少ないのが導入を妨げているのかなーと思っています。 または GroupieEpoxy をすでに導入しているプロジェクトにおいて、Pagingを追加する実例が少ないのもあるのかなと思います。 こちらについては、DroidKaigiにプロポーザルを出して落選してしまったので、ブログかRejectConあたりで整理できればな、と思っています。

おわりに

取り止めなく書いてしまいましたが、今の率直な気持ちとしては「どんどんPagingを利用する人が増えて、機能開発やバグ修正(あるのか?)がされるようになれば!」という感じです。 Pagingは2.1.0リリースの後、長期間の(かつ大規模な)Kotlinを利用した開発に入っているので、もっともっと「利用したい!」という声が上がるようになればいいなー……!

なんてことを考えていたら、たるさんに誘われて技術書博2に寄稿することになりました。

内容はPagingライブラリv3.0.0(開発中)で取り組まれているKotlin化と、Kotlin化したことによりより便利になった箇所の解説になります。 しっかりと探したわけではありませんが、Pagingについてandroidx-maste-devをそこそこ読んで解説しようとしているのは、他にないのではーと思っています。

興味が惹かれるようでしたら、是非お手に取ってみてください。

個人開発が滞りがちなので小さいアプリを作った話

思い立ってFlutterで簡単なアプリを作ってみました。 はてなブックマークの状態を閲覧する(監視する)アプリです。

github.com

開発経緯

ここのところを振り返って考えてみたところ、個人でアプリを作れていませんでした。作っても小さなライブラリっぽいもの程度。 本業の中でKotlinによるAndroidアプリ開発はそこそこやっているので、KotlinではなくSwiftやFlutterで作らねば、とも。

そんなわけで11月頭に「3ヶ月間で3つ(なんでもいいから)アプリを作る」ことを目標にしました。 今回は1つ目のFlutterアプリになります。

やったこと

開発要件

この半年ほどの挫折の原因を考えてみると、大抵が「ログイン処理」でした。 Googleアカウントによるログインにしたい、けれどGoogleログインにするためにはアプリ開発者の情報を……匿名アカウントで代用……匿名アカウントからメールアドレスへの昇格が……めんどい……みたいな感じで取りやめてしまうことがしばしば。 このため「ログイン処理」が不要であることを第1の条件に、合わせて今時っぽく公開されているAPIを利用したアプリにすることを第2の条件にして開発することに。

そんな中、はてなブックマークドキュメントを眺めていたところ「はてなブックマークエントリー情報取得API」がに気づきました。

developer.hatena.ne.jp

developer.hatena.ne.jp

これなら「ログイン不要」でユーザーに任意のURLを入力してもらうことで「はてなブックマーク情報取得API」のデータを表示できます。 合わせて、ユーザーの入力したURLをローカルDBに保存するなど、ネイティブアプリらしいリッチさも多少取り込めそうでした。 そんなわけで、「URLを入力するとはてなブックマーク情報を表示する」アプリの開発を行った次第です。

機能紹介

  • URL入力によりはてなブックマークエントリー情報(lite版)を取得
    • 取得した情報内のScreenShotをキャッシュしながら読み込み
      • (ただ、どの画像をとってもno_imageのような……?)
  • 入力されたURLは端末内DBに保存
    • Android/iOS共に対応するようSQLiteによる簡易な実装に
    • アクセスした日時によるソート
    • Swipeによる削除

学び

  • AndroidのRoomでSQLiteの書き方を覚えたので割とDBはなんとかなる
  • BLoCやProviderによるロジック記述の概要を掴んだ
    • まともに作り終えていたのがWebViewCheckerだけだったので……
  • BLoCによりSteamBuilderを使ってあらかたをStatelessWidgetで組めることを実感
  • 休日とか寝る前とかに書くならやっぱりFlutter
  • BitriseでAndroidのリリースステップ書くのはやっぱり簡単

スクリプトはこちらの relase でaabを作成するところまで対応しました。

---
workflows:
  release:
    steps:
    - activate-ssh-key:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone: {}
    - flutter-installer:
        inputs:
        - is_update: 'false'
    - flutter-build:
        inputs:
        - project_location: "$BITRISE_FLUTTER_PROJECT_LOCATION"
        - android_output_type: appbundle
        - android_output_pattern: "*build/app/outputs/bundle/*/*.aab"
        - platform: android
    - sign-apk:
        inputs:
        - android_app: "$BITRISE_AAB_PATH"
    - deploy-to-bitrise-io:
        inputs:
        - deploy_path: "$BITRISE_SIGNED_AAB_PATH"

今後

機能的にAppleの審査に通るわけもなく、またAndroidでリリースしても利用者いないと思うので(ブラウザで十分……)今回の開発は「Bitrise上でリリースステップが回せる」状態になったここで完了としようと思っています。

作成したアプリアイコンはそこそこよくできたんじゃないかな、と気に入っているのでどこかで使うかもしれません。。。

f:id:D_R_1009:20191116213842p:plain
作るのに30分ぐらいかけたアプリアイコン

次はもうちょっと機能のあるものを作ってみようと思います! アプリ開発者がんばっていくぞ!