Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

Android で HTML をいい感じで表示できるようにした話

こんにちは。開発本部の CLINICSカルテ の開発を担当している @seka です。メドレーでは貴重な (?) エンジニアの若者枠として日々奮闘しております。

今回、開発本部で定期的に開催している勉強会「TechLunch」で、「Android で HTML をいい感じで表示できるようにした話」 という題で発表しましたので、その内容について紹介させていただきます。

1. きっかけ

医師たちがつくるオンライン医療事典MEDLEY (メドレー) をアプリ化することができるか検証してみて欲しいという相談を受け、Android のモックを作成することになりました。

アプリらしい UI を目指して開発を進めていたのですが、MEDLEY では病気記事が CMS などに見られるような HTML 形式で管理されており、そのまま表示してもイメージしたようなデザインが実現できないかも...という課題に直面しました。

f:id:medley_inc:20181030115802p:plain

2. HTML を表示するまで

いくつかのステップに分解して、HTML の要素を Androidコンポーネントに置換していくことで対応する方針を立て、その実現可能性を調べました。大まかなフローはこんな感じです。

  1. 事前に HTML と Androidコンポーネントの対応を決める
  2. HTML を Kotlin でも扱える形式に変換する
  3. HTML の要素を探索する
  4. マッチした要素を Androidコンポーネントで置き換える それぞれのフローについて解説していきます。

1. 事前に HTML と Androidコンポーネントの対応表を作る

簡単にですが下表のような対応を決めます。(検証段階だったので、いくつかの要素は省略しています)

f:id:medley_inc:20181029222937p:plain

2. HTML を Kotlin で扱える形式に変換する

HTML を生の String として操作するのは流石に辛いので、Kotlin でも扱いやすいような形に変換します。

今回は jhy/jsoup という便利そうなライブラリを見つけたため、これを利用することにしました。

jsoup は Java 製の HTML パーサーで example にもあるように HTML を Kotlin でも扱いやすい形式に変換することができます。

下記の例は Wikipedia のページを取得し Root Node から小要素を探索していく様子です。

HTMLタグの情報・親要素・内容のテキストを利用したいため保持しておきます。

val doc = Jsoup.connect("http://en.wikipedia.org/").get()
doc.childNode(1).childNode(2).childNode(1)
/*
{Element@5739} "<div id="mw-page-base" class="noprint"></div>"
 attributes = {Attributes@5755} 
 baseUri = "https://en.wikipedia.org/wiki/Main_Page"
 childNodes = {Collections$EmptyList@5720}  size = 0
 shadowChildrenRef = null
 tag = {Tag@5756} 
  canContainInline = true
  empty = false
  formList = false
  formSubmit = false
  formatAsBlock = true
  isBlock = true
  preserveWhitespace = false
  selfClosing = false
  tagName = "div"
 parentNode = {Element@5729} 
 siblingIndex = 1
*/

3. HTML の要素を探索する

HTML の要素を探索する 再帰を利用して Root Node から小要素を探索し、jsoup で置き換えていきます。

class HTMLConverter(private val html: String) {
	fun parse() {
		val body = Jsoup.parse(html).normalise().body()
		inspect(body, ElementViewHolder())
	}

	private fun inspect(parent: Element) {
		parent.children().forEach {
			if (parent.children.isEmpty()) {
				return@forEach
			}
			inspect(it)
		}
}

4. マッチした要素を Androidコンポーネントで置き換える

事前に定義した方法に従って、HTML をそれぞれ対応するAndroidコンポーネントに変換していきます。

Img 要素の場合:

Glide を利用して画像を非同期で取得し ImageView にラップする。

private fun convertImage(el: Element): View? {
	val url = HttpUrl.parse(el.absUrl("src")) ?: return null return ImageView(context).apply {
		layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
		val header = LazyHeaders.Builder().addHeader("Content-Type", "image/bmp").build()
		val glideUrl = GlideUrl(url.url(), header)
		Glide.with(context)
			 .asBitmap()
			 .load(glideUrl)
			 .into(this)
}

Table 要素の場合:

Table と TableRow をそれぞれ作成して合成する。

private fun parseTable(el: Element): View? {
	return TableLayout(context).apply {
		layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
	}
}

private fun createTableRow(): View? {
	return TableRow(context)
}

private fun composeTableRow(self: TableRow, parent: TableLayout): Boolean {
    table.addView(self)
    return true
}

Android Emulator で表示してみる

上記の方法で作成したモックがこちらになります。

f:id:medley_inc:20181029223014g:plain

ただ HTML を表示するだけであれば WebView を利用すればもっと楽に表示することもできますが、UI をカスタマイズしたい場合にはデータに変更を加えなければいけません。

今回のようなアプローチをとることで、もともとある HTML のデータを壊すことなく Android に適したデザインを実現しやすくなったのではないでしょうか。

さいごに

コミュ障故に人前で話すことを避けてきたのですが、 TechLunch という機会をいただき Android なネタを発表をさせていただきました。

今回作成したモックは技術検証目的であるためリリースの予定はありませんが、発表のために書いた実装例は seka/HTMLConverter.kt として公開しています。

このようなアプローチを取る機会は少ないと思いますが、読んでくださった方の力になれれば幸いです!

「とりまわかるTTS」というお話

こんにちは、開発本部の宮内です。先日のメドレーの社内勉強会「TechLunch」で、「とりまわかるTTS」と題してWeb Speech APIのお話をしました。

 

Web Speech APIとは?

macOSに、sayというコマンドがあるのはご存知でしょうか? このコマンドは引数で受け取った文字列を発音してくれるというコマンドです。

ターミナルアプリを開いて、次のようなコマンドを入力してみてください。

say -v Kyoko "ご用件は何でしょう?"

このようにテキストから人間の声のように発音させる仕組みを音声合成といいます。

このような音声合成音声認識に関してはmacOS以外にもAmazon TranscribeGoogle Cloud Speech APIなどのクラウドサービスやAndroidではTextToSpeechクラスというAPIが用意されていたりもします。

今回の発表で使ったWeb Speech APIは、ブラウザでこれらの音声合成・認識を行うためのAPI仕様です。

 

実際に試してみる

音声合成サンプルページを作りましたので、サンプルプログラムを抜粋して使い方の説明をしていきます。

voiceschangedイベント内で利用可能な音声を取得する

const speechSynthesis = window.speechSynthesis;

speechSynthesis.addEventListener("voiceschanged", () => {
  buildVoiceOption($voices, speechSynthesis.getVoices());
});

window.speechSynthesis.getVoices関数を使うと利用可能な音声の一覧が取得できます。

ただし、ページロード直後ですと、タイミングによっては空配列が帰ってくることがあります。 そのため、voiceschangedイベントを受け取ってから、window.speechSynthesis.getVoices関数を呼び出すことによって、確実に実行できるようにしています。

返却されてくるvoiceは仕様としてはユーザエージェントとブラウザの場合だとローカルに用意されている音声の種類で決ってくることになっています。

macOSの日本語であれば以下のような種類のvoiceが返ってきます。

f:id:medley_inc:20181019115926p:plain

buildVoiceOptionは利用する音声をselectタグで選択できるように設定する関数なので割愛します。

SpeechSynthesisUtteranceで音声の設定をしていく

SpeechSynthesisUtteranceとは以下のような働きをするAPIになります。

Web Speech APIのSpeechSynthesisUtteranceインターフェイスは、音声要求を表現します。 これには、音声サービスが読むべき内容とその読み方に関する情報(例えば、言語、ピッチ、音量など)が含まれています。

https://developer.mozilla.org/ja/docs/Web/API/SpeechSynthesisUtterance

SpeechSynthesisを使って実際に音声を発音させるにはこのAPIを使ってどのように発音させるのか?を設定していく必要があります。

function setVoice(utterance, voices, voiceName) {
  const choices = voices.filter(voice => voice.name === voiceName);
  if (choices.length > 0) {
    utterance.voice = choices[0];
  } else {
    const defaultVoice = voices.find(voice => voice.default);
    if (defaultVoice) {
      utterance.voice = defaultVoice;
    }
  }
}

$form.addEventListener("submit", event => {
  event.preventDefault();
  const utterance = new SpeechSynthesisUtterance($textarea.value);
  setVoice(utterance, speechSynthesis.getVoices(), $voices.selectedOptions[0].dataset.name);
  utterance.pitch = $pitch.valueAsNumber;
  utterance.rate = $rate.valueAsNumber;
  utterance.volume = $volume.valueAsNumber;
  speechSynthesis.speak(utterance);
});

フォームがサブミットされたら SpeechSynthesisUtteranceクラスのインスタンスを作成し、 それを引数にspeechSynthesis.speak関数を呼び出せば、テキストボックスに入力されたテキストとSpeechSynthesisUtteranceで設定したピッチ、音量などを元に設定された音声が出力されるようにしています。

(サンプルプログラムではエラー処理を省いているため、ピッチや音量の調整スライダーで極端な値を指定すると、正しく音声が出力されないことがあります)

今回の記事では紹介していませんが、Web Speech APIにはもうひとつSpeechRecognition APIがあり、こちらは音声認識をブラウザでできるようにするAPIになっています。興味があればぜひ調べてみてください。

 

まとめ

今回はWeb Speech API音声合成のさわりを紹介しましたが、ご覧のとおりとても簡単に使うことができます。

例えば、ブラウザの内容の読み上げをしてアクセシビリティを高めたりなどの使い方や、読みが難しい専門用語の発音を聞かせるようにするなど色々な使いかたが考えられるのではないでしょうか?

この記事をご覧のみなさんもちょっと忘れかけられた、このAPIを使ってみてはいかがでしょう?

新オフィスのデザインを任された話

先日、新卒エンジニアさんの内定式(メドレー初!)をおこないました。バキバキでキラキラのイケメン揃いなので中途イケメン枠で入社した私にとってこれまでの地位が危ぶまれ戦々恐々としています。開発本部デザイナーの小山です。
 
内定式もですが、大きな変化がメドレーにありデザイナーの役割が広がる機会が訪れたので、この場を借りてお話させていただきます。
 
近年デザイナーは分野を飛び越えた取りくみが求められる職種になりつつありますクラシカルデザインが中心だった頃、それができるのはスターデザイナーであり、ごく限られた人たちでした。いまではテクノロジーフレームワークの進歩により、デザイン思考やコンピュテーショーナルデザインなど、デザイナーが関われる分野はさらに広がりを見せています。いちデザイナーの私もそれに応えたいという想いはありつつも取りくむ難しさを感じています。
 
今回は分野を飛び越える難しさを日頃感じているUIデザイナーの私が、オフィスデザインという普段とは異なる分野に取りくんだお話をさせていただきます。
 

これからの受け皿を設計する

 
実は先月9月上旬に、メドレーは古巣の新六本木ビルを離れ六本木グランドタワーへ本社移転しました。私が入社した去年3月時点から社員数が3倍近く増えたので、「社員数拡大のためのキャパシティ確保設備の充実」「組織の一体感の強化が主に取りくむべき移転の課題でした。その解決のためオフィス専門の設計チームが初期のアーキテクトと施工管理を担当し、空間の方針とデザインと什器のディレクションマエダと私が担当しました。
 
先だって取りくんだのは空間の方針です。この先のメドレーの姿を踏まえ、そこに到達するための受け皿となるように設計しました。
これまでメドレーは社会の公器としての意識を持ち、様々なサービスで医療課題に取りくんできました。それが今年で創業10年を迎えています。規模も拡大し、さまざまな人がこのメドレーに参画するようになりベンチャー企業としてだけでなく社会の大きな責任を果たす存在になりつつあります。
そうしたときの空間の役割としてこれまでのベンチャーマインドとこれからの大きな責任を背負う姿勢の両方を意識できる空間づくりが良いのではと考え以下のような方針を組み立てました。
 

可変と不変の両極を横断する空間

 
急速に成長するためのベンチャーマインドと、拡大していく社会の公器として責任を負う姿勢を同居させるために、ただ新しくするだけではなく、今までのメドレーはしっかり持ち合わせる。そしてそれを日々の業務のなかで行き来できるように空間の方針をつくり、デザインに反映していきました。もちろん制約が多く結果論的な部分はありますが、新しくするために全てを壊すのではなく、かといって古いものを大事に取っておくのでもない、これまでのメドレーらしさとこれからのメドレーの2つをポジティブに意識し設計しました。
 
執務室は旧本社ビルの雰囲気をそのままスライドさせつつ、先に挙げた3つの課題を取りくむため機能を拡張しました。もっとも変えたのはエントランスから会議室にかけてです。コーポレートカラーである赤色を一切使わず真っ白な空間にしました。執務室が変えない場所であるなら、ここは変える場所であり、自分たちを一度否定し絶えず新しくしていく場所として位置付けました。これから様々な人と出会える場所としても日々新鮮な気持ちになれると考えたからです。

f:id:medley_inc:20181010113717p:plain

f:id:medley_inc:20181010113722j:plain

f:id:medley_inc:20181010113729p:plain

f:id:medley_inc:20181010113741p:plain

f:id:medley_inc:20181010113736p:plain

 

空間デザインの独特な難しさと向きあう

 
この空間をつくるために、この分野特有壁と向きあいました。UIデザインでは扱わない大きなサイズ感や専門知識を総動員して出来上がる空間を想像する力など、普段の仕事にはない技術や感覚を求められることが多く、独特の難しさを痛感しました。
 
たとえばUIデザインでは簡単にモックアップがつくれますが空間で「よし試しに壁たてるか!」なんてことはできません。仮にモックアップをつくれたとしてもUIデザインほどの情報量で仕上がることはなく、そこは培ってきた経験と知識で補わなければなりません。専門のチームがいるので、えーい丸投げ!あとヨロシク!という考えも頭をよぎりましたが、決めるのは私の役目でもあるので、円滑なコラボレーションにするために、その人たちの知識や感覚に追いつくことは急務でした。
 
ここまでの話を聞くと体力的にキツそうと思われるかもしれませんが、実際は現業と通じる部分やデザイナーの感覚を分野を超えて持ち込める部分もありましたので、楽しみながら取りくめました。例えばデザイナーは物の形や色の違いに敏感な種族なので、それが2Dでも3Dでもすぐさま察知できます(程度にもよりますが)。図面と比較してわずかな壁の色や照明の輝度や色、わずかな目地のズレなどなど。
 
とはいえ異なる分野で直感1つで勝負できないのも理解しました。まだまだ学びは多そうですが、この取りくみを日頃の仕事にフィードバックしたいと思います。下記は取りくみを通して同じような機会を得たときに使える備忘録としてまとめてみたポイントです。
 
  • 考えかたの定型化 - 何かをデザインするのなら進め方に違いはないはず。違っても自分の考えかたの型をもとにカスタマイズする
  • 礼儀としての知識 - よほど未開拓でない限りその分野の専門家がいるはず。コラボレーションするために最低限の礼儀として知識は身につける
  • 違和感を無視しない - その分野の常識や知識のインプットが追いつかなくても、そこで感じる引っかかりを共有することで周りが立ち返れる
 

分野が違うだけで別世界ではない

 
今回は分野を変えることで発見したことや取りくみを、オフィスデザインを通してお話させていただきました。振りかえると備忘録の3つや思考の整理の方法など異なる分野だとしても、自分の専門分野での大事な考え方と共通する部分も多く感じました。もちろん全ての分野を渡り歩いたわけではないので当てはまらない場合はありますし、先述したように独特の難しさもあります。ただこの3つがどんな分野でも飛び越えれる最初の道具のうちの1つにすることで、良いスタートが切れるのかなと私は感じています。
 
異なる分野は今までの常識が全く通用しない別の世界ではないことを意識しながら、新しいことに挑戦していきたいと思います。
 
ここまで読んでいただき、ありがとうございました!
 

さいごに

 
メドレーが向き合う医療の課題は複雑です。課題を解決するために1つの分野からの一点突破もありますが、多角的な分野からの突破もおこなっています。デザイナーでありながら、エンジニアでありながら、様々な分野に横断しスイッチでき課題解決に向き合うことができます。もしご興味のある方はぜひご連絡ください。
 

松江で再びサテライトオフィス体験してきました

お疲れ様です。開発本部の宍戸です。

9/11〜9/14まで、昨年もお世話になった松江市でお試しサテライトオフィス勤務を行ってきました。

昨年度は総務省のサテライトオフィス事業を利用する形でしたが、今年は松江市独自のプロジェクトとして実施されるとのことで再度松江市さんからお声がけをいただき、開発本部から3名でサテライトオフィス勤務をしてきましたので、その様子をレポートします。

オフィスの様子など

今回のサテライトオフィス勤務は、全日程を松江テルサ別館で行いました。昨年もお世話になったこちらの施設ですが、開発に必要なものは基本的にすべて揃っているので、到着してすぐに作業することができました。(なぜ真ん中の卓に全員座らなかったのか、仲悪すぎだろと言われたりしますが決してそういうわけではありません(真顔))

f:id:Layzie:20180928172848j:plain

(写真は、今回アテンドしてくださった松江市の土江さんの許可をいただき使わせていただきました)

 

こちら松江テルサ別館には、松江オープンソースラボというOSSに関する作業や交流のために提供される施設があります。今回伺ったタイミングでは、このラボのエリアは、区画を広げるべく現在改装中でした。

話を伺うと、松江市サテライトオフィスを構えるWeb系企業も徐々に増えつつあるようで、現地での勉強会の開催などに利用しやすいよう今回の改装を行っているとのこと。(大部屋として使ったり、分割して使ったりなどなどできるようにしているそうです)

働く環境だけでなく、コミュニティ支援についても市が積極的にサポートしていく雰囲気を伺い知ることができ、あらためてRuby Cityとしての気概を感じました。

松江の雰囲気など

松江への到着は初日のお昼頃でしたので、まずは前回も伺った八雲庵さんでお蕎麦をいただきました。松江城のお堀のすぐ近くにあり、歴史ある雰囲気が印象的でした。割子そばという、何段かに分けられたそばに直接つゆを注いで食べるスタイルが、こちらのメジャーな食べ方とのこと。

f:id:Layzie:20180928172901j:plain

お昼を食べた後、再びサテライトオフィスで開発をした後は、前回も伺った現地の居酒屋、根っこやさんで夕食をいただきました。

今回もコーディネーターの方のオススメで地酒の「王祿の渓」をいただきました。期間中はいくつかのお店を回りましたが、日本海の魚から宍道湖しじみ、そば、地酒などなど、美味しいものに困らない土地だなーという印象が強く、ごはん(とお酒)大好き人間の自分には魅力的なものばかりでした。

前回お邪魔した古民家風オフィス「松江城下」なども、お昼休憩の際に簡単に案内していただきましたが、すでにこちらはサテライトオフィスとして企業が契約をし、稼働を開始しているとのこと。(個人的にはちょっと中を覗いてみたかったので残念...)

松江城を囲むお堀も観光スポットの一つとのことで、最終日には松江城と共にこちらも体験させていただきました。仕事の疲れを史跡巡りで癒やされに来るのも良さそうです。

f:id:Layzie:20180914101033j:plain

定番のお参り

最終日には、プロダクトの成功祈願も兼ねて出雲大社にお参りに行ってきました。(出雲大社は医療の神様とも言われる大国主命をお祀りしており、その出自は因幡の白うさぎという古事記の一節とのこと。不勉強にてこのタイミングで知りました・・・)

当日はあいにくのお天気でしたが、雨の中濡れる静かな出雲大社も厳かな雰囲気があり、パワーを沢山いただいてきました。

f:id:Layzie:20180914134135j:plain

まとめ

以上、簡単ではありますが、松江市でのサテライトオフィスお試し勤務の様子を振り返ってみました。

昨年のブログでも書かれていますが、思ったよりも都内からのアクセスが良いなと思いましたし、街自体もおちついて静かなので(テルサ別館が駅前にあるのに、日中は電車の音くらいしか気にならない)、非常に集中して作業することができました。

また普段一緒に机を並べて作業しているチームから一時的とはいえ離れて作業をしてみて、隔離された環境での集中しやすさを得た一方、やはり面と向かってメンバーと話したほうが円滑なコミュニケーション取れるなということも改めて実感しました。(実際にこの期間中、東京のメンバーに色々動いてもらっていたのに、自分はリモートだったので、もどかしく感じる部分もありました・・・💦)

短期間ですが、個人ではなくスモールチームでリモートワークをしてみて良いところ、対面のほうが良いところを実際に把握できたのも収穫でした。

さいごに

今回のお試し勤務は、松江市役所の土江さんにコーディネートいただき、実現することができました。

スケジュール等々きっちり準備頂いたおかげもあり、滞りなく業務を行うことができましたし、ただただオフィスに籠もって開発をしていただけではきっとわからなかった、松江という場所の雰囲気なども知ることができました。土江さんをはじめ、今回お世話になりました皆様、改めて本当にありがとうございました!

ということで、簡単ではありますが、第二回松江市でのサテライトオフィスお試し勤務のレポートでした。

メドレーでは、エンジニア・デザイナーを絶賛募集中です。ご興味ある方は、こちらからぜひご連絡ください。

www.wantedly.com

iOSDC Japan 2018にメドレーが協賛しました

こんにちは、開発本部の高井です。

メドレーは、去年に引き続き8/30〜9/2に早稲田大学で開催された iOSDC Japan 2018(以下iOSDC)に協賛しました。 みなさんご存知かと思いますが、iOSDCは国内のiOSイベントの中ではtry! Swiftと並ぶ最大級のイベントです。 f:id:medley_inc:20180913175434p:plain

(オンライン診療アプリ「CLINICS」初期開発時からSwiftで実装しています)

CLINICS (クリニクス)

CLINICS (クリニクス)

  • Medley, Inc.
  • メディカル
  • 無料

メドレーは今回、シルバースポンサーとして協賛させていただきました。ブース出展はしていないのですが、スポンサー枠で私が参加してきました。

オープニングのスポンサー紹介は去年に引き続き今年も三石琴乃さんのナレーションでした!豪華です。

イベントの様子

ランチのお弁当はもちろん、朝はドーナツ、夕方のLTが始まるとビールが提供されるなど至れり尽くせりでした。セッションは最大4つが同時に進み、それに加えてアンカンファレンスと特定のテーマを設けたディスカッション企画などもあったのですが、進行も非常に円滑で非常に参加者の満足度の高いカンファレンスではないかと思います。 f:id:medley_inc:20180913175617p:plain

セッション

今年はSwiftのコンパイラのソースからSwiftの機能を解説するようなセッションがいくつかありました。去年はそのようなセッションはなかったのではないかと思います。Swiftがオープンソース化されて3年弱ぐらいになり、日本でもSwiftのコミュニティがより成熟しているように感じました。 また、機械学習などのキャッチーなトピックの話題が少し減って、業務上得た知見や設計についてなどの実用的なテーマが多かった印象でした。 特にViewの設計や実装についてのセッションが面白かったです。やはり、iOSアプリ開発ではView周りの実装が悩みの多い領域ですよね。

ここからは特に気になったセッションをいくつかご紹介します。

MicroViewControllerで無限にスケールするiOS開発

www.icloud.com

UIパーツごとにViewControllerを持たせて、一つの画面の異なる機能を複数のエンジニアで開発しやすいように実装しているというお話でした。一般的な画面単位でViewControllerを持つ実装だと、開発の人数を増やしてもコンフリクトやオーバーヘッドが発生しやすくなり、効率的に開発することが難しくなってしまいます。そのため、開発チームの規模をスケールさせることができないということからそのような方法を取り入れたそうです。

MicroViewControllerを採用したことによって、一つのアプリに20人のエンジニアを充てることができるようになったということでしたが、他にそんなところあるのでしょうか、、、という気がしないでもなかったです。ただ、数人でやっていてもコンフリクトはよく起こりますし(project.pbxproj、、、!)、機能を追加したり、削除したりということを素早く試行錯誤するのには良さそうだなと思いました。

また、ビルドの効率化のためにサンドボックスアプリを用意したり、ViewControllerのテンプレートを作って実装の効率化を図るなど開発効率向上の参考になる取り組みも多かったです。

バイス・OSバージョンの依存が少なく、メンテナンスしやすいビューを作る

ビューの実装について起こりがちな問題とその対処法について紹介したセッションでした。特にレイアウト崩れを防ぐために意識すべきポイントについて、具体的な失敗例を示しながら紹介されていました。

UIコンポーネントのサンプル実装(https://github.com/folio-sec/Folio-UI-Collection/tree/master/Folio-UI-Collection)も紹介されていましたが、CLINICSでも共通UIをコンポーネント化して使っているので、カスタムViewの実装方法やコンポーネントの粒度などが再確認でき、非常に有益でした。また、UIのユニットテストもあり、CLINICSではあまりUI関連のテストは書けていないので参考にしたいと思います。

宣言的UICollectionView

UICollectionViewに複数の種類がある場合(よくあります!)に、宣言的な実装をすることでコードの見通しをよくする方法を提案されているセッションでした。

自分もあのswitch文をまとめようとして、うまくいかなかった経験があるのでとても興味深かったです。すぐに自分たちのコードにも適用できる実用的なセッションでした。

あとはReactorKitのライブコーディングや差分検出アルゴリズムに関連する話がいくつかあり、ちょうど業務でReactorKitとRxDataSourcesを使っているので参考になりました。

差分アルゴリズムの原理について

UITableViewなどで変更があった箇所だけを更新するのに利用される差分検出のアルゴリズムについて解説したセッションでした。IGListKitやRxDataSourcesなどで使われているアルゴリズムAndroidのDiffUtilで利用されているアルゴリズムの原理について紹介されていました。ライブラリを使っていると、利用方法は分かるけど内部でどう動いてるかはあまり理解していないということも割とあると思いますが、その辺りを理解できると用途に応じて最適なライブラリを選択できたり、より効果的な使い方ができそうだと思いました。

5000行のUITableViewを差分更新する

差分更新のライブラリを使って多数行を更新した際に発生した問題の紹介とその原因を特定して、改善した内容についてのお話でした。採用していたライブラリはDifferでしたが、ライブラリごとの特徴やInstrumentsによるボトルネックの特定などが参考になりました。

まとめ

iOSDCは去年に引き続きの参加だったのですが、今年のセッションもどれも興味深く、勉強になることが多かったです。またセッションの裏で特定の技術についてディスカッションするコーナーができるなど年々充実してきているように感じました。また、LTのときの会場の一体感はとても良いなあと思いました。

f:id:medley_inc:20180913175405p:plain 弊社のアプリ開発でもそういった知見などを活かして開発していける仲間を引き続き募集しています! 興味がある方は、こちらからご連絡ください。

www.wantedly.com

Kotlin Fest 2018にメドレーが"ひよこスポンサー"として協賛しました

こんにちは、開発本部平木です。去る8/25に行われた、日本初(!!)のKotlinの言語カンファレンスであるKotlin Fest 2018に弊社は"ひよこスポンサー"として協賛させていただきました。

公式Twitterで紹介された様子

今回スポンサーチケットで参加させていただいたので、つれづれとレポートを書いてまいります。

メドレーとKotlinの関わり

なぜ、メドレーがKotlin Festに協賛したかというとKotlinを使ってAndroidアプリを作っているからになります。

オンライン診療アプリ CLINICSのAndroid版で使っています。 play.google.com

Androidで正式にKotlinサポートすることがアナウンスされてからできるところをJavaからKotlinに書きかえていっています。

方針としては、ムリに全部のソースをKotlinにするという形ではなく改修などで触ったソースで余力があれば書きかえるというスタンスでやっていますが、それでも現在50%弱のソースがKotlinになってい ます。

Kotlinで書いた場合にJavaよりも可読性や堅牢性が上がるというメリットを実感していたところ、今回のKotlin Festの開催を知り、協賛させていただいたという次第です。

イベントの様子

会場は東京コンファレンスセンター品川でした。自分は初訪問だったのですが、設備も充実しており良い会場だと思いました。

無限に出てくるかのようなコーヒー・飲み物とお菓子が大変ホスピタリティを感じさせます。

スポンサーブースも盛況で、なかでもYahoo! Japanさんのモブプロ実演や、CyberAgentさんのKotlinクイズなどが人気を集めていました。

f:id:medley_inc:20180903111847j:plain

M3さんのブースでいただいたロゴ入りじゃがりこ

f:id:medley_inc:20180903111902j:plain

セッション

セッションは2セッションが同時に行われるという形式でした。

自分が参加したセッションのみですが簡単な感想でご紹介します。

Kotlinで改善するAndroidアプリの品質

speakerdeck.com

JavaからKotlinへの書きかえを考えた場合のアプリの品質を主軸にしてメリットを紹介する…というものでした。

Effective Javaの中で紹介されている項目について、Kotlinではどうなるかという視点での紹介は大変興味深かったです。

KotlinはJavaよりもNull安全を始め、堅牢だというイメージがありましたが、こうしてJavaするべきであるという項目がKotlinでは言語仕様レベルで対応されていることが多いというのを目の当たりにすると、さらに頼もしく思えるというようなセッションでした。

Kotlinアプリのリファクタリングポイント

www.slideshare.net

既に存在するKotlinのコードをどのような指針でリファクタリングしていくかというセッションです。

「こういうときに書き方複数あるけどどうしよう…」という例ばかりだったので、自分達のアプリでもすぐに使えるような実践的なセッションでした。

中でもいかにMutableなプロパティを避けるかというフローチャートは、理路整然としていてこれからKotlinを書いていく上でかなり参考になりました。

Kotlin linter

speakerdeck.com

自分の場合、JavaScriptなどでもわりとLinterは興味がある分野だったのですが、KotlinのLintについて はandroid-lintしか使ったことがなかったので、紹介されているLinterの情報が参考になるセッションでした。

KotlinのLinterもやはりASTを触らないとオリジナルルールの設定ができないのかーなど普段触れていなかった知識が得られて有意義でした。

が、KotlinのASTはPsiViewerというIntliJ プラグインくらいしか対応してなさそうで、自分で設定するとなると若干つらそうだなという印象でした。

AST Exploreあたりで気軽に試せると良いですね。

Kotlinで愛でるMicroservices

speakerdeck.com

サーバサイドKotlinをMicroserviceでガンガン使用するために必要なエッセンスがまとまったセッションでした。

元々使っていたGoとの使いわけや、実際どのようなアーキテクチャで作って、デプロイや監視など運用をどのようにしているのかなどがコンパクトにまとまっていて大変分かりやすかったです。

サーバサイドKotlinは弊社で使う予定は現状まだ無いのですが、このセッションを見た限りミニマムに始めることが可能な感じに思えたのが収穫でした。

まとめ

Kotlinの日本初のカンファレンスでしたが、来場者もかなり多くKotlinエンジニアの裾野が広いなという印象でした(セッションによっては立ち見も出ていました)。

またセッションも初級から上級まで幅広く取り揃えられていたので、飽きることなく楽しめましたし、来年以降も続いていってほしいと思ったカンファレンスでした。

メドレーでは今後も色々なTechカンファレンスをスポンサードして参ります。色々な場所で、お会いできたらと思います!

HTTP Cacheで求人サイトのスピード改善を試してみた話

こんにちは、開発本部の楊です。メドレーの社内勉強会「TechLunch」で、前回は、Reactの基本を紹介しましたが、今回はHTTP Cacheで、医療介護求人サイト「ジョブメドレー」のスピード改善ができないか検討した話について、共有しました。

なぜHTTP Cacheについて話すことにしたのか

ジョブメドレーには、医療機関や保育園、介護施設などさまざまな事業所の求人が掲載されています。現在、14万を超える事業部の求人が掲載されており、かつ事業所側で求人原稿を修正することもできるため、求職者側が閲覧するスピードについては、いつも意識して改善に取り組んでいます。

その試行錯誤の中で、HTTP Cacheを使って改善する方法について、実現性など含めて検証することにしました。

今回は、あるページをモデルケースに試してみました。このページは、PCは平均250ms、モバイルは180msになっています。

f:id:medley_inc:20180824171410p:plain

ユーザが該当のページを見たときに「内容に更新がないときはブラウザ側のキャッシュを利用してスピードを最適化する」「ページが更新されていたら、最新の内容をすぐユーザへ反映する」という要件でスピード改善されることを目指すことにしました。

HTTP Cacheとは?

Google Developersでは以下のように説明されています。

ネットワーク経由で情報を取得するには時間もコストもかかります。レスポンスが大きいと、クライアントとサーバ間のラウンドトリップを何度も繰り返す必要があるため、レスポンスが利用可能となってブラウザで処理できるようになるまで時間がかかります。さらに、ユーザ側ではデータの通信コストが発生します。そのため、前に取得したリソースをキャッシュに保存して再使用できることは、パフォーマンスを最適化する上で非常に重要です。

この機能はほぼ全てのブラウザに対応しており、HTTPヘッダーでCacheControlETAGLast-Modifiedなどを利用して、リソース更新するタイミングなどを細かくコントロール可能です。

RailsでのHTTP Cache

expire_inでキャッシュ

1時間キャッシュしたい場合は、コントローラにコードを一行書けば大丈夫です。

expires_in(1.hour)

これで問題なく1時間キャッシュされるのですが、「ページが編集されたら即時にユーザへ反映する」という要件を満たしてはいません。

ETAGを利用

では「ページが更新されたら最新の内容をすぐユーザへ反映して、更新がない時はブラウザ側のキャッシュを利用してスピードを最適化する」を行いたい場合はどうすればよいでしょうか。 ここではETAGを利用しようと思います。

ETAGはページの内容によって、ユニークな文字列を作成して、ブラウザ側でページの更新あるかどうかを判定するものです。

ETAGはどう作成されているか

RailsのデフォルトではHTTP Cacheを有効になっていて、ETAGを自動的に作成しています。

header['ETag'] = Digest::MD5.hexdigest(body)

上のコードのようなイメージで、レスポンスBodyからETAGを作成します。

つまり、サーバから返されるHTMLソースの内容が毎回同じであれば、ブラウザは前回キャッシュした内容を読み込むようになります。

サーバレスポンスタイムの短縮

今回の対象ページの機能としてサーバ側では主に二つの部分で時間がかかります。

  1. DB、Redisなどで画面表示が必要なデータ取得、ロジック処理
  2. クライアントに返すHTMLのレンダリング

対象ページは「HTMLのレンダリング」する時間が長かったので、それを改善できれば、サーバレスポンスタイムを一気に短くできます。

画面に表示する必要なデータに変換がなければ、HTMLのレンダリングをせずにレスポンスを返す機能がないかをさらに調べました。

fresh_whenを使う

名前の通り、いつ画面更新するかをコントロールするメソッドです。

データベース中の該当データが更新されたら、新しいETAGを作成するコードは以下のようになります。

fresh_when(etag: [@job_offer, @job_offer.facility])

modelからETAGをどう作成するか

modelのcache_keyからETAGを作ります。

該当ページで使用するjob_offerモデルのcache_keyは"job_offer/5-20071224150000"のような感じになります。

modelのidとupdated_atの組み合わせでユニークなcache_keyを作成しています。

Railsの該当コード

def cache_key(*timestamp_names)
  case
  when new_record?
    "#{model_name.cache_key}/new"
  when timestamp_names.any?
    timestamp = max_updated_column_timestamp(timestamp_names)
    timestamp = timestamp.utc.to_s(cache_timestamp_format)
    "#{model_name.cache_key}/#{id}-#{timestamp}"
  when timestamp = max_updated_column_timestamp
    timestamp = timestamp.utc.to_s(cache_timestamp_format)
    "#{model_name.cache_key}/#{id}-#{timestamp}"
  else
    "#{model_name.cache_key}/#{id}"
  end
end

自前の作成したクラスからETAGをどう作成するか

modelだけではなく、自前で作成したクラスでデータ管理をしてるところもあります。 そちらでの実現方法も試してみました。

そこでETAGを作るコードがRailsでどうなっているか追ってみました。

Railsの該当コード

def retrieve_cache_key(key)
  case
  when key.respond_to?(:cache_key) then key.cache_key
  when key.is_a?(Array)            then key.map { |element| retrieve_cache_key(element) }.to_param
  when key.respond_to?(:to_a)      then retrieve_cache_key(key.to_a)
  else                                  key.to_param
  end.to_s
end

このように、自作クラスにcache_keyのメソッドを定義したら、その結果からETAGが作成されるようになっています。このメソッドを使って、ユニークな値を返せば大丈夫です。

ここまで調査したことを踏まえて、該当ページをHTTP Cacheで実装をしてみました。以下が該当の疑似コードになります。

class JobOfferBrowsingHistory
  def cache_key
    # return job offer ids
  end
end

fresh_whenに渡す

@user_history = JobOfferBrowsingHistory.new
fresh_when(etag: [@job_offer, @job_offer.facility, @user_history])

テスト

開発環境で該当ページを2回ロードしました。 1回目は1200ms、キャッシュが効いた2回目は130msになりました。すごく早いです! もちろん、ページ内のデータが更新されたら、最新の内容がページに反映されるようにもなっています。

これで「ページのデータが更新されたら、最新の内容をすぐユーザへ反映する」「更新がなければHTTP Cacheを返す」という状態が実現しました。

課題

  • 実装ミスで、更新が必要なのに、更新されない問題が起こりえる
    • 例えば、画面に新しいmodelを追加したが、fresh_whenに渡すのを忘れたとか
  • 問題なく更新されることを保証する仕組みの実装

まとめ

HTTP Cacheを利用して、ジョブメドレーのスピード改善を検討してみた調査過程を紹介しました。

expire_inETAGの作成方法など色々調べて、最終的にfresh_whenで実現できましたが、運用していく上での課題については、引き続き検討して行きたいと思います。