Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Flutterもくもく自習室 in スタディプラス #3 を開催しました

こんにちは。 スタディプラスではAndroidの開発を、趣味でflutterの開発をしている若宮(id:D_R_1009)です。

6月22日(土)に開催した「Flutterもくもく自習室 in スタディプラス #3」の結果についてまとめます。

connpass.com

Flutterもくもく自習室とは?

第1回のブログをご参照ください。

tech.studyplus.co.jp

運営スタッフから見た第3回の様子

今回は運営スタッフが1名増え、3名で開催しました。

参加者の様子は当日のTwitterのハッシュタグ #flutter_studyplus で見れます。

須藤(id:kurotyann)

今回は天候が悪かったにもかかわらず、前回の倍以上の参加者が参加してくれました。

参加していただいた皆様、ありがとうございました。

当日は参加者が増えたことにより、自習の内容が多岐にわたってきたように思います。

私が観測した範囲だと、下記のような自習が行われていました。

  • 趣味でFlutterのアプリ開発を試す人
  • 趣味でFlutter for Webを試す人
  • Flutterに関係するOSSリポジトリへPRを出そうとする人
  • 次のプロジェクトでFlutterを検討しており、開発に関わる疑問や相談をしている人
  • その他の言語やフレームワークを試す人

Flutterもくもく自習室と銘打っていますが、Flutterにかかわらず、自分のスキルアップのために参加者の皆さんはもくもくと学習しています。

その結果、弊社オフィスの雰囲気はとても良く、当日は私も心地よく集中して開発することができました!

回を重ねるたびに良い環境へ変わっているので、今後も月1ペースで開催していければと思います。

若宮(id:D_R_1009)

想定外の激しい雨が降ってしまい、少し外を歩きにくい雰囲気の開催となってしまいました。 そんな中、14名の方に参加いただきとても嬉しく思っています。

個人的な成果としては、前回に引き続き簡単なAndroidライブラリの開発に成功しました。

github.com

Androidアプリ開発に3時間、その後のflutter開発に1時間ほどの配分でしたが、どちらも考えていた以上の開発を行うことができました。 休日に家で進めていてもここまで捗ることはそうそう無いため、運営スタッフとしても「もくもく自習室」を活用できているなと感じます。

参加者のみなさま、ありがとうございました! また何かお気づきの点などあれば、ぜひスタッフまでお寄せください!

大石(id:k_oishi)

今回より運営スタッフとして参加しましたiOSエンジニアの大石です。 当日はあまり天気が良くなかったのですが、たくさんの参加者の方々に集まっていただき感謝いたします。

自身の進捗としましては、現在個人開発しているイベント向けアプリのFlutter移行を進めており、今回はFlutterの開発環境の構築を行いました。 最終的にはAndroid StudioとFlutter Pluginを導入して、Android StudioからiOS端末でアプリの起動まで進めることができました。 ここ数日の間に個人のMacBook ProとMac mini、職場のMacBook Proに開発環境を構築しましたので、そこそこのやる気を感じています。

次回の開催も予定していますので、お気軽にご参加ください。 今後ともよろしくお願いいたします。

次回の開催は?

次回のもくもく学習会の開催日は7月中を予定しています。 connpass上にて募集をいたしますので、よければ下記アカウントのフォローなどをお願いします。

connpass.com

それでは、次回の「Flutterもくもく自習室」でお会いしましょう!

GitHubのPull Panda連携を(さっそく)導入しました!

Androidチームの若宮(id:D_R_1009)です。 今朝方、Twitterを眺めていたら下記のツイートが目にとまりました。

「これは便利そうだ!」と感じたため社内Slackに投稿し、

f:id:D_R_1009:20190618132709p:plain

利用を開始したところ

f:id:D_R_1009:20190618132725p:plain

期待以上の便利さだったので、本ブログでも紹介したいと思います。

f:id:D_R_1009:20190618132756p:plain

Pull Pandaとは

https://pullpanda.com/

GitHubのリリースでは下記のように紹介されています。

We’re excited to share some big news: we’ve acquired Pull Panda to help teams create more efficient and effective code review workflows on GitHub.

https://github.blog/2019-06-17-github-acquires-pull-panda/

ブログでも紹介されている通り、Pull Pandaには"Pull Reminders"・"Pull Analytics"・"Pull Assigner"の3機能があります。

  • Pull Reminders : Slack通知
    • Pull Requestが開かれた、コメントされたなどの条件でSlack通知をカスタマイズできます
    • 指定の時間にPull Requestをチェックし、コードレビューやコードレビューへの対応を促すリマインダー機能も備えています
  • Pull Analytics : Pull Requestに関連する様々なデータ分析機能
    • Pull Requestの平均マージ時間や修正量、各コントリビューターのコメント量などを一覧にします
    • チームやリポジトリごとの取り組みが可視化されるため、データをベースにした組織文化の改善を行うことができます
  • Pull Assigner : 自動レビュアーアサイン機能
    • Pull Requestへのレビュアーが偏らないよう、自動的にコントリビューターをアサインします

Slack用GitHubアプリとの違い

Slackの公式サイトで紹介されている通り、SlackとGithubを連携させているチームも多いと思います。

https://get.slack.help/hc/ja/articles/232289568-GitHub-%E3%81%A8-Slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B

大きな違いとしては、下記3点があげられると感じています。

  • 通知をSlackチャンネルだけでなく個人のDMとして受け取ることができるので、Slack通知を個人やチームに合わせてカスタマイズしやすい
  • レビューのリマインダー機能があるため、「レビューをお願いします!」と個人間でやり取りする必要がなくなる
  • 「レビュー終わりました! 対応お願いします!」とレビュアーがレビュイーに連絡する必要がなくなる

どれもチーム内の個人の活動でなんとかしていたことを自動化 + ルールとして運用することができるようになります。 「ツーカーの仲」で開発しているチームより、多人数で協働しているチームをサポートするツールではないでしょうか。

運用ルール

弊Androidチームの場合

現在は下記ルールで運用を開始しています。

  • リマインダー機能(チャンネルへのリマインダー)

    • 11:00と17:00の1日2回リマインダー at #android_pr
    • レビュー待ち、レビューコメント対応待ちの2条件で通知
    • PRは作成されて2時間以上たったものを通知
    • 通知したくないPRには WIP ラベルをつける
  • リマインダー機能(個別DMへのリマインダー)

    • カスタマイズ推奨
    • 開発スタイルに合わせておまかせ
  • 自動アサイン機能(Automated review assignment)

    • Androidチームは CODEOWNER を利用しているため対応しない
    • Androidチームが増えたら対応を検討(2019年6月現在3名)

設定について

スタディプラスでは個々人の働き方に合わせた出勤時間となるため、可能な限り緩めのルールで運用したいと考えています。 そのため、リマインド設定は下記の条件としました。

  • 昨日レビューが終わっていないPull Request ⇨ 11:00にリマインド
  • 当日15時までに作成されたPull Request ⇨ 17:00にリマインド

今後

Androidチームで試験的に取り入れとなりましたが、社内の他チームでも取り入れる機運が高まっています。 社内の全チームで展開し、知見を取り入れてより便利に使っていきたい……と思っていたら、他チームの導入がどんどん進んでいました。

f:id:D_R_1009:20190618143248p:plain

気軽にPull Requestのフローを改善できるので、ぜひぜひ取り入れてみてください!

スキーマ駆動開発のススメ

こんにちは,For School事業部のid:atomiyamaです.

現在Studyplus for Schoolはサービスのフルリニューアルを行っています.
弊サービスはこれまでRailsでslimを使いViewを提供してきましたが,今後より良い体験をユーザーへ届けるためにリニューアルを行いサーバーサイドとクライアントサイドを分離しました.
リニューアルに向けて現在サーバーサイドはRailsでJSON APIサーバーの開発を行っており,その中で導入したスキーマ駆動開発の話をします.

TL;DR

  • 技術スタックはOpenAPI3.0, swaggerUI,committee
  • クライアントサイド開発者と連携してJSONスキーマを仮決定する.
  • サーバーサイドの開発者はRailsコントローラにそのJSONをべた書きした仮実装を行う.
  • クライアントサイド,サーバーサイドの開発者が互いにフィードバックしながら並行して実装を進める.

スキーマ駆動開発とは

スキーマ駆動開発とは,
「スキーマをはじめに定義し,スキーマを元にAPI開発者,利用者が同時に開発を進めていくこと」です.

スキーマ駆動開発を導入するモチベーション

今回スキーマ駆動開発を導入した背景は以下の2つの問題により思ったようなスケジュールで開発が進行してしなかったことです.

  • サーバーサイドの実装が完了するまでクライアントサイド開発者が実装に着手できない.
  • 一度実装したAPIにクライアントサイドから変更の依頼が入り手戻りが頻発する.

今回のフルリニューアルのような長期にわたるプロジェクトでは特に既に開発を終えたタスクに対してな手戻りが頻発することは当初のスケジュール通りにプロジェクトを進めていく上で大きな問題になります.
またクライアントサイドにおいてはAPIで提供されるデータの仕様が不明確なままでは開発を進めることができませんからサーバーサイドの進捗から強く影響を受けてしまいます.

そこでサーバーサイド,クライアントサイドが並行して独立して開発できるようにスキーマ駆動開発を導入しました.

スキーマ駆動開発

私達のチームではスキーマ駆動開発を進める上で以下のようなステップにそって開発を進行しています.

  1. APIのスキーマを利用者の合意のもと決定する
  2. APIスタブサーバを提供する
  3. 本実装を行う

APIドキュメンテーション

APIのスキーマを決定しドキュメント化するために弊社ではOpenAPI3.0を使っています. OpenAPI3.0に従ってyamlで定義を書くんですがサービスがある程度の規模になってくると一つのyamlファイルにすべてのエンドポイントの仕様を書くのは辛くなってきます😭
そこで以下の用にエンドポイントURIのパスと対応するレベルでファイルを分割し,Rakeタスクでファイルを結合してswaggerUIが読み込み可能な形に吐き出しています.

├── app
├── bin
├── config
├── db
├── dockerfiles
├── lib
├── log
├── public
├── spec
├── swagger
│   └── v1
│       ├── main.yaml
│       ├── paths.yaml
│       ├── components
│       │   └─ user.yml
│       └── paths
│           └─ users.yml
├── tmp
└── vendor
# swagger/main.yml
openapi: 3.0.0
info:
  title: SampleAPIApp
  version: 0.0.1
servers:
  - url: https://example.com/v1
paths:
  $ref: ./paths.yaml
components:
  schemas:
    $ref: ./components/schemas.yaml

# swagger/paths.yaml
/v1/users:
  $ref: ./paths/users.yaml

# swagger/path/users.yaml
get:
  summary: ユーザー一覧
  tags:
    - ユーザー
  responses:
    200:
      description: OK
      content:
        application/json:
          schema:
            type: object
            required:
              - users
            properties:
              users:
                type: array
                items:
                  type: object
                  required:
                    - name
                    - age
                    - email
                  properties:
                    name:
                      type: string
                      description: 名前
                      example: スタプラタロウ
                    age:
                      type: integer
                      description: 年齢
                      example: 20
                    email:
                      type: string
                      description: メールアドレス
                      example: sutapura.tarou@example.com

APIスタブサーバの提供

前項でスキーマが決定しswaggerUIリーダブルな形に変換できたのでswaggerUIの公式イメージを立ち上げ出力したファイルを読み込むことで下のようにAPIのスキーマが閲覧可能になりました.
f:id:atomiyama:20190612170745p:plain ここから実際にRailsの実装を行っていくのですが,いきなり本実装をはじめてしまうと完了するまでクライアント開発者は開発に着手できなくなってしまうので最低限の実装でAPIスタブを実装します.

実際にはswaggerUIのResponsesのExample valueにあるオブジェクトをコピーして,Railsのコントローラにそのまま貼り付けます.

# app/controllers/v1/users_controller.rb
class V1::UsersController < ApplicationController
  def index
    return json: {
      "users": [
        {
          "name": "スタプラタロウ",
          "age": 20,
          "email": "sutapura.tarou@example.com"
        }
      ]
    }
  end
end

テストも最低限書いておきます.テストではcommitteeを使っています.
committeeを使ってテストをすることでOpenAPI3.0に則って定義したスキーマと差分の無いレスポンスが返却されることをテストすることができます.
committeeの導入についてはこちらを確認してください.

require 'rails_helper'

RSpec.describe V1::UsersController, type: :request do
  describe "GET /v1/users" do
    subject { get "/v1/users" }

    # 最低限のテスト
    it "200" do
      subject
      expect(response).to have_http_status(200)
      assert_schema_conform
    end
  end
end

ここまでをやったら一度PullRequestを作成してデプロイしてしまいます.
これらの作業は2,3時間もあれば終わってしまう作業ですし,ここまで完了すればサーバーから仮のデータが提供されるようになっているためクライアントサイドの開発者も開発を進めることができます.

本実装

残すはクライアント,サーバーともに前項までにやった仮の実装を本実装していくだけです.
ひたすらにスキーマ通りにデータを返せるようにコーディングします.

まとめ

スキーマ駆動開発を導入することにより早い段階で決定されたAPIスキーマ(APIスタブも含む)を元に,クライアントサイド,サーバーサイド互いが自身の作業に専念できるようになりました.
個人的にいいなと思った点は,最初のスキーマを決定する際にクライアントサイド開発者のフィードバックを受けるので互いの仕様への理解をすり合わせる空気感が自然とできることです.
またほぼ同時に本実装が進むため,もしどちらかの実装の都合によりスキーマに仕様変更が入っても完成前なので大きな手戻りにはなりにくいためスケジュール遅延リスクも最小限に抑えることができます.

是非これからAPI開発に取り組む方はスキーマ駆動開発取り入れてみてはどうでしょう.

Android Architecture Componentsに感動した話

初めまして、Studyplus開発部のAndroidエンジニアの隅山です。今年5月からスタディプラスでAndroidアプリ開発を行っています。

本格的にAndroid開発を始めてまだ日が浅いので、初中級エンジニア目線で学んだことをまとめていきたいと思います。

背景

初めてAndroid開発を行った時感じたことが「Androidの画面表示関連の仕様の複雑さ」でした。

以前開発してた時もAndroidのライフサイクルや画面回転あたりで非常に苦しめられていました。しかし今回、Android Architecture Components(AAC)を学ぶ+導入することで、ライフサイクルや画面回転問題が簡単に解消されたのでAAC実装例を紹介します。

やったこと

今回、ユーザが入力する文字列に対応した検索結果を、リアルタイムでリスト表示する機能を実装しました。

実装としては画面にEditTextとListViewを用意し、EditTextの文字の変更通知に合わせてListViewのアイテムを更新するという仕組みとなります。AACを使わない場合と使う場合で大きく変わった点が「EditTextの文字変更通知」部分なのでそこに着目していきます。

AAC使わない場合

まずはAACを使わないやり方、TextWatcherで文字変更通知を受ける場合です。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val itemList = listOf("くろ", "あか", "あお", "しろ")
        val editText = findViewById<EditText>(R.id.edit_text)
        val listView = findViewById<ListView>(R.id.list_view)
        val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, itemList)
        listView.adapter = arrayAdapter

        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                // 文字変更通知を受けて、ListViewの中身更新
                val list = if (s.isNullOrEmpty()) itemList else itemList.filter { it.contains(s) }
                arrayAdapter.clear()
                arrayAdapter.addAll(list)
            }
            override fun beforeTextChanged(s: CharSequence?, s: Int, c: Int, a: Int) {}
            override fun onTextChanged(s: CharSequence?, s: Int, b: Int, c: Int) {}
        })
    }
}

自分が感じたこの実装のメリット、デメリットは下記となります。

  • メリット
    • Androidの標準APIのみで書ける
  • デメリット
    • 画面回転考慮する必要がある
    • アクティビティの役割が大きすぎる
    • モダンな実装ではない

特に、画面回転問題を考慮する場合、onSaveInstanceStateなどでデータを保持する必要があります。 また、サーバーから検索結果を取得し表示するなどの機能拡張を行うと、アクティビティクラスのコード量も役割もどんどん増えてしまいます。

AAC使う場合

つぎにAACのViewModel+LiveDataで文字変更通知を受ける場合です。

class SampleViewModel : ViewModel() {
    private val itemList = listOf("くろ", "あか", "あお", "しろ")

    // serchTextは双方向BindingによりEditTextと接続
    val searchText = MutableLiveData<String>()
    val items: LiveData<List<String>> = Transformations.map(searchText) { text ->
        // ここで文字変更通知を受け、リストに表示するアイテム作成
        if (text.isNullOrEmpty()) itemList else itemList.filter { it.contains(text) }
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel = ViewModelProviders.of(this).get(SampleViewModel::class.java)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1)

        binding.lifecycleOwner = this
        binding.viewModel = viewModel
        binding.listView.adapter = adapter

        viewModel.items.observe(this, Observer { items ->
            // リスト表示アイテムを受け、ListViewに表示
            adapter.clear()
            items?.let { adapter.addAll(it) }
        })
    }
}

自分が感じたこの実装のメリット、デメリットは下記となります。

  • メリット
    • ViewModelがViewModelProviderで管理されているため画面回転されても情報が保持される
    • SampleViewModelで文字変更通知を受け、リストに表示するアイテムを作成するため、アクティビティは流れてきたアイテムを表示する役割のみ
    • モダンな実装のため、Google公式の最新アップデートに追随しやすい
  • デメリット
    • AACに関して勉強する必要がある

文字変更通知もLiveDataを使うと簡単に受けることができ、リストに表示するアイテムを自由にViewModel側でカスタマイズすることができます。今回は簡単な文字列リストを表示するだけですが、実際は検索文字を用いてサーバアクセスしたため、SampleViewModelでKotlin Coroutinesなどを使って実装しました。

まとめ

Androidでつまづきやすい画面回転によって情報が落ちてしまう問題や、クラスの役割が膨大になる問題をAACを導入することで解決できたのは感動しました。

今回は簡単な例を示すためリストに文字列表示するだけでしたが、実際の利用シーンでは検索文字列をサーバに投げてレスポンス結果をリストでリアルタイム表示しています。この際、Kotlin Coroutinesによる非同期処理や入力待ちの遅延処理を行なっています。AACを使わない場合だとアクティビティが肥大化して運用できない形になっていたため、今後の運用面も考慮して開発することができたのはよかったと思います。

AndroidのことやAACのことは同じチームメンバーの若宮さんや中島さんに教わりつつ開発することができたため、大きくつまづくことはほとんどなく非常に助かりました。Androidの画面回転やライフサイクルなどで詰まっている方は是非Android Architecture Componentsを導入してみてください。

AndroidのCI環境を移行しました!

こんにちは、Androidチームの若宮(id:D_R_1009)です。

2019年5月よりAndroidチームのCI環境をBitriseからCircleCIに移行いたしました。 今回は移行の経緯や結果などをまとめたいと思います。

CI環境変更経緯

昨年の6月ごろからBitriseを利用していました。 それ以前はWorckerを使っていたようです。

tech.studyplus.co.jp

BitriseはGUIによって容易にステップの追加/削除が可能なため、自動化したい処理の管理が非常に簡単です。 ステップにはコミュニティ主導で作られているものもあり、機能追加やメンテナンスが活発に行われています。 弊社のアプリに関して言えば、build.gradleファイルの整理時にgradle-play-publisherをBitriseのステップに置き換えるなどの様々な試作を行うことができました。

これらのことから、Bitriseを利用することになんら不満はありませんでした。一方で弊社全体のCI環境を見ると、少し違和感のある状況になっていました。

チーム 環境
Android(Kotlin/Java) Bitrise
iOS(Swift/Obj-C) CircleCI
Server(Ruby) CircleCI

上記の通りCI環境がチームによって分断されているため、CI環境に対する投資とそのリターンの効率が悪くなっています。 また、CI環境構築や改善についての知見がそれぞれのチームに偏ってしまうため、CI構築のノウハウが育ちにくい状況になっていました。

これらの状況を踏まえ、AndroidチームもCircleCIを利用することを決めた次第です。

CI移行ステップ

Androidのビルドについて立ち止まって考えてみると、一般的なCIサービスで利用できないものは特にないことに気づきます。

そのため何らかの方針を決めてCIで自動化する処理を決める必要があると考えました。 今回の移行ではCIで実行したいことは何かを整理すること、CIでは何をシェルスクリプトで書くべき何かを整理すること、この2点を重視しています。

なおCIについて何を考えていくかについては、darumaさんのLTを参考に考えていきました。

おすすめです。

スタディプラスAndroidチームのCI方針

"Ruby"を活用していく点がポイントだと考え、fastlaneによるビルド環境構築を選択しました。

fastlane.tools

fastlaneはiOSアプリの様々な自動化においてデファクトスタンダードとなっているツールです。 Androidではあまりなじみはありませんが、様々な対応がなされています。

実際にステップ構築にあたり考えた方針は下記の通りです。

  • fastlaneで可能なことは、全てfastlaneで実行する
  • fastlaneで実行できないがGradleプラグインで実行できるものは、Gradleプラグインで実行する
  • fastlaneとGradleプラグインで実行できないことだけ、シェルで実行する

出来上がったconfigファイルを見返してみるとCrashlytics Beta用NoteにGit logを記載する箇所だけシェルによる実行となっており、この方針は満たせているかなと感じています。

BitriseからCircleCIに移行で苦労した点

CircleCI上に各リリース用のkeyを設定する以外には、特にありませんでした。 というのも、CircleCI 2.1のconfiguration機能を使うことで、CircleCIの各処理を小さくまとめて記載することができたためです。

たとえば、Dangerを走らせる場合には

commands:
  setup_bundle:
    steps:
      - restore_cache:
          key: v1-danger-cache-{{ checksum "Gemfile.lock" }}
      - run:
          name: Bundle install
          command: bundle check --path ./vendor/bundle || bundle install --jobs=4 --retry=3 --path ./vendor/bundle --clean
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-danger-cache-{{ checksum "Gemfile.lock" }}
  run_danger:
    steps:
      - run:
          name: Run danger
          command: bundle exec fastlane android check_pull_request

とcommandsを定義します。これを

jobs:
  check_pull_request:
    executor:
      name: android_defaults
    steps:
      - checkout
      - setup_android
      - setup_bundle
      - run_danger

のようにjobとすることで、Bitriseと同じようにステップとして扱うことができます。

移行を始めた当初は書き慣れないYAMLファイルやRubyの環境構築(Bundlerとは、Gemfileとはなど)に手間取りました。 しかし学習が進むについれ、手元の環境で構文チェックをしながら構築することができ、生産性が加速度的に高まっていったように感じます。

CircleCI上にAndroidリリース用のファイルを設定する方法は、下記ブログが一番実用的だと感じました。

medium.com

弊社の状況に合わせて試行錯誤した結果、ほぼ同一の対応をすることになっていました。 一度このブログの手順に合わせて構築し、各環境に合わせてカスタマイズすると良いのではないかと感じています。

移行結果

移行した結果、実行時間の削減が見られました。 以下の2ファイルはキャッシュがない状態で、ほぼ同一の処理を行っています。

CircleCI Bitrise
f:id:D_R_1009:20190524162647p:plain
CircleCI stageビルド
f:id:D_R_1009:20190524162341p:plain
Bitirise stageビルド

2019年6月現在ではdebug/stage/releaseの全てのビルドをCircleCIに移行していますが、ほぼ同じか半分程度の実行時間になっています。 Bitriseではビルド待ちの問題が発生しなかった点を差し引く必要がありますが、概ね実行時間は短縮されたと言えます。

まとめ

BitriseとCircleCI、どちらも利便性に優れたCI環境です。 特にCIで実行するステップの管理に対して、アプローチは異なりますがどちらも簡潔だなと感じました。 簡潔な処理を適切に組み合わせることとなるので、だんだんとステップを編集するコストを軽く感じるようになりました。

一部キャッシュの扱いについて違いがあるため個別のCIについての学習は発生しますが、利用しながら改善していけば十分な範疇だと思います。 なんらかの事情において移行したいケースや、別のCI環境も利用してみたいと感じるようであれば、気軽に試してみても良いのではないでしょうか。

よりよいCI環境について知見のあるエンジニアの方がいましたら、ぜひこちらからご連絡いただければと思います。 一緒に最高のDXを作り上げましょう!

弊社オフィスで「Flutter Meetup Tokyo #9」を開催しました

こんにちは、スタディプラスの須藤(id:kurotyann)です。

今回のブログでは、5月22日(水)に弊社オフィスで開催した「Flutter Meetup Tokyo #9」についてまとめます。

f:id:kurotyann:20190522194615j:plain

Flutter Meetup Tokyo とは?

Flutter Meetup TokyoはFJUGが主催している、東京で開催するFlutter勉強会です。

今回で9回目を迎え、会場だけでなく、YouTubeでのライブ配信も行っています。

当日の様子は、ライブ配信のアーカイブまたは、Twitterのハッシュタグから確認できます。

www.youtube.com

twitter.com

運営スタッフから見た Flutter Meetup Tokyo

弊社ブログからは運営に関わった社員より、当日の様子や感想をお伝えします。

なお、当日の各発表については参加者の参加ブログを見てください。

Flutter Meetup Tokyo #9に行ってきました|あぼぼ|note

須藤(id:kurotyann)

弊社では、今年の2月から新規事業でFlutterを使ってアプリを開発しています。現在のところ、Flutterでの開発は順調に進んでおり、Flutterの対応力と進化のスピードに日々、驚いています。

いつか弊社オフィスで「Flutter Meetup Tokyo」を開催し、さらに登壇もすることで、このFlutterの魅力を伝えられればと思っていました。それが今回の第9回目で実現することができ、嬉しいと同時に少しホッとしています。

というのも、私は運営と5分LTの準備に追われ、前日と当日はとてもバタバタしておりました。定員70名という規模のイベント運営は初めてだったこともあり、軽食や飲み物の準備、会場設営など想像以上にやることが多かったです。

しかし、社員のみんなやFJUGの管理者の方々のサポートで、大きなトラブルもなく開催することができました。もしまた機会があれば、弊社で開催できればと思っています。そのときには、新規事業のアプリも完成してリリースされているはずです!

参加していただいた皆様、本当にありがとうございました!

若宮(id:D_R_1009)

Flutter Meetup Tokyoをついに弊社で開催することができました! 個人的には今回のMeetupが5回目の参加となります。 当日はほとんどバーカウンターの向こう側で過ごしていたのですが、これもまたいい思い出になりそうです。

今月はGoogle I/O 19でFlutter 1.5が発表されたこともあり、真新しいLTが多く非常に興味深い会になったのではないでしょうか。 合わせて、懇親会の雰囲気なども楽しんでいただけたようであれば嬉しいなと、運営スタッフとして感じています。

業務ではKotlinによるAndroidアプリ開発が主となっていますが、社内でもFlutterが盛り上がってきているので、引き続き個人開発を進めていきたいなと思っています。 もくもく勉強会など色々な形でFlutterコミュニティをサポートできればと思っているので、引き続きよろしくお願いいたします。

Flutter Japan User Groupの皆様、そして参加してくださった皆様、本当にありがとうございました!

大石(id:k_oishi)

Flutter Meetup Tokyoへの参加は2回目ですが、今回は入社間もない自社での開催となり当日の運営スタッフとして参加しました。

初めて勉強会の運営スタッフを経験しまして、大変ではありましたがとても良い経験をすることができたと同時に、普段参加している他社さんでの勉強会の運営の方々への感謝も忘れてはいけないと感じました。

初めてということもあり、今回は様々な面で改善の余地があったと感じましたが、今後に生かしていければと思っています。

登壇および参加していただいた皆様、ありがとうございました。

おわりに

次回のFlutter Meetup Tokyo #10の詳細は未定のようです。

しかし、6月には弊社オフィスにて「Flutterもくもく自習室 #3」を開催予定です。

第一回と第二回のFlutterもくもく自習室の模様は弊社ブログに投稿しております。

tech.studyplus.co.jp

もし、興味のある方がいたら、どうぞ第三回に参加して一緒にもくもく開発しましょう!

Studyplusのとある画面でYouTubeの動画再生に対応し、やらかしたお話

こんにちは、Studyplus iOSチームの明渡(ID: m_yamada1992)です。

今回は、今年3月にリリースしたStudyplusアプリにて大学の情報を表示する画面でYouTubeの再生に対応したお話、およびiOSアプリ側の実装にて盛大にやらかした話をつづっていきます。

YouTubeをStudyplusのアプリで表示する

動画を表示することになった背景

Studyplusは現状受験生のユーザーが非常に多く、「同じ志望校を目指す仲間と励まし合いたい!」というニーズに応えるため大学にまつわる情報を取り扱う機能が手厚く揃っております。志望大学を探す機能、同じ大学を志望しているユーザーの勉強時間ランキングなど。

そうすると、「Studyplusで志望大学を探すユーザーに自校をもっとアピールしたい!」という大学さんも少なからず存在するわけです。アピールする新しい手段の1つとして、動画を掲載もできるように対応することとなりました。

実現方法

YouTubeの動画サムネイルを表示して、サムネイルがタップされたらウェブビューのみの画面を表示して動画を表示するようにしました。実装イメージが以下のとおりです。

f:id:m_yamada1992:20190513212011p:plain:w300f:id:m_yamada1992:20190513212017p:plain:w300

左がサムネイル表示画面、右が遷移先の動画再生のみを目的とするWKWebView画面

なお、公式ライブラリの利用は真っ先に検討したのですが、iOSに関してはとうの昔に非推奨のUIWebViewを挟んで動作させることが前提だったので断念しました。 AndroidはきちんとメンテされているYouTube Android Player APIを採用してます。

YouTubeの動画サムネイルを取得する

YouTubeにて動画の情報を取り扱う手段として正式に提供されているYouTube Data API (v3)のうち、Videosリソースにて動画のサムネイル画像URLが取得できるので、そちらを使用しました。

ひとくちに動画情報といっても動画の投稿者やタイトル、説明文など今回不要な情報もたくさんあるので、クエリパラメータのpartfieldsで必要なレスポンスの値を絞り込みました。

let queryItems = [
    URLQueryItem(name: "id", value: id),
    URLQueryItem(name: "part", value: "snippet"),
    URLQueryItem(
        name: "fields",
        value: "items(snippet(thumbnails(standard(url),medium(url))))"
    )
]

過去に外部のAPIを使用する場面はそこそこあったのですが、「返ってくるレスポンスのうち○○と△△しか使わない」といったコメントを残さないと分かりにくいことがありました。 このように不要な値はレスポンスに含まないよう実装で制御し、使っている値が一目瞭然にできるのはすごく画期的に思えます。

YouTube独自のオーバーレイ表示へ対応

サムネイル画像をタップしたあと表示するWKWebViewの画面なのですが、Studyplusのアプリで素直な実装にて表示してみるとちょっとした問題が発生しました。

Studyplusアプリで実際にYouTubeの動画を再生した際、以下のキャプチャのような表示が可能です。

f:id:m_yamada1992:20190513162321p:plain

問題点

  • 端末の縦方向表示のみをサポートしているのに、YouTubeの動画プレイヤーを横方向で表示できてしまう
    • 端末を横にした状態で動画プレイヤーを閉じると、もとの画面でステータスバーが消えてしまう

なお、消えた状態のまま前の画面に戻ってもステータスバーは復帰せず、通常のナビゲーションバー下辺までの高さがステータスバー分縮んだままとなります。

キャプチャからも見て取れる通り、オーバーレイ表示上でステータスバーのみ標準UIを流用していることが影響している挙動のようです。

今回選んだ解決方法

記事冒頭のキャプチャでお察しかもしれませんが、YouTubeを表示するためのWKWebView画面でのみステータスバーの表示を諦めました。画面を表示時にステータスバーは非表示へ変更、画面を閉じるときに元通り表示すると他の画面は影響しないことが確認できたためです。

YouTube的にはプラットフォームの標準仕様より、ユーザーがいかに快適に動画を視聴できるかを追求した結果の挙動なのかしらとは思います。が、なぜステータスバーだけ標準OSで表示しているものを引っこ抜いていくのだろうか・・・?

こちらの手段に落ち着くまでにそこそこの試行錯誤や調査を要して戸惑ったので、そこまで独自の挙動をするのだったら全部独自のビューを表示すればいいのにと思ってしまいます。

やらかしたお話

結論から申し上げますと、YouTubeの動画サムネイル取得失敗のパターンが無限ループする実装を施してしまいました。

起こったこと

GCPのAPIなので、クォータを食い切ります。以下は弊社開発環境でのYouTube Data APIクォータ利用状況のGoogle Cloud Consoleの様子です。

GoogleCloudコンソール上で確認した、開発環境でクォータ上限突破した際のキャプチャ

キャプチャのうち、赤い点線が1日のクォータ上限です。現実的に上限を使い切ることはまずないだろうという値を設定しております。

1回目の時点で-8時間表記であることを見落として、夜中の3時に起こったアプリの操作では起こりえないという判断をしてアプリ担当としてはスルー。2回目に再発してこれはおかしいぞと調べたら無限ループを仕込んでいたことが発覚。

リリースを予定していたバージョンを数日前に審査提出・通過完了しており、翌日のリリース準備万端! というタイミングで2回目の再発でした。 同じ環境に接続されているうち1個の端末でエラーが発生するとクォータを一気に1日の上限まで使い切っちゃいます。

同じ事象を本番環境で発生させてしまうとサムネイルの取得が全ユーザーで最大24時間できなくなります。存在する動画はサムネイルが取得できるものとして実装しているため、YouTubeの動画をアプリで表示できなくなります。致命的です。

原因

対象の画面で既存バージョンから存在する、ほとんど同じ形で単純に画像の表示を行うセルにて取得した画像サイズに応じて高さを変更していました。そしてYouTubeのサムネイルを表示するセルを新しく作成するときに流用したのですが、その際に本来不要な画像サイズに応じた高さ変更処理諸共流用して放置して開発を進めたのが直接の原因です。

ディレクターチームにリリースを予定している機能を一通り動作確認してもらったあと、iOSチーム内でのソースコードレビューにて指摘をもらって上記の処理をリファクタリングしたのが事の発端でした。

実装の詳細

まず、動画を表示するセルにて表示する内容を設定するメソッドは以下のようなイメージです。

func setup(text: String, completion: @escaping (_ loadImageRatio: CGFloat?) -> Void) {
    // ...省略...
    if let thumbnailUrlString = videoThumbnailUrl {
        // YouTubeのサムネイルのURLをすでに取得済みの場合、画像を表示
        loadImage(urlString: thumbnailUrlString)
    } else {
        // 動画サムネイルURLの取得に1回失敗していた場合、このタイミングでリトライ
        guard let videoId = youtubeId else { return }
        VideoRepository.getVideoData(id: videoId, success: { thumbnailUrl in
            /* このセル用のコンテキストインスタンスへサムネイルのURLを保持 */
            // 取得したYouTubeのサムネイルのURLから画像を取得して表示
            self.loadImage(urlString: thumbnailUrl)
        }, failure: { _ in
            /* 動画を表示できない旨を表記 */
        }, finally: {
            // 成功したときは取得した画像の縦横比率を、失敗時は失敗したとき用の画像の縦横比率を渡す
            completion(imageRatio)
        })
    }
    // ...省略...
}

YouTubeから動画のサムネイルURLをすでに取得して保持済みの場合はその画像を表示、何らかの影響でサムネイルURLを取得できずに動画が表示できていなかった場合はリトライしてみるという実装です。

なお、この画面で表示する動画IDは弊社側で大学から頂戴したデータを管理するので、まず存在しないものはないはずという前提です。一瞬ネットワークの調子が悪くて取得失敗したなら、スクロールして戻ってきた際にリトライしたほうが親切だろうと。

問題が発生した動画を表示するセル生成処理

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...省略...
    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath)
    cell.setup(text: text, completion: { imageRatio in
        /* このタイミングで取得完了した画像の縦横比率をこのセル用のコンテキストインスタンスへ保持し、以降画面へセルを再表示しても同じ高さで表示 */
        tableView.reloadRows(at: [indexPath], with: .none)
    })
    // ...省略...
}

上記のうち reloadRows(at:with:) メソッドを呼んでいる箇所で、リファクタリング前にperformBatchUpdates(_:completion:)メソッドを引数nilで呼ぶという一見なぜこの処理をこのタイミングで呼ぶか分かりにくい状態でした。ですので、そもそも reloadRows(at:with:) メソッドでも同じ挙動が期待できる()から、こっちのほうがシンプルでいいよね! という結論にいたり修正してこの実装に落ち着きました。

※厳密にはreloadRows(at:with:) メソッドを実行時にはtableView(_:cellForRowAt:)メソッドが呼ばれ、 引数nilのperformBatchUpdates(_:completion:)メソッド実行時は呼ばれないという違いがあります

起こったこと

  1. セルを生成する
  2. 指定した動画IDがなんらかの原因で存在しない、またはアクセス不可なので動画のサムネイルURL取得を再度試みる
  3. 共通の完了処理を呼ぶ
  4. 完了処理でreloadRows(at:)を呼ぶ
  5. 動画を表示するセルを再生成する(1に戻る)

という流れでの無限ループが完成しました。 動画を表示するためのセルが見える範囲内に存在する限り、無限に処理が走り続けます。

対応

さらにそもそもの話で、YouTubeの動画サムネイルはサイズ指定して取得できるため、画像が空の状態で一旦表示したあとに高さが変わることへの考慮は一切必要ございませんでした。 なので、サムネイル取得の試行完了後に行なっていた再読み込み処理をばっさり削除するかたちで事象は解消しました。

対応後の実装

セルのsetupメソッドからcompletion引数を削除しました。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...省略...
    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath)
    cell.setup(text: text)
    return cell
    // ...省略...
}

なんて無駄で弊害のある処理をしていたのでしょう! 驚きのシンプルさ。😭

修正版のリリース

リリース予定前日であり、その日修正版で審査を再提出してリジェクトされずに翌日リリースできるという保証はありません。ですので、社内のディレクターチームへ事象の説明や修正をいつリリースするかの対応を相談しました。

結果的な対応としては、もともとのバージョンを一旦予定通りリリースした直後に修正版を審査に提出、審査が通った次の日にリリースしました。

事象が発生したとしてもユーザー影響が限定的であることを鑑みての判断です。今回の事象が発生するのはケース入稿元のデータがおかしくないと滅多なことで発生し得ないこと、リリース日当日から動画の表示を始める大学がまだそれほど多くなかったこともあり。

さいごに

至極当たり前でたいへん初歩的ですが、以下を肝に銘じました・・・

  • こまかな仕様検討が開発と並行で走らざるおえない場合、未決定の状態で極力実装を進めない
    • 動画の取得が失敗した際、失敗したとき用画像を代わりに表示するかサムネイルの表示領域自体を非表示にするか開発フェーズ終盤まで確定させていなかった
      • 決まったあと不要と確定した処理を削れば良いかと思っていた節もある
    • 後回しにしがちだったエラーケースも早めに決めてもらえるよう、ディレクターへ積極的に声がけする
  • 既存の類似UIの処理を流用する際は、その時点で絶対に使用するとわかっている処理のみ絞り込んで利用する
    • "大は小を兼ねる"という概念はソースコードに当てはまらない、当てはめちゃいけない
  • リファクタリング先の影響範囲動作確認を徹底する
    • 全く同じように動くだろうと確認漏らすのはダメ絶対

現在担当しているプロジェクトの実装の品質は、前に担当したものよりかは安定しつつあるかなと自負してます。いや、安定していかないと不味いんですが。

ただし、新規事業開発でお忙しいところiOSチームリーダーの須藤さんにかなりの助力を頂いております。 今回の記事に記載したYouTubeのオーバーレイ表示に対する対応やサムネイルURL取得処理無限ループへの対応をはじめ、現在進行中のプロジェクトも。

まだまだ知識経験共に足りないひよっ子ですが、少しでも早くより力になれるよう、ひよっ子なりに学び取ったことは血肉として今後に活かしてゆく所存です。

そして、やらかしが発覚したあとの対応を決めるまでのスピード感や、「ブログ当番時に今回やらかした内容したためます〜」という具合にアウトプットすることへの寛容さにも驚いたり。

実は事象が発覚したリリース前日に自宅からのリモート勤務をしていて、これは1回会社に出勤して各方面へ直接相談しないと不味いかなと肝を冷やしました。 が、ディレクターチームへの対応の相談はMTGすら発生せず全部Slackのチャットで完結しました。ちょっと感動しました・・・良い環境に身を置けているなぁとしみじみ思います。