Studyplus Engineering Blog

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

Kotlin Coroutinesを(SDKに)導入しました!

初めまして。Studyplus開発部の若宮(id:D_R_1009) です。9月よりAndroidアプリの開発を行っています。

Kotlin、いいですよね。Kotlin Coroutines、シビれますよね。
ということで、Kotlin1.3がリリースされたことを記念してStudyplus-Android-SDKにKotlin Coroutinesを早速導入した話をします。

Studyplus-Android-SDKとは

Studyplus APIの詳細はこちらをご確認ください。

Studyplus-Android-SDKはStudyplus APIを簡単にご利用いただくためのSDKです。社内のAndroidチームメンバーにより開発・メンテナンスされています。
2018年の8月にフルスクラッチを行い1系から2系にバージョンが大きく上がりました。なお、2系からKotlinを利用しています。

Githubリポジトリ : https://github.com/studyplus/Studyplus-Android-SDK-V2

SDKは次の2つの機能を提供しています。

  • Studyplus APIによる学習記録の投稿
  • Studyplus AndroidアプリによるStudyplusとの連携

この学習記録の投稿処理をRxJava/RxAndroidからKotlin Coroutinesへ切り替えたv2.1.0系のリリースを、2018年10月31日に行いました。今回はこのRxJava/RxAndroidをKotlin Corutinesへ置き換えた経緯、置き換えの所感について書いていきたいと思います。

なぜRxJava/RxAndroidをやめてKotlin Coroutinesへ置き換えたのか

stable版がリリースされて嬉しかった 開発チーム内で上がっていた「RxJava/RxAndroidの利用は色々と過剰なのでは」という疑念を解消するためになります。

疑念1 : 利用シーンに対して

SDK内で行う通信処理は、主にサーバーのStudyplus APIを叩き学習記録を投稿する、というシンプルなものになります。そのため、本来Streamを扱うためのRxJavaであるのにOne-Shotな処理を行うに止まっていました。
SDKの基本機能としては現在のSDKでほぼ要求は満たされていると考えており、これ以上Streamを必要とする処理が追加される見込みもありません。

疑念2: SDKが持つ依存関係に対して

SDKを使っていただくデベロッパーの視点に立った時、RxJava/RxAndroidへの依存をSDK側で作成してしまうことに対してセンシティブになる必要があると思っています。
もちろんRxJava/RxAndroidは多くのプロダクトで利用されており、近年では当たり前な選択肢になっています。しかし、Androidの標準ライブラリに組み込まれるには至っていません。

Kotlin Coroutinesでは

Coroutinsのasync/awaitは1回の通信処理に対して1つのCoroutinesとなるため、処理と実装が必要十分な関係性になります。その意味で、SDK内の処理を整理し非同期処理をasync/awaitに統一することは、非常に理にかなった行為であると判断しました。

依存関係については少しチーム内で議論するところなり、はっきりとは解決しませんでした。
ご存知の通りKotlin Coroutinesの導入にはGradle上で依存関係を追加する必要があります。その点において、RxJava/RxAndroidを利用している問題は解決していません。
現時点での結論としては、SDK v2ではKotlinを記述言語として採用していることを重視しました。 RxJva/RxAndroid(+RxKotlin)を利用するよりもKotlin Coroutinesを利用する方が、はるかに依存関係がシンプルであると判断しています。

RxJava/RxAndroidとRxKotlinの比較

RxJava

// API通信
internal interface ApiService {
    @Headers(value = [
        "Accept: application/json",
        "Content-type: application/json"
    ])
    @POST("/v1/study_records")
    fun postStudyRecords(
            @Header("Authorization") oauth: String,
            @Body studyRecord: StudyRecord)
            : Observable<PostStudyRecordsResponse>
}

~~~

// 投稿処理
fun postRecord(context: Context, studyRecord: StudyRecord, listener: OnPostRecordListener?) {
    if (!isAuthenticated(context)) {
        throw IllegalStateException("Please check your application's authentication before this method call.")
    }

    ApiClient.apiClient.postStudyRecords(context, studyRecord)
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                    { listener?.onResult(success = true, recordId = it.recordId) },
                    { listener?.onResult(success = false, throwable = it) }
            )
}

Kotlin Coroutines

// API通信
internal interface ApiService {
    @Headers(value = [
        "Accept: application/json",
        "Content-type: application/json"
    ])
    @POST("/v1/study_records")
    fun postStudyRecords(
            @Header("Authorization") oauth: String,
            @Body studyRecord: StudyRecord
    ): Deferred<PostStudyRecordsResponse>
}

~~~

// 投稿処理
fun postRecord(context: Context, studyRecord: StudyRecord, listener: OnPostRecordListener?) {
    if (!isAuthenticated(context)) {
        throw IllegalStateException("Please check your application's authentication before this method call.")
    }

    runBlocking {
        try {
            val deferred = ApiClient.postStudyRecords(context, studyRecord)
            val result = deferred.await()
            listener?.onResult(success = true, recordId = result.recordId)
        } catch (t: Throwable) {
            listener?.onResult(success = false, throwable = t)
        }
    }
}

返りの型が変わるだけなので簡単ですね。Singleを使う程度の通信なら、書き換えのコストは非常に低く済みそうです。

まとめ

Coroutinesの導入

通信処理への導入は低コストであると実感しました。 そのためStudyplus Androidアプリへの導入を進めたいなと感じています。 特に新規に追加する機能はCoroutinesを積極的に導入をしようと話をしています。

ひとまず足がかりとして kotlinx-coroutines-rx2 の導入を着々と進めています。こちら、成果が出たら本ブログで公開いたします。

SDKの今後

より依存関係を減らし、軽量なライブラリを目指していきたいと思っています。
GsonやRetrofit、Retrofit-Kotlin-Coroutines-Adapterの依存関係は早期に解消していきます。

今回の置き換えにおいてはRetrofitによる簡潔なインタフェースがあったから迅速に対応できました。 しかしこれらの利便性と何を引き換えにしているか、常に検討しながら開発をしていくべきだとも感じる機会になりました。 今後とも、Studyplus-Android-SDKをよろしくお願いいたします。

最後に

一緒にKotlin Coroutinesを使って開発をしたい方はこちらからぜひご応募ください! 開発部一同、お会いできる機会を心よりお待ちしております。

Sidekiq Enterpriseを使う

お久しぶりです。サーバーサイドエンジニアの花井です。

みなさんは非同期処理に何をお使いですか? ActiveJobでしょうか? Resqueでしょうか? Sidekiqでしょうか?

弊社では歴史的経緯から、上記全てのgemをプロダクトで使っていますが、 この度Sidekiq Enterpriseを導入して非同期処理の統一に着手しました。 Sidekiq Enterpriseの日本語記事があまりなかったので、利用の検討や実際に使う際の注意点などを紹介できればと思います。

What is Sidekiq

SidekiqはRuby製の非同期処理フレームワークです。 resqueを使うと、ジョブのリトライとユニーク性の担保のために自前で実装が必要でつらいので、sidekiqの採用に至りました。

また、非同期処理の流量を制限したい(特に外部サービスへの問い合わせが発生する所)という意図があり、Enterpriseを採用するに至りました。

今回は以下の2機能を使ったので、その設定方法を紹介します。

  • RATE LIMITING
  • PERIODIC JOBS

How to use

install

ここから申し込みをすると利用に必要なキーの情報が送られてきます。 Gemfileに指定された内容を追記して、$ bundle install します。

Enterpriseを入れるとき、同時にProのインストールもします。 Enterpriseを申し込むとProの機能も使えるようになるというのは、こういうカラクリのようです。

Implementation

Wikiが充実しているので、そちらを参考にすればほとんど迷うことなく設定できます。

ActicveJobなどと併用している場合、Sidekiq単体(include Sidekiq::WorkerしたWorker)に変更する必要があるようです(Wikiに例がなかったのと、resqueueからの置き換えだったこともあり、Sidekiq単体で使うように書き換えていますがもしかしたらできるかもしれません)。

RATE LIMITING

大量の子ワーカーを作るような非同期処理1つ1つの中で、APIを叩くようなケースでは、許容量を超えたリクエストをしてしまうことがあると思います。そんな時に指定した量を超えないように制限できる機能がRate Limitingです。 ※Redis 2.8以上が必要です。

弊社ではサブシステムから大量のアプリ内メッセージを送信する際に、APIサーバーへ負荷をかけないようにすることを目的として使っています。

特別な設定は必要なく、流量を制限したいWorkerに、以下のような設定を記述します。

class HogeWorker
  include Sidekiq::Worker
  DELIVER_LIMIT = Sidekiq::Limiter.concurrent('limitter_name', 50, wait_timeout: 5, lock_timeout: 30)

  def perform
    DELIVER_LIMIT.within_limit do
          # 必要な処理
    end
  end
end

Sidekiq::Limiter.concurrent には、リミッター名、同時実行数、ロック空き待ち時間(秒)、ロックを手放す時間(秒)を指定できます。 Web UIにタブが追加され、そこからどのくらいロック待ちが起きたかなどのメトリクスを確認することができますので、チューニングも簡単にできます。

PERIODIC JOBS

非同期処理の定期実行をcron形式で指定できます。Sidekiqを使う場合、sidekiq-cronを使う方も多いと思いますが、Enterpriseにすると標準装備されています。 弊社では集計処理など、ピークタイムを避けて深夜に行いたい処理の実行に活用しています。

/config/initializer/sidekiq.rb の中に以下のように実行を開始する時間と、対象のWorkerを指定します。 cronに設定する時間は内部的にTimeクラスで操作されるので、システムのタイムゾーンに合わせて記述する必要があります。resque-schedulerを使っているとタイムゾーンも指定できるので、システムのタイムゾーンと異なるタイムゾーンを扱っている場合は注意が必要です。

Sidekiq.configure_server do |config|
  config.periodic do |mgr|
    mgr.register('0 20 * * *', Hoge::HardWorker)
    mgr.register('0 21 * * *', FugaWorker)
    mgr.register('0 23 * * *', Piyo::PiyoWorker)
  end

まとめ

プッシュ通知の送信が重複しないように、次はUnique Jobsを利用予定です。 他にも色々な機能があるので、必要に応じて活用していきたいです。

一緒にプロダクトの改善をやってくれる仲間を募集しています!

おまけ

Enterpriseの効果かわかりませんが、設定中に相談のIssueを立ててたところ早めに返事がもらえました。 また、確認の過程で翻訳のIssueが見つかったらしく、コントリビュートもできました。

Google Cloud Functions を利用して社内コミュニケーションを円滑にしていく

はじめに

こんにちはスタディプラスでCTOしています島田です。

現在スタディプラスには新メンバーが続々と入社して、うれしい悲鳴があがっています。 その悲鳴にほんのり応えた例の紹介です。

課題

  • Slackチャンネルが乱立(186のパブリックチャンネル)。そのため新メンバーが、どのチャンネルでどんな目的の会話をしているかを把握する事が困難
  • 上記の理由により、質問や必要な情報を取得するチャンネルを探すコストが上がっていた

f:id:yo-shimada:20181029101901p:plain

理想状態

  • 新メンバーが必要なSlackチャンネルを、迷う事なくすぐに見つけられ発言ができることで業務を円滑に遂行することができる

アプローチ案

理想状態を実現する方法を幅広く考える。

  • A) 全てのチャンネルを把握したメンバーを養成し、新メンバーが必要なチャンネルを聞く
    • =>❌ : 属人性・コストが高い
  • B) 定期的に不要なチャンネルを精査する
    • => ⭕️

定期的に不要なチャンネルを精査する

  • A) 毎月不要なチャンネルかどうかを聞いてまわってアーカイブする
    • =>❌ : コストが高い
  • B) 人力でSlackのアナリティクスから「最後にアクティブだった日」をCSVエクスポートして、そのCSVを利用してプログラマブルなアプローチをする
    • =>❌ : 人力要素がある。CSVだとチャンネルIDがエクスポートされないで、扱いが複雑になりそう
  • C) 不要なチャンネルを「最終投稿から一定期間投稿がない」と一旦定義して、Web APIを利用して過疎化しているチャンネルを探す。それを自動化して定期実行をする
    • => ⭕️

Web APIを利用したアプローチ

このアプローチだけでは「理想状態」へ到達する事はもちろんできないです。しかしそのためのファーストステップとして、まず明らかに不要と思われるチャンネルのアーカイブを自動化するというアイデアは良いかと思いました。

今回やること

  • 不要なチャンネル(30日間投稿がない)をアーカイブする
    • ただしパブリックチャンネルでも_で始まる業務以外のチャンネルは除く。例) #_boardgame
  • アーカイブ処理を自動化して定期実行をする

実現方法

で、実現方法を検討。 - A) チャンネルをアーカイブするプログラムを自分のPCでcrontabを実行する - =>❌ : 属人性が高い - B) チャンネルをアーカイブするプログラムを外部サービスを利用して定期実行 - => ⭕️

大きな課題解決に対する第一歩的でありお試しなので、限りなく運用負荷を低くしたいと思い、以下の技術を利用することにしました。

手順

  1. GCPの事前準備
  2. SalckのTokenを生成
  3. 実装
  4. Cloud Functions デプロイ
  5. Google Apps Scriptで定期実行を登録する

GCPの事前準備

HTTP のチュートリアル の「始める前に」を参考に事前準備。

SalckのTokenを生成

https://api.slack.com/apps より「Creaet New App」からAppをつくりトークンを生成 参考: Slack API 推奨Tokenについて

実装

プログラムの構成

  1. channels.list でチャンネル一覧を取得。
  2. _で始まるチャンネル以外を抽出
  3. 最終投稿を知るために、channels.history で履歴を取得。( channels.info だとAppを作成したユーザーが参加しているチャンネルでないとlatestが取得できなかったため )
  4. 各チャンネルの最終投稿が30日以上経過しているかチェック
  5. 対象となったチャンネルをchannels.archive でアーカイブを実行
  6. chat.postMessage で完了を通知

もろもろ準備

Node Slack SDK をインストール

$ npm init
$ npm install @slack/client -s

アーカイブをする処理を実装

$ vim index.js
const { WebClient } = require('@slack/client');
const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);

function archiveChannels(intervalDays) {
  web.channels.list({ exclude_archived: true })
    .then((res) => {
      const publicChannels = res.channels.filter(c => c.name.indexOf('_') != 0 );
      publicChannels.forEach(c => archiveForlornlyChannel(c, intervalDays));
      // 通知するチャンネルのID
      postMessage('CXXXXXX', 'SlackArchiveを実行しました');
    })
    .catch(console.error);
}

function archiveForlornlyChannel(channel, intervalDays) {
  web.channels.history({ channel: channel.id })
    .then((res) => {
      const ts = res.messages[0].ts;
      const latest = new Date(parseFloat(ts * 1000));
      const now = new Date();
      const diff = now.getTime() - latest.getTime();
      const days = Math.floor(diff / (1000*60*60*24));
      if (days > intervalDays ) {
        console.log(channel.id, channel.name, days);
        archiveChannel(channel);
      }
    })
    .catch(console.error);
}

function archiveChannel(channel) {
  web.channels.archive({ channel: channel.id})
    .then((res) => {
      console.log(res);
    })
    .catch(console.error);
}

function postMessage(channel_id, text) {
  web.chat.postMessage({ channel: channel_id, text: text })
    .then((res) => {
      console.log(res);
    })
    .catch(console.error);
}

exports.ArchiveChannel = (req, res) => {
  archiveChannels(30);
  res.status(200).send('OK');
};

デプロイ

関数をデプロイ

$ gcloud beta functions deploy ArchiveChannel --trigger-http

Slackトークンの環境変数をデプロイ

$ gcloud beta functions deploy ArchiveChannel --set-env-vars SLACK_TOKEN=Slackのトークン

Google Apps Scriptで定期実行を登録する

Google Apps Script(以下GAS)にプロジェクトを作って、定期実行を登録する。

参考:無料でお手軽Cron!Google Apps Scriptを使ってみる

  • GASにプロジェクトを作って、関数をコールするScriptを実装
  • 定期実行をするトリガーを登録
function fetchToSlackArchive() {
  UrlFetchApp.fetch("https://ここに関数URL");
}

実行

  • 設定した時間内で実行がされること(Slackに通知が来ること)を確認

f:id:yo-shimada:20181029102229p:plain

  • GCPのログビューアでみると55チャンネルがアーカイブされました

f:id:yo-shimada:20181029104006p:plain

  • Slakのアナリティクス

f:id:yo-shimada:20181029101940p:plain

まとめ

Cloud Functions を利用して簡単に開発と実行を実現する事ができました。 本質的な課題解決までは遠いですが、まずはその橋頭堡となる仕組みが出来たのではないかと思います。

新しい概念・技術をいきなりプロダクション環境で導入するには腰が重くなってしまいがちですが、社内のプロセスを改善するためにサクッとお試し導入することで、その技術の良さや導入ハードルを経験する事ができたと思います。 スタディプラスでは社内システムやプロダクション環境で積極的に新しい取り組みや技術の活用をして行くことを検討しています。 こういった取り組みに共感ができる方、是非一緒に働きませんか! こちらよりお待ちしております。

elm-upgradeに従ってElmのバージョンを0.18から0.19へ上げる

ForSchool事業部の石上です。ウェブのフロントエンドを中心にStudyplus for Schoolの開発に携わっています。

あるアプリケーションのElmのバージョンを0.18から0.19に上げる対応をしました。今回はこのことについて書きます。

背景

Studyplus for School で新たにちょっとしたサブシステムが必要になり、その小さなSPAのためのウェブフロントエンドの言語として今回、Elmを採用しました。

弊社のプロダクトのほとんどは、サーバーサイドはRuby on Railsで作られており、ウェブフロントエンドはJavaScriptかTypeScriptです。Elmでウェブのフロントエンドを書くことはけっこう挑戦的でしたが、会社としても他の言語に手を伸ばしていきたいという話もあり、CTOとチームリーダーから許しをいただきElmでの実装に至りました。

ある程度実装が終わった段階でElm 0.19のリリースが発表され、これまでのElmの仕様から大きく変更がありました。主要パッケージの移動、関数の仕様変更の数々...。しかしこれはある程度覚悟していたことです。今回実装したものがまだリリース前かつ小規模なアプリケーションだったので、1日くらいガッとやれば対応できるだろうということで、対応しました。

対応手順

  • Elmのバージョンを上げる
  • elm-upgradeを実行
  • elm-upgradeで自動修正されない部分を手で書き換え

Elmのバージョンを上げる

yarn upgrade elm@0.19-bugfix2

elm-upgradeを実行

elm-upgradeというツールがあるので、それを使います。

npx elm-upgrade

elm-upgradeを実行すると、修正の方針について何点か質問されるのでそれに答えながら修正を実行していきます。

INFO: Found elm at node_modules/.bin/elm
INFO: Found elm 0.19.0
INFO: Found elm-format at node_modules/.bin/elm-format
INFO: Found elm-format 0.8.1
INFO: Cleaning ./elm-stuff before upgrading
INFO: Converting elm-package.json -> elm.json
INFO: Detected an application project (this project has no exposed modules)
INFO: Installing latest version of elm-community/list-extra
Here is my plan:

  Add:
    elm-community/list-extra    8.1.0
Would you like me to update your elm.json accordingly? [Y/n]:
  • elm-stuffを消す
  • elm-package.jsonからelm.jsonへ移行する
  • 最新のelm-community/list-extraをインストールする

と言われています。

Elm 0.19からはパッケージの依存関係を記録するファイルが elm-package.json から elm.json になっています。 インストールしたモジュールのコードはelm-stuff から ~/.elm になりました。

elm-upgrade がこの移行をやってくれます。パッケージを入れ直そうとしているので、そのための質問が続きます。全部Yesと答えました。

完了すると、以下のメッセージが表示されます。

SUCCESS! Your project's dependencies and code have been upgraded.
However, your project may not yet compile due to API changes in your
dependencies.

See <https://github.com/elm/compiler/blob/master/upgrade-docs/0.19.md>
and the documentation for your dependencies for more information.

Here are some common upgrade steps that you will need to do manually:

- elm/core
  - [ ] Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate
- undefined
  - [ ] Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/
  - [ ] Replace uses of Date and Time with Time.Posix
- elm/html
  - [ ] If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document
  - [ ] If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox
- elm/browser
  - [ ] Change code using Navigation.program* to use Browser.application
  - [ ] Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward
- elm/url
  - [ ] Changes uses of Navigation.Location to Url.Url
  - [ ] Change code using UrlParser.* to use Url.Parser.*

elm-upgradeで自動修正されない部分を手で書き換え

上記のメッセージの通り、以降はソースを手で直していきます。親切にリストになっているので、これを上から潰していけばいいでしょう。

- elm/core
  - [ ] Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate
- undefined
  - [ ] Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/
  - [ ] Replace uses of Date and Time with Time.Posix
- elm/html
  - [ ] If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document
  - [ ] If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox
- elm/browser
  - [ ] Change code using Navigation.program* to use Browser.application
  - [ ] Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward
- elm/url
  - [ ] Changes uses of Navigation.Location to Url.Url
  - [ ] Change code using UrlParser.* to use Url.Parser.*

Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate

2件ありました。以下のように修正しました。

- toString 1000
+ String.fromInt 1000

Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/

https://package.elm-lang.org/packages/elm/time/latest/ を読むと、Elm 0.19においての時間の取扱について書かれています。Daylight Saving Timeの話を出したりしつつ、人間用の時刻をモデルやデータベースに持たせるんじゃない! ということが書かれています。データとしてはPOSIXタイムとタイムゾーンとして扱い、人間用の表現はモデルではなくビューの関数の中でやるんだぞというようなことが書かれています。なるほど。

Replace uses of Date and Time with Time.Posix

0.19ではそのような考えがモジュールに反映されています。これまでの0.18ソースで使っていたDateとTimeをTime.Posixに置き換える必要があります。

Time.everyしか使っていなかったので特に変更は必要ありませんでした。

If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document

使っていないので関係ありませんでした。

If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox

使っていないので関係ありませんでした。

Change code using Navigation.program* to use Browser.application

今回このアプリケーションはSPAとして実装、つまりページを読み込み直さずに状態とURLを書き換えてページ遷移を行いたいと考えていました。このフロントエンド側でのルーティングの機能は、Elm以外の言語・ライブラリでもたいてい何かしらの形で提供されています(ReactならReact Routerなど)。

Elm 0.18では「クリック時のデフォルト挙動を無効化しつつ、引数でメッセージを渡して副作用を起こす」みたいなヘルパーを書いてこれを実現していました。 こういう感じです

Elm 0.19ではこんなことをしないでもこの機能を実装することができるようになったようです。

Browser.applicationを見ると、関数シグネチャはこうなっています。

application :
    { init : flags -> Url -> Key -> ( model, Cmd msg )
    , view : model -> Document msg
    , update : msg -> model -> ( model, Cmd msg )
    , subscriptions : model -> Sub msg
    , onUrlRequest : UrlRequest -> msg
    , onUrlChange : Url -> msg
    }
    -> Program flags model msg

applicationに渡すレコードの中に、onUrlRequestonUrlChangeがあります。名前からしてこれらを使えば良さそうです。それぞれ説明を読んでみます。

onUrlRequest

When someone clicks a link, like <a href="/home">Home</a>, it always goes through onUrlRequest. The resulting message goes to your update function, giving you a chance to save scroll position or persist data before changing the URL yourself with pushUrl or load. More info on this in the UrlRequest docs!

リンクのクリック時にデフォルトでonUrlRequestメッセージが発行されるとのこと。ありがたい!

onUrlChange

When the URL changes, the new Url goes through onUrlChange. The resulting message goes to update where you can decide what to show next.

↑URLが変更されるとonUrlChangeになるとのこと。

UrlRequest Browser.UrlRequestUrlChange Url.Urlというメッセージを用意してあるとすると、こういう感じになります。

    Browser.application
        { view = view
        , init = init
        , update = update
        , subscriptions = subscriptions
        , onUrlRequest = UrlRequest
        , onUrlChange = UrlChange
        }
update msg model =
        UrlRequest urlRequest ->
            case urlRequest of
                Browser.Internal url ->
                    ( model, Navigation.pushUrl model.nav.key (Url.toString url) )

                Browser.External url ->
                    ( model, Navigation.load url )

        UrlChange url ->
            ({ model | url = url }, Cmd.none)

Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward

Elm 0.18でページ遷移したいときは、Navigation.newUrl "/hoge" みたいな形でできました。0.19のBrowser.NavigationにはnewUrlは無く、見てみるとpushUrlを使えば良いようです。

しかし遷移先の文字列をただ渡すのではなく、Browser.Keyなるものを渡す必要があるみたいです。

pushUrl : Key -> String -> Cmd msg

このKeyは外から書き換えたり出来ない値で、initで入ってくるものをModelに持っておいて、pushUrl などURL変更の関数に渡して使います。

You only get access to a Key when you create your program with Browser.application, guaranteeing that your program is equipped to detect these URL changes. If Key values were available in other kinds of programs, unsuspecting programmers would be sure to run into some annoying bugs and learn a bunch of techniques the hard way!

Changes uses of Navigation.Location to Url.Url

言われている通り修正しました。

Change code using UrlParser. to use Url.Parser.

言われている通り修正しました。

その他

elm installできない?

Elmのモジュールの依存関係はnpmなどと同様、1つのJSONファイルに記録されます。しかし npm install で全部ダウンロードというようなことはできません。elm 0.19で elm install を実行すると以下のメッセージが表示されます。

~/project (feature/update-elm-to-0.19 *)$ elm install
-- INSTALL WHAT? ---------------------------------------------------------------

I am expecting commands like:

    elm install elm/http
    elm install elm/json
    elm install elm/random

Hint: In JavaScript folks run `npm install` to start projects. "Gotta download
everything!" But why download packages again and again? Instead, Elm caches
packages in /Users/ishigami/.elm so each one is downloaded and built ONCE on
your machine. Elm projects check that cache before trying the internet. This
reduces build times, reduces server costs, and makes it easier to work offline.
As a result elm install is only for adding dependencies to elm.json, whereas
elm make is in charge of gathering dependencies and building everything. So
maybe try elm make instead?

パッケージは./elm-stuff ではなく、 ~/.elm にキャッシュされます。

Variable Shadowing

こんなエラーも出てきました。

./src/Main.elm
[=========================                         ] - 1 / 2-- SHADOWING --------------------------------------------- src/Views/Hoge.elm

The name `hoge` is first defined here:

23| viewHogeName hoge =
                    ^^^^^^^
But then it is defined AGAIN over here:

25|         Just hoge ->
                 ^^^^^^^
Think of a more helpful name for one of them and you should be all set!

Note: Linters advise against shadowing, so Elm makes “best practices” the
default. Read <https://elm-lang.org/0.19.0/shadowing> for more details on this
choice.
Detected errors in 1 module.

怒られている通り、こう直します。

viewHogeName maybeHoge =
    case maybeHoge of
        Just hoge ->
            hoge.name

        Nothing ->
            ""

エラーメッセージの通り、なんでこう直さないと怒られるんだというのは以下に書かれています。

Regex

I cannot find a `Regex.regex` variable:

330|             Regex.regex "hogehoge"
                 ^^^^^^^^^^^
The `Regex` module does not expose a `regex` variable. These names seem close
though:

    Regex.never
    Regex.find
    Regex.replace
    Regex.split

Hint: Read <https://elm-lang.org/0.19.0/imports> to see how `import`
declarations work in Elm.

Regexも変わっていました。今回エラーが出た Regex.regexRegex.fromString になったようです。以下のような変更が必要でした。

removeHogehoge hogehoge =
    let
         regex =
-            Regex.regex "hogehoge"
+            Regex.fromString "hogehoge"
        in
-            Regex.replace Regex.All regex (\{ match } -> "") hogehoge
+            Regex.replace regex (\{ match } -> "") hogehoge

さらにその他

非標準ライブラリにもElm 0.19で変わったものがあり、その対応も必要でした。

所感

変更範囲はかなり大きいので、大きめのアプリケーションを0.19対応するのは相当大変そうです。

しかし、elm-upgradeが最初にやることリストを出してくれたりコンパイルエラーにここ読めリンクが適切に貼られていたりと、とても親切に感じました。

変更点についても、Browser.applicationで簡単にSPAを作れるようになっていたりして、SPAのフレームワークとして改善されていると感じました。

参考記事

UIPickerViewをUIControlを使用してキーボードの様に表示する

こんにちは。 入社して一ヶ月が経過したiOSエンジニアの弘田です。
今回はUIPickerViewをキーボードの様に表示する方法を解説します。

なぜそんなことをするの?

昔のiPhoneでしたら画面の中心などにUIPickerViewを表示しても画面サイズが小さかったので片手で操作できていましたが、最近は大画面化が進み片手での操作が難しくなってきました。
操作性を損なわずにUIPickerViewを使用してもらう為にも今回の方法が役にたつと思います。

Human Interface GuidelinesでもPickerついて触れているページがありこの様な記載があります。

Avoid switching screens to show a picker. A picker works well when displayed in context, below or in close proximity to the field being edited.

翻訳
ピッカーを表示するように画面を切り替えることは避けてください。ピッカーは、編集中のフィールドの下、または近くにコンテキストで表示されたときにうまく機能します。

今回目指すもの

※シミューレーターなのでキーボードを閉じるアニメーションが少しおかしいです

実装

1. UIControlを継承したclassを作る

class pickerKeyboard: UIControl {

}

2.イニシャライザを作成し、自身がタップされた時にinputViewを出す処理を作る

class pickerKeyboard: UIControl {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        addTarget(self, action: #selector(tappedPickerKeyboard(_:)), for: .touchDown)
    }
    
    @objc private func tappedPickerKeyboard(_ sender: PickerKeyboard) {
        self.becomeFirstResponder()
    }
}

3.canBecomeFirstResponderをtrueで返して自身をFirstResponderにする

canBecomeFirstResponderのデフォルトはfalseになっていて、
trueを返さないと後述のinputViewで指定したViewが表示されません。

class pickerKeyboard: UIControl {
    
    //~~~省略~~~
    
    override var canBecomeFirstResponder: Bool {
        return true
    }
    
}

4.FirstResponderになった上でinputViewをoverrideする

ここでは表示したいViewを返します。
今回はUIPickerViewをaddSubviewしたUIViewを返します。
UIViewを返す理由はSafeAreaに対応するためです。
inputViewについて(Apple公式)

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputView: UIView? {
        let pickerView: UIPickerView = UIPickerView()
        pickerView.delegate = self
        pickerView.dataSource = self
        pickerView.backgroundColor = UIColor.white
        pickerView.autoresizingMask = [.flexibleHeight]
        
        // SafeArea対応をする為にUIViewを挟む
        let view = UIView()
        view.backgroundColor = .white
        view.autoresizingMask = [.flexibleHeight]
        view.addSubview(pickerView)
        
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        pickerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
        
        return view
    }
}

5.inputAccessoryViewをoverrideしてUIPickerViewを閉じるボタンを作る

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputAccessoryView: UIView? {
        
        let view = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
        view.frame = CGRect(x: 0, y: 0, width: frame.width, height: 44)

        let closeButton = UIButton(type: .custom)
        closeButton.setTitle("閉じる", for: .normal)
        closeButton.sizeToFit()
        closeButton.addTarget(self, action: #selector(tappedCloseButton(_:)), for: .touchUpInside)
        closeButton.setTitleColor(UIColor(red: 0, green: 122/255, blue: 1, alpha: 1.0), for: .normal)

        view.contentView.addSubview(closeButton)
        
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        closeButton.widthAnchor.constraint(equalToConstant: closeButton.frame.size.width).isActive = true
        closeButton.heightAnchor.constraint(equalToConstant: closeButton.frame.size.height).isActive = true
        closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
        closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true

        return view
    }

    @objc private func tappedCloseButton(_ sender: UIButton) {
        resignFirstResponder()
    }
}

6.通常のUIPickerViewと同様にUIPickerViewDelegateとUIPickerViewDataSourceを継承してデータを表示

class pickerKeyboard: UIControl {
    let array:[String] = ["A","B","C","D","E"]
        
    //~~~省略~~~
}

extension PickerKeyboard: UIPickerViewDelegate, UIPickerViewDataSource {
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return array.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return array[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // delegateなどでViewControllerに選択された情報を渡す
    }
}

7.StoryboradやXibでUIViewのCustomClassとして設定する

まとめ

手順4のUIPickerViewを作った時にpickerView.backgroundColor = UIColor.whiteとしているのでわかり難いですが、 別の色に変更するとSafeArea対応できていることが確認できます。

記事で紹介したコードはGithubで公開しています。
https://github.com/srknra/PickerKeyboard

今回はUIPickerViewをキーボードの様に表示してUXを低下させない様な工夫でしたが、
Studyplusのアプリでは他にもユーザーの事を考えて様々な工夫をしてるので今後もブログで紹介していこうと思います。

Visual Studio Codeの拡張「GitHub Pull Requests」を使ってみた

こんにちは。7月に入社したStudyplus開発部の田口です。
先日Microsoft社が発表したVisual Studio Codeの拡張のGitHub Pull Requestsを試してみたのでその記事を書きます。

まずは弊社のエンジニア陣が普段なんのエディタを使っているのか、アプリチーム以外でアンケートを取ってみました。

  • RubyMine … 2人
  • IntelliJ IDEA … 1人
  • Atom … 1人
  • Visual Studio Code … 1人
  • Vim … 3人
  • Emacs … 1人

Vim強しですね。Visual Studio Codeは自分しか使っていないようでした。
また、アプリチームの方々もXcodeやAndroid Studio以外で、普段よく使うエディタを聞いてみたところ

  • Sublime Text … 1人
  • Atom … 1人
  • Visual Studio Code … 2人

でした。こちらはVisual Studio Codeを使ってるエンジニアが2人いました。嬉しいですね。

RubyやRailsを書くエディタとなるとまず思いつくのがJetBrains社のIDEであるRubyMineですが、前にReact/Reduxを書く際にVisual Studio Codeを使って以来すっかりお気に入りのエディタになったのでずっと使っています。
GitHub社がMicrosoft社に買収されてから、マイクロソフト製のプロダクトにGitHub関連の機能が追加されていくのかなと予想していたのですが、まさにそういったパッケージだと思います。
今回は、GitHub Pull Requestsを実際に使ってみた様子を記載していきます。

インストール

基本的には、Visual Studio Codeの拡張機能が検索できるVisual Studio Marketplaceで「GitHub Pull Requests」と検索すれば出てきます。
Image from Gyazo

使い方

サインイン

拡張を追加すると、画面右下にGitHubへのサインインを求めるダイアログが出るので、クリックしてGitHubにサインインします。

追加項目

GitHub Pull RequestsをインストールしてからVisual Studio Codeのソース管理タブを開くと、「GITHUB PULL REQUESTS」という項目が追加されています。デフォルトは閉じていて最下にあるのでちょっと見落としがちです。
GitHubで管理されているプロジェクトをルートとして開くと、自動的に「GITHUB PULL REQUESTS」に以下の項目が追加されます。
Image from Gyazo
ここではGitHub上にあるプルリクエストを項目別で見ることができます。自分が作成したプルリクエストのみ、自分がアサインされたプルリクエストのみといった項目があります。
例として、RailsのGitHubリポジトリに上がっているプルリクエストを見てみます。
Allを選択すると、今見ているGitHubリポジトリの全プルリクエストが表示されます。
Image from Gyazo
Railsには2018/09/25現在で700以上のプルリクエストが上がっていました。基本的には最新の20件までがデフォルトで表示されるみたいです。

Conversationを見る

確認したいプルリクエストをクリックして開き、さらにその下の「Description」をクリックすることで、GitHub上の「Conversation」タブで見れるページがエディタ部分に表示されます。
こちらも、Railsのリポジトリを例に見ていきます。
Image from Gyazo
ブラウザでGitHubを確認することなくここでプルリクエストの概要やレビューのやり取りを確認できます。Visual Studio Codeで設定したカラースキームでプルリクエストが確認できるのが個人的に嬉しいポイントです。
コミットハッシュのリンクをクリックするとブラウザでGitHubの当該コミットのページを開きます。
右上のCheckoutボタンで、当該ブランチをワンクリックでチェックアウトできたりもします。
また、コメントやレビュー、プルリクエストを閉じたりもVisual Studio Code上でできます。便利ですね。
Image from Gyazo
現状ではプルリクエストのマージはできないみたいですね。

差分を確認する

GitHub上の「File Changed」も簡単に見れます。「Description」以下がファイルごとの差分になっており、それをクリックするとエディタ部分に表示されます。
Image from Gyazo
アイコンは

  • A … Add
  • D … Delete
  • M … Modify

だと思われます。直感的ですね。
行番号の右にある+ボタンをクリックすることで、コメントをつけることもできます。
Image from Gyazo

今後改善してほしい点

非常に便利なこの拡張ですが、個人的に今後のバージョンアップで改善してほしい点としては

  • マルチルートワークスペースに対応してほしい
  • マージできるようにしてほしい
  • レビュアの設定などができるようになってほしい

という感じでしょうか。
GitHub上でできることがすべてVisual Studio Code上でできるようになってくれれば最高なんですが、さすがに高望みな気もしています。
個人的に特に気になる点はマルチルートワークスペース非対応なところです。複数のリポジトリを横断的に見るためにマルチルートワークスペースを利用しているのですが、現状はマルチルートワークスペースだとGITHUB PULL REQUESTSの項目が表示されないようになっています。
ここが改善されて対応してくれるようになるとさらに虜になると思うので、期待したいところです。

まとめ

GitHub Pull Requestsを触ってみて、自分はかなり便利だと感じました。
まだバージョンも0.1.6(2018/09/25現在)なので、今後のアップデートに期待したいと思います。

buildersconに行ってきた

スタディプラスCTOの島田です。 今回は9/6~8に開催されたbuilderscon tokyo 2018へ行ってきた感想を書きます。

f:id:yo-shimada:20180912200947j:plain

最初に

buildersconとは

buildersconは、「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭りです

という趣旨のもと、インフラ・IoT・サーバー・デザイン・etcと本当に幅広いジャンルのセッションが聞けます。

builderscon.io

スタディプラスは今回スポンサーとして協賛をさせて頂きました。 ノベルティグッズとしてペンと付箋を提供しました。

f:id:yo-shimada:20180912201804j:plain

当日は私を含め3名(島田、石上、田口)でbuildersconに行ってきました。 どのセッションも大変興味深かったのですが、その中でもこの3名のがそれぞれ特に印象に残ったセッションの感想を書かせてもらいます。

島田

IoT開発の闇

SNS等でのシェア禁止の内容という事で詳細は触れられないのですが、闇(笑)を物凄く堪能できました。これだけで前夜祭行って良かったかも。

パスワードレスなユーザー認証時代を迎えるためにサービス開発者がしなければならないこと

speakerdeck.com

パスワード認証の現状は、色々と漏洩リクスがあり課題・問題ありという整理に共感。 ではパスワードレスを実現するための最新動向はどうなっているかという点での、FIDO 2.0、WebAuthn APIの紹介にまだまだ普及への時間がかかりそうだが、未来を感じた。

「Web とは何か?」 - あるいは「Web を Web たらしめるものは何か?」

Webの黎明期の動向から丁寧に説明があり、Webに関わる人の視点から、どうWebが変化したが整理されていた。 そして、今後のWebがOSに近づいていくのでは、という流れが秀逸。

石上

ForSchool事業部の石上です。 私は普段ウェブアプリケーションのエンジニアなので、それに関連ありそうな発表を中心に聞いてきました。

個人的に1日目、2日目でそれぞれ特に面白かったのは以下の発表です。

Electronによるアプリケーション開発事情2018

ElectronベースのMastodonクライアントWhalebirdを個人で開発されているh3potetoさんの発表。 趣味でここまでできるのすごい...。

リリース直後はElectron製ということで評判が悪かったそうですが、原因を調べてパフォーマンスの問題を解決したりして、今では多くの人に快適に使ってもらえているそうです。

こちらの発表では、実装面でのSwiftでiOSアプリを開発していたときとの比較があり、勉強になりました。

私はiOSのことを全然知らないので、なんとなくSwiftのほうがGUI設計の考え方は進んでるんだろうなくらいに思っていました。 そのため、Fluxを使えることがElectron選択のメリットとなることに少し驚きました。

一方で、TootのStreamingによる描画パフォーマンスの劣化の話では、iOSのUITableViewの描画の最適化みたいなところは便利にできてるんだなと感心しました。

その他もMac App Storeへの配布周りのつらみなど、お試しデモアプリだけ作っていては出てこないような問題について聴けて貴重でした。

デザイナーとうまく協働する方法

buildersconの中では異色な、デザインをつくる際のコミュニケーションについての発表でした。 偉い人の意見を優先してしまう、あるいはデザイナーの作ったものを無条件で正しいとしてしまったりせず、論理的にデザインを作っていく方法について話されていました。

「よい」というニュアンスを自分の中ではなく組織内で共有するためのプロセスが必要で、それに基づいてデザインの評価は行われるべき。そうでないと、デザインがセンスや好みだったり偉い人が決めるみたいな状況になってしまう。そうならないように、徹底した言語化と文書化が必要という話でした。

普段開発ばかりしていると、ドキュメントをあくまでも補助的な、「あったらいいよね」くらいのものとして考えてしまいがちです(少なくとも私はそうでした)。しかし、こちらの発表を聞いて、ドキュメントは議論のベースに使ったり手戻りを防ぐための重要なツールであると認識することができました。

田口

1日目、2日目それぞれで自分が良いと思ったセッションの感想を書きます。

1日目: ブロックチェーン(DApp)で作る世界を変える分散型ゲームの世界

speakerdeck.com

この発表は、発表者の緒方さんご自身がブロックチェーンを用いて作っているゲームの説明を元に、分散型ゲームの世界を解説していくというものでした。 発表が丁寧でわかりやすい説明だったので、ブロックチェーンをほとんど知らない自分はとても助かりました。

ブロックチェーンは様々な分野で活用される可能性がありますが、まだまだ実験段階のようで、現在は主に「投資」の面で利用されていることが多いそうです。 そんな中、「投資」ではなく「利用」に重きを置いたブロックチェーンの活用という点で、ブロックチェーンを利用した様々なゲームが紹介されていました。 発表内では、ブロックチェーンゲームの特徴として ・ゲーム内で購入や投資したものが資産になる可能性がある ・セキュアで公平な取引 ・「トークンエコノミー」と呼ばれるトークンの互換性 の三つが挙げらています。 個人的には特に三つ目のトークンエコノミーが気になります。価値をトークンに落とし込むことで、様々なものに応用できそうです。 シームレスな接続のために規格が統一されてきているそうなので、今後が楽しみです。

2日目: RDB THE Right Way ~壮大なるRDBリファクタリング物語~

speakerdeck.com

普段自分たちが利用しているRDBの設計・リファクタリングをどのようにやっていくかという発表でした。 アンケートシステムの回答を保存するという具体的な例を元に、どのような罠があり、どう対応していくかが明確でわかりやすかったです。

データベースで扱うのは「データ」、アプリケーションで扱うのは「情報」であり、どのようにデータを保存するかを設計する「データ設計」と、データをどのように加工・利用するかを設計する「情報設計」を、まずそもそも認識できていなかったなと反省しました。 情報を優先してデータ設計をするとデータに矛盾が生まれるので、まずはモデリングをしっかり行うことも常に念頭に置いていこうと思います。 Entityの定義やそれの関連付けなど、とても勉強になることばかりでした。 そーだいさんが紹介されていたSQLアンチパターンの本をしっかり読もうと思います。

最後に

3名とも初めて参加したのですが、どのセッション大変面白く是非とも来年も参加をしたいと思います!

記念撮影ブースで石上、田口が撮影してもらった素敵な写真をあげておきます。

f:id:yo-shimada:20180912201421j:plain