センサクッキング 技術検証とユーザー体験検証

こんにちは.スマートキッチン事業部の鈴本 (@_meltingrabbit) です.
今回は 失敗しない範囲で自分の料理を創作したい という願望を叶えられるかもしれないセンサクッキングについてです.

はじめに

日々の料理を可観測に,可制御に

唐突ですが,私は,料理とは複雑系であり,調理とは創造的であると思っています.
そのような中で,より日々の調理をより楽しくしたい,もっと変化が欲しい,ちょっと実験的な要素を加えてみたい,と考える人は多いのではないでしょうか?
かといって,ただでさえ忙しい日常において,ちょっと変わったことをして失敗して,時間やお金,食材を無駄にしたくないと思うのも当然です.
そんなわけで,調理中に料理の内部状態量が把握でき,失敗を防ぎつつちょっと冒険できるようなシステムの技術検証とユーザー体験の検証についての話をしたいと思います.

センサクッキング

f:id:meltingrabbit:20191211172344p:plain
センサクッキング イメージ図

センサクッキングとは,その名の通り,料理中に多種多様なセンサを駆使して,料理の内部状態量を把握できるようにしようとするものです.
これによって,

調理中の料理の内部状態量が把握できる
 ↓
自らの行為・操作がどのように料理に影響するのかを理解・学習できるようになる
 ↓
データを蓄積すれば,ひいては自分の料理の好みや傾向を把握できるようになる
 ↓
日々の料理がもっと楽しく創造的になる

となることを期待しています.

「あれを試したらどうなるだろう?」「 今度はこういうふうに作ってみよう! 」というように,自分で能動的に考えた操作を自由に試せ,その自身の体験から得た知識が増えることでさらに創作への意欲が湧くという,好循環が生まれると考えています.

さらに,副次的な結果として,例えば,

  • 低温調理を試してみたけど,温度がわかるので火が通るのを確認できた
  • 使ったことのない調味料を使ってみたが,塩分濃度がわかるので味が濃すぎることはなかった

といったように,調理を失敗しないためのセーフティになる,という側面もあります.

既存調理器におけるセンサ利用

そもそも,既存の調理器においては,どのようなセンサが使われているのでしょうか?
例えば炊飯器には,本炊きから蒸らしまでの温度プロファイルを制御するために温度計がついています.
さらに最近では高機能なものも増えてきており,所望の温度へ素早く正確へ制御するための質量センサ(三菱IHジャー炊飯器など)や,安全のための圧力計などがついているものもあります.
最近流行りの自動調理器 (例えばヘルシオ ホットクック) などは,追加でかき混ぜる機構などが追加され,料理のレパートリが格段に増えましたが, 取り付けられたセンサの種類が格段に増えた,といったことは耳にしませんし,私達ユーザーがそのセンサ情報を見ることはできません.

既存の料理用センシング機器

最近は “スマートキッチン” という言葉もよく聞くようになりました.
それに伴い,様々な調理家電が販売されています.

なかでもスマートオーブンは,肉に温度計を刺し,内部温度をコントロールしながら加熱することができます.
このような家電はいくつかありますが,基本的には温度しか測定できないものが多い印象です.

f:id:meltingrabbit:20191211175717j:plain
june smart oven

また,健康志向の高まりも相まって,手軽に塩分や糖度を測れるデバイスも販売されています.
ただし,調理中に連続的に測定するといった使い方は想定されていません.

f:id:meltingrabbit:20191211180329j:plain
TANITA塩分計

今回の目的

さて,今回の検証の目的は,大きく次の2点です.

フィージビリティスタディ

フィージビリティスタディ,つまり技術的な側面から実現可能性を検証します.
システムとしては,

  • 調理器具に多様多種なセンサを取り付ける
  • 取得されたデータを無線で送信する
  • リアルタイムに可視化する

という要素によって構成されます.
最も重要となるのは,調理の邪魔にならず,安全にリアルタイムにセンシングできるかです.

  • 液体がある部分は100度以上には上がらないので,温度要求が緩和される
  • そもそも液体があることを前提としたセンサが多い(濃度計など)

などの理由から,今回はモデルケースとして鍋を使った調理に焦点を当てました.
そのために,簡単なプロトタイプ製作を行いました.

また,料理での変化がセンサできちんと検出できるかも検証ポイントです.

ユーザー体験の検証

  • 調理中にセンシングをするとどのように測定値が変わるのか
  • 自分の行為がどう測定値に影響するのか
  • そもそも調理中にセンシングをして楽しいのか?(最も重要)

などを検証するために,プロトタイプで作ったシステムを用いて実際に調理実験を行いました.

やったこと

システム構築

まずはじめに,
センサ → マイコン → 無線データ転送 → リアルタイム可視化
のシステムを構築しました.

特にこだわりもないので,マイコンは(社内に100台以上転がっていると噂の)M5Stackを,無線はBLE (Bluetooth Low Energy) を,可視化ツールは自作Webアプリとしました.

f:id:meltingrabbit:20191211224657j:plain
とりあえず熱センサとガスセンサを鍋に取り付けた.

f:id:meltingrabbit:20191211224711j:plain
システム全景.右側のPCにリアルタイムでグラフ化される.

安価に購入可能なセンサをシステムへ統合

秋月電子などの電子部品屋に売っているようなセンサをひとしきり買いあさり,鍋に取り付け,いい感じならそのままシステムへ統合していきました.
買ったセンサは,
温度計(熱電対),pH計,ガス(一酸化炭素,二酸化炭素,アンモニア,エタノール,etc...)センサ,蒸気センサ,色(RGB)センサ,赤外線6バンドスペクトルセンサ,マイク,ピエゾ素子(振動センサ),転倒センサ,加速度センサ,土壌水分センサ,などなどです.
個人的に欲しかった粘度センサは,高価すぎて断念.

f:id:meltingrabbit:20191211225325j:plain
買いあさったセンサたち

既存の調理用センサを分解・改造しシステムへ統合

塩分計と糖度計が欲しかったのですが,いい感じにセンサ部分だけを手に入れることができなかったため,既存のデバイスをばらして使うことにしました.

塩分計はばらしてみると比較的単純な回路だったため,センサ部分の電極をM5Stackへ引き出し,その信号から塩分濃度を推定させました.
また,塩分計自身のスイッチもM5Stackから制御できるようにし,任意のタイミングで測定可能となりました.

f:id:meltingrabbit:20191211231011j:plain
バラされ,センサの電極を外部に引き出される塩分計

f:id:meltingrabbit:20191211231042j:plain
センサ部分に加え,スイッチ部分もM5Stackに乗っ取られた塩分計

対して,糖度計はセンサ部分が堅牢でアクセス不能だったため,備え付けられていた液晶画面への信号を読み取り,測定値を取得するようにしました.
こちらも塩分計同様,M5Stackからスイッチを制御して測定します.

f:id:meltingrabbit:20191211231441j:plain
バラされ,配線をつけられた糖度計
f:id:meltingrabbit:20191211231452j:plain
液晶画面は,1桁につき数字7セグメント+小数点の8セグメント,それが4桁分ある.なお,ダイナミック駆動という信号方式を採用しているので,セグメント数と比較すると信号線数は大幅に少ない.
f:id:meltingrabbit:20191211231447j:plain
無事,液晶に表示されている数字を読み込めた.

センサクッキングを体験

ひとしきりのセンサを鍋に取り付け終わったところで,さっそく調理で使ってみました,

配線がすごいことになっていますね.
きちんと回路と構体を作ればもっときれいに小さく作れますが,今回はプロトタイプであり,検証として使えるものを手早く作ることが目的であったので,これで良いのです.

よだれ鶏,筑前煮,卵スープなどを作ってみました.

f:id:meltingrabbit:20191211233540j:plain
多様多種なセンサが取り付けられた鍋.右の黒いのはバッテリ.

f:id:meltingrabbit:20191211233544j:plain
調理風景

f:id:meltingrabbit:20191211233041p:plain
センサから無線で受け取ったデータを可視化する画面

技術的なフィージビリティ検証結果と考察

全体的なフィージビリティ

f:id:meltingrabbit:20191216011807p:plain
センサクッキング デバイス コンセプト図

まず,システム全体としての技術的な実現可能性についてです.
実現可能性は十分ある一方,部分的に難しい点がありそうです.
 

目的で挙げた,

  • 取得されたデータを無線で送信する
  • リアルタイムに可視化する

の2つはすでに成熟した技術があり,技術的ハードルは低いです.

  • 調理器具に多様多種なセンサを取り付ける

に関しては,検証が必要です.

センサ自体は,上図のように大きく以下の4つに大別されます.

  • プローブ系:液面にセンサ部分を突き刺して/浸して使う.濃度計など.
  • 非接触系:鍋の縁にクリップのように取り付ける.色センサなど.
  • 環境系:調理環境に設置するもの.気温計や湿度計など.
  • 制御履歴系:コンロなどの調理器具の入出力履歴など.

要求が厳しいのが上の2つであり,

  • 液面に触れたり油はねなどで壊れないための防水性
  • 加熱調理に耐えられる耐熱性
  • 料理を邪魔しないための小型化

が求められます.

まず,小型化に関しては容易でしょう.
センサ部分を除いて,マイコン,無線モジュール,操作用のスイッチ2個程度の回路であれば,2cm四方程度の回路で十分収まるはずです.

耐熱性も,鍋系であれば,センサが触れる液面の温度は100度以下なので,そこまで問題にならないはずです.
ICなどは100度でも動くものが多いです.
しかし,はんだは200度くらいから溶ける可能性があるので,揚げ物などは対策を施さないと難しいでしょう.
また,急激な温度変化はクラックの原因となるので,熱がゆっくり伝わるようにといった対策は必要かもしれませんが,そこまで難しくないはずです.
プローブ系であれば,回路は熱環境のよい上部に取り付け,センサ部分(電極部分)のみを高温にさらされる部分に配置するなどの工夫も可能です.

防水性は,色センサなどは透明な容器で覆ってしまえばよく,熱電対などセンサ部分が電極のものはもともと問題がない一方,ガスセンサなどセンサ部分が暴露しているものはもしかしたら難しいかもしれません.

様々なセンサを使ってみてわかった,気をつけなければならないこと

各センサについての具体例は後述しますが,様々なセンサを実際に使用してみて気づいた,実際に調理用センサを作り込んでいく上で課題になりそうなことについてまとめておきます.

まず,調理環境という極めて(センサにとって)劣悪な環境にセンサを置くことについてです.
耐熱,防水についてはすでに述べましたが,他にも考えなければならないことは多々あります. 例えば,キッチンによっても明るさなどの環境は異なるし,同じキッチンでも状況によって環境は変わります.
さらにノイズ源が多いという問題もあります.例えば,

  • 人が近づいたことによる外乱
  • 鍋をかき混ぜた事による外乱
  • IHクッキングヒーターの高周波外乱

などです.

一方で,センサクッキングの目的を考えると,規格化された使用を想定する必要がない,とみることもできます.
どういうことかというと,大事なのは自分の行為・操作が料理にどのような影響を与えるのかがわかればいいので,たとえセンサ値が急に変わったとしても,それが窓を開けたことによるのか,センサを覗き込んだことによるのか,鍋をかき混ぜたことによるのか,自分がわかれば問題ない,という考え方です.

ただ,そうであったとしても,再現性の高さ(つまり,同じように調理したら,同じような測定結果になること)が重要であることには変わりありません.
この再現性を担保するのに重要なのがセンサの校正作業なのですが,これがユーザビリティを下げるのは間違いなさそうです.
特にpH計などはいちいち標準液にセンサを浸すなど,めんどくさい作業が多いのが実情です.
(高校生時代に次のようなことをよく言われた.機械(センサのこと)とは,目盛りを正確に刻むのはとくいだけれど,絶対値を決めることはできない.例えば,機械に0度と100度を教えてあげれば,その間を0.1度や,0.001度などといった精度で刻むことは可能だけれど,機械が自ら基準値を知ることはできない.)

他にも,人間の味覚とセンサの物理的な定量評価のギャップも注意しなければならないと考えます.
例えば,温めると塩味をより強く感じる,など,人の味覚は様々な要因に影響されます.
同じ塩分濃度でも,それを食べたときの感じ方は様々であることを考慮に入れて,センサの測定値を解釈していく必要がありそうです.

個別センサについて

ここから,いくつかの個別のセンサについて,使用感を取り上げてみたいと思います.

温度計

熱電対を使用しました.
これは,2種類の金属の接合部分に温度差があると起電力が生じることを利用したセンサです.
測定したい箇所に熱電対を貼ればいいだけなので,極めて容易であり,耐熱性や防水性もバッチリです.

今回使ってみて一番良かったのは,リアルタイムでグラフ化することのメリットが結構大きいということです.
とりわけ,温度グラフの傾きがわかるのが良かったです.
傾きから,「この火加減だとどれくらいで湧きそう」とか,「あと数分は目を離していても問題ない」といったことが読み取れました. また,他のセンサログと組み合わせることで,「この温度のタイミングで調味料を入れるといい」といった発見も期待できるかもしれません.

なお,今回測定したのは鍋の表面や煮物の液体部分であって,食材の内部温度などではありません.

これは,対象に光をあて,その反射光の強度をRGBそれぞれについて取得できるセンサです.

料理中,かなりダイナミックに変わって楽しかったです.
沸かした水に塩を入れただけでも変わりました.
ただ,センサ直下に何があるかや,環境光によって値が大きく変わってしまうので,調理としての再現性はあまり望めないかもしれないです.

塩分計

今回使用したのは,交流電圧を印加して液体の伝導率を測ることで,溶解している電解質分量から塩分濃度を推定するセンサです.
しかし,鍋の素材によってセンサ出力にものすごいノイズが載ってしまう,などといった難しい点がありました.

pH

pHの計測にはガラス電極法という計測手法が主流であり,今回もこの類のセンサを使用しました.
これは特殊なガラス膜の両側でpHが異なると,その間に起電力が生じることを利用しています.

使ったセンサの問題かもしれませんが,センサの時定数が分オーダーと,かなり大きかったです.
「この調味料を入れたらpHがこうなった」などの変化を見るのは厳しいかもしれません.
センサの時定数と料理の時定数の関係は,他のセンサでも鍵となる可能性があります.

ガスセンサ

MEMSタイプのガスセンサを使用しました.

隣で炒めものすると値が跳ね上がったりしました.
ただ,取得して嬉しいかと言われると...?

糖度計(屈折計)

溶液の屈折率を測定することで,糖度を推定するセンサを用いました.

糖度を直接測定しているのではなく.屈折率を測定しているため,原理的に糖分以外にも反応してしまうようです.
たとえば,アルコール計も屈折計なので,センサ値が糖によるものかアルコールによるものかの切り分けはできません.
このように,ある物理量に影響を与える料理のパラメタが複数ある,といったことは他のセンサでも起きうるので,難しい問題です.

ピエゾ素子(振動センサ)

ピエゾ素子,別名圧電素子を用いました.
これは,特殊なセラミックに圧力を加えると電圧が生じる素子なので,例えば,薄いピエゾ素子を小刻みに振動させると,局所的に応力が生じ,起電力が生じます.

沸騰の様子などを捉えられるか? と思っていましたが,なぜかIHクッキングヒーターの高周波外乱をもろに拾ってしまい,全然だめでした.
IHクッキングヒーターがOFFのときは,液面の揺れなどを捉えることができて,なかなか面白くはありました.

鍋以外の調理への展開

鍋料理以外への展開についても少し考察しておきます.
非接触系のセンサであれば,そこまで問題にならないはずです.
ただし,長時間高温にさらされ,センサ内部まで温度が上がってしまう可能性や,長時間にわたって油はねを受けてセンサ部分が汚れたりする可能性は否定できません.
また,濃度計などのように水分があることが前提のセンサは基本的に使えないので,使えるセンサの種類は減ってしまうかもしれません.

他にも,小型化や耐熱性が難しいとは思いますが,米粒サイズの9軸センサなどをチャーハンと一緒に炒めて,米の振る舞いなどを知れたら面白いかもしれませんね.

センサクッキング ユーザー体験の検証結果と考察

2つ目の検証目的である,ユーザー体験についてです.

楽しいのか問題

そもそも,「センサクッキングをしてたのしいのか?」という問題があります.
今回,数回に渡って実際にセンサクッキングを体験しましたが,基本的には楽しかったです.
(基本的には,とあるのは,自分の作ったシステムが正常に動作するかに神経を使っていたため)

ただし,この楽しさが持続するかは別問題です.
楽しさが持続しなければ,「はじめに」の項で述べた,自らいろんなことを試し,いろんなことを発見し,理解し,日々の料理をより創造的にするサイクルは回せません.
最初は何もかもが新鮮で,目新しく面白いかもしれませんが,だんだん飽きてくる可能性も十分にありえます.
今回は数回使っただけですが,今後はデバイスをきちんと作り込み,例えば数週間使ってみるといった長期的な検証が必要になると考えます.

あたりまえ or なにもわからない の二極化

センサによって,“あたりまえ” と “なにもわからない” に二極化する傾向がありそうでした.

例えば,温度計や塩分濃度計などはわかりやすく,「加熱したから温度が上がった」や「調味料を足したから塩分濃度が上がった」と,比較的自明な結果が得られるセンサと,赤外線スペクトルセンサや屈折計(アルコールにも糖度にも感度がある,要因が複合的なセンサ)のように,値の変化がよくわからないセンサのように,体験が二極化する傾向が見られました.

前者は,言い方を変えると驚きがなく,つまらないと感じるかもしれません.
一方で,自分の操作がどのように料理へ影響するかのイメージがつかみやすく,学習効果という意味合いではよい体験が得られるかもしれません.

対して後者は,意外性という意味では面白いかもしれませんが,「なんか値が変わったんだけど,なんで?」「これは何を意味するんだ?」のように,値はみれるが,結局何なのかよくわからず理解につながらないかもしれません.

あくまで主観ではありますが,ざっくりと今回使ったセンサを体験でプロットしてみました.

f:id:meltingrabbit:20191216015927p:plain
センサ ユーザー体験マップ

ユーザー体験のさらなる検証

「楽しいのか問題」の項でも述べましたが,ユーザー体験のさらなる検証には長期の使用に耐えられるデバイスをきちんとつくり,長期間使い続ける必要があると考えます.

  • 何度も使っていくことによって,本当に自分の中で相関関係を見いだせるのか?
  • 前回の調理との差分がわかることで,料理の感度解析ができるのか?

などといった体験の検証には,もう少し時間が必要です.

また今回の検証で,センサクッキングで料理の内部状態量の可観測性が向上する可能性を見出すことはできましたが,そこからどう自分の料理を創造的に変えていくかという制御性の部分については検証できませんでした.
この検証も同様に長期使用試験が必要であると考えます.

まとめ

センサクッキングのプロトタイプを作り,実際にそれを体験しました.

それによって,

  • 調理環境のセンサ情報をリアルタイムで可視化することは可能
  • 環境耐性の高い温度計などは事前の想定通り有益な情報が容易に取得できた
  • 調理中に様々なセンサ値の変化が観察できた

などは概ね事前想定通りでしたが,

  • 繊細なセンサなどは調理環境で使用するために工夫が必要
  • 調理環境は想像以上にノイズ源(特にIHクッキングヒーター)が多い
  • センサの校正も含めた再現性の担保は難しい
  • 調理中の些細な変化や急峻な変化にセンサの精度や応答速度がどこまで追従できるかなどの検証は別途必要
  • センサ値の変化が理解しづらいものもある

といった,新たな知見が得られました.

また,センサクッキングによって,料理の内部状態量の可観測性は向上しそうだけれど,そこから複雑・膨大な料理の内部状態量を正しく理解できるか,についてはもう少し検証が必要そうです.
例えば,塩分濃度が◯◯なので,この食材の硬さが△△になって,味が□□になる,と正しく知識化でき,そしてそれをコントロールできるかは,また別問題です.
ただし,センサクッキングの体験がこのような理解への手助けとなり,自身の料理に対して何かしらのフィードバックをもたらす可能性は大きそうです.

個人的には,結構面白かったので,きちんと作り込み長期的に使ってみたいです.
そして,適切にデータを蓄積していけば,好みや調理の傾向以外にも,思っても見なかったことがわかるかもしれません.

【開催レポ】Cookpad Tech Kitchen #22 決済基盤の最新事情

こんにちは。ユーザー・決済基盤部の大石です。 2019年11月27日にCookpad Tech Kitchen #22 決済基盤の最新事情を開催しました。

f:id:eisuke-oishi:20191212180318p:plain

クックパッドの技術的な知見を定期的にアウトプットすることを目的とする本イベントの22回目のテーマは「決済」ということで、我々ユーザー・決済基盤部から宇津三吉大石 が登壇し、クックパッドの決済基盤での取り組みについて発表させていただきました。

発表プログラム

大石 英介「クックパッドにおける決済基盤の歴史とこれから」

まず最初に大石からクックパッドの決済基盤 Financier の変遷とユーザー・決済基盤部という決済基盤の運用や開発を行う部署としてなぜ独立しているのか、そしてこれからの展望について話させていただきました。

クックパッドの決済基盤は日々進化しており、その中でもアプリ内課金のサポートが一つの大きな転機であり、このあとの宇津、三吉によるアプリ内課金に関する発表につながるイントロダクションとなりました。

宇津 宏一「アプリ内課金の最新事情 クライアントサイド編」

次に宇津より、決済基盤とアプリ内課金の機能をつなぐクライアントライブラリ Cusine を中心に、決済に関するクライアントライブラリの技術スタックや導入理由などを発表いたしました。

三吉 貴大「アプリ内定期購入における状態管理と"通知"の活用」

最後は三吉より、決済基盤で実装されている Google Play real-time developer notification / Apple App Store server-to-server notification を活用したサブスクリプションの状態管理について話させていただきました。

Q&Aセッション

最後に登壇者を交えてQ&Aセッションを行いました。いくつかの質問をピックアップして紹介致します。

決済部分専門のチームだと新規開発よりも運用工数が増えていくことで増してくるマンネリ感。打開策としてどういったことをやっていたりしますか?

運用に関してはできるかぎり自動化や利用者が解決できるようにして、人間によるオペレーションや運用負荷を抑えるようにしています。いまのところは運用に忙殺されてなにもできないということは避けられてると思います。

APIを扱うSDKはイメージが着くが、決済や認証などはアプリのライフサイクル/UIのどこでその処理を行うか?が重要だと思っている。が、そのSDKを呼び出すタイミングなどはどう制御しているのか?(導入時に支援としてモブしたりとか??)

SDKにおいては実装ガイドラインも提供しています。ガイドラインでは導入時に実装すべき事と呼び出しタイミングについても触れられており、これを元に実装して頂いています。もちろん、モブプログラミングのように直接導入支援を行ったケースもあります。

感想

決済をテーマにしたイベントというのは他のテーマに比べて多くはない印象でどうなるか不安もありました。しかし参加者の皆様の多くが決済の実装や運用に関わっていらっしゃったようで、懇親会では突っ込んだ質問が飛び交い決済に関わるエンジニア同士の一体感を感じました。 参加者の皆様の取り組みなどもお聞きし、我々の決済基盤もまだまだやるべきことがあるなと刺激にもなりました。今回お話した内容はごく一部ではあるのでまた開催できればと企んでいます。

最後に

我らユーザー・決済基盤部は決済あるいはユーザー管理や認証・認可に興味のあるエンジニアを募集しております。まずはお話だけでもという方も歓迎ですので、下記より応募ください! https://cookpad.wd3.myworkdayjobs.com/ja-JP/jobs/job/-/_R-001685

cookpad storeLive のクライアントアプリ開発の裏側

こんにちは。メディアプロダクト開発部の柴原(@nshiba310)です。 趣味は Destiny2 というゲームです。

普段は cookpad storeLive(以下、storeLive)のクライアントサイド(AndroidTV)の開発を行っています。 本記事では storeLive のクライアントサイドの開発についてご紹介したいと思います。

storeLive とは

スーパーマーケットの店頭に設置した縦型55インチの大型サイネージで、著名人や料理研究家による料理デモンストレーション映像をLiveや収録動画で提供するアプリです。
プレスリリースはこちら

スーパーの担当者はまず storeLive が置かれている売り場に適した再生したい動画を選択します。 storeLive は担当者が動画を止める操作をするまで選択された動画をループで再生し続けます。
また storeLive では土・日曜日など、比較的人が集まりやすいタイミングでライブ配信を行うことがあり、ライブ配信が始まるとアプリは自動的にライブ閲覧画面を起動し、終了すると自動的に前に再生していた動画再生画面に戻るようになっています。

データの自動更新

storeLive は通常のアプリと違い基本的に人間の操作が動画の選択時しかありません。
そのため、ライブ閲覧画面の起動や動画情報の更新などは自動で行う必要があり、 storeLive ではポーリング機能を実装しライブ配信や動画の情報を自動的に更新しています。

仕様としては、

  • サーバーリクエスト時にレスポンスに次回実行時間が含まれていた場合にはその時間で次の実行を登録
    • ライブ配信が近づいてきたら高頻度で情報の更新を行うため
  • デフォルトは10分間隔で実行
    • ライブ配信を本番より15分ほど先に開始しておきクライアントから10分間隔でアクセスすることにより、必ず全ての端末でライブ閲覧画面を起動させる
    • 高頻度リクエスト時は1分間隔で実行されることを想定

の2つがあり、これを実現するために storeLive では AlarmManager を採用しました。

AlarmManager を採用する理由

近年の Android アプリ開発において、定期実行処理を実装する場合には JetPack Components の WorkManager を使うことが候補としてあがってくると思います。 しかし、 WorkManager には以下の制限があり今回要求されている仕様を満たすことができないため使用することができませんでした。

  • 最低の繰り返し実行間隔は15分
  • 厳密な実行時間は保証されない
    • 実行間隔は WorkManager に設定した時間間隔の中で端末の状態が最適かどうか(Doze Mode や WiFi 接続している等)を判断し最適なときに実行されるため常に何秒/何分後に実行される、という保証ができない

一方で AlarmManager には setAlarmClock() というメソッドがあり、これを用いると1秒単位で実行時間を設定でき、きちんと設定した時間に発火してくれます。

WorkManager にはリトライ機構もあり可能なら使いたかったですが、今回は仕様に合わず AlarmManager を採用しました。

以下の例では 100 秒後に発火するアラームを設定しています。

val triggerTimeSec = 100

val calendar = Calendar.getInstance().apply {
    timeInMillis = System.currentTimeMillis()
    add(Calendar.SECOND, triggerTimeSec)
}
val intent = Intent(context, TestBroadcastReceiver::class.java)
val pendingIntent =
    PendingIntent.getBroadcast(
        context,
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT
    )
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setAlarmClock(
    AlarmManager.AlarmClockInfo(calendar.timeInMillis, null),
    pendingIntent
)

このようなバックグラウンド処理に関しては Android Developers にガイドがあるため一度読んでおくことをおすすめします。
https://developer.android.com/guide/background

オフライン機能

スーパーはいろいろな人が出入りする場所のためいろいろな電波が飛び交う可能性があり、ネットワーク状況が不規則に不安定になる可能性があります。
また storeLive が稼働する環境はスーパーの固定された位置を想定しているため、ネットワーク状況が悪くなってもネットワーク状況がいい場所に移動できないということもあり、ネットワークが不安定でアプリがうまく動かないという問題が稼働当初からありました。
他にも、 storeLive は基本的に動画を再生し続けるアプリであり人間の操作はほとんど存在しません。それに加えて、アプリを操作して動画を再生する人と動画を閲覧する人は別の人間です。そのため、通常のアプリでネットワークに接続出来ない場合によく取る手段としてリトライボタンを表示したり、 ネットワークの接続を確認してください といったような表示で凌ぐことはできません。

これらの問題がありますが、ネットワーク接続が切れるたびに担当者に復旧作業を行うようお願いしていては運用コストが跳ね上がってしまいかなりの負担になってしまうため、 storeLive ではできるだけアプリの安定性を高めるためにオフライン機能を実装しました。

オフライン機能を実装するにあたって取得したデータをローカルに保存する必要があり、今回はDBに保存することにしました。
Android でDBを扱うライブラリはいくつかあると思いますが、今回は Room を採用しました。
Room を採用した理由については、 storeLive プロジェクトでは Kotlin coroutines を採用していることや、 ViewModel や LiveData といった JetPack Components を採用しているので、これらと連携する手段が公式で用意されているためです。
また、チームとして Google が推奨しているアーキテクチャや JetPack Components をなるべく採用していこう、という動きがあるのも理由の一因です。

Room と LiveData で実現するオフライン機能

Room と LiveData を組み合わせて使うと簡単にそしてシンプルにオフライン機能を実装することが可能です。

Room 以下のようなデータを定義します。

@Entity(tableName = "movies")
data class Movie(
    @ColumnInfo(name = "id") val id: Int,
    @ColumnInfo(name = "movie_url") val movieUrl: String
)

次に Room で DAO を定義します。
以下の例では select 文を発行する関数を実装していますが、このとき戻り値を LiveData をラップすることでデータを LiveData 経由で非同期に取得することが可能です。
ちなみに LiveData を使う場合は裏側で勝手に別スレッドでDBに問い合わせするため、 selectAvailableMovies() 関数を呼び出すときに別スレッドで呼び出す必要はありません。

@Dao
abstract class MovieDao {

    @Query("SELECT * FROM movies")
    abstract fun selectAvailableMovies(): LiveData<List<Movie>>
}

使用するときは普通に observe してあげるだけです。
LiveData にデータが渡されるタイミングとしては、メソッドを呼び出した時の他に、 SELECT * FROM movies のクエリ結果に変更があった場合(movies table にデータが insert された場合等)に新しいデータが渡されます。

class MainActivity: DaggerAppCompatActivity() {

    @Inject
    lateinit var movieDao: MovieDao

    onCreate(savedInstanceState: Bundle?) {
    
        movieDao.selectAvailableMovies.observe(this, Observer { movies ->
            // 渡されたデータを処理する  
        })  
    }
}

こうすることで Activity や Fragment などデータが欲しい場所で API を叩くのではなく、 LiveData を observe しておき、新しいデータが欲しい場合は API を叩き結果を DB に insert すると、 UI の更新が可能です。
一見 DB が間に入っているため面倒ですが、一度でも UI が表示できれば DB にデータが入っているため、ネットワークがないとき、つまりオフラインでもアプリが動作するのでこれでオフライン機能の完成です。

また今回のオフライン機能は DB のデータを真としているため表示してる Activity/Fragment 以外からのデータの更新が可能です。
前述したとおり、 storeLive ではポーリング機能が存在してます。
もともとはポーリングでデータの更新を行ったあと BroadcastReceiver を用いて現在表示している Activity にデータの更新通知を送っていたのですが、今回のオフライン機能を実装したことでポーリングでデータの更新を行ったあと DB にデータを insert するだけで良くなったのは非常に嬉しかったです。

リストの表示には Paging と組み合わせて使う

JetPack Components の中にはリスト表示を行うためのライブラリとして Paging があります。 詳細な説明は省きますが、通常 Paging は DataSource クラスを使ってデータを読み込みます。
DataSource クラスには Factory クラスが用意されており、 Room は DataSource.Fractory クラスを戻り値にすることができます。

@Dao
abstract class MovieDao {
    
    @Query("SELECT * FROM movie_list_item")
    abstract fun selectMovieList(): DataSource.Factory<Int, MovieListItem>
}

また、 ktx の version 2.1.0-alpha01 から DataSource.Factory クラスに toLiveData() という拡張関数が用意されています。
この関数は内部で LivePagedListBuilder を用いて LiveData<PagedList> に変換してくれるため、そのまま RecyclerView でリストの表示が可能となります。

val movieList = movieDao.selectMovieList().toLiveData(
        config = Config(
            pageSize = 10,
            prefetchDistance = 10,
            enablePlaceholders = false
        )         
    )

movieList.observe(this, Observer {
    // RecyclerView で表示
    adapter.submitList(it)
})

オフライン機能の難しかった点

管理画面で操作した内容はいつ端末に反映されるのか

オフライン機能ではローカルにデータを保存するため、一度データを取得したあとはネットワークに繋がらなくてもアプリの表示が可能です。
そのため、何もしなければアプリ上からは これはいつのデータか というのはわからず、管理画面からデータの変更を行っても、それはいつ端末に反映されたのか、すでに反映された後なのか、といった判断が難しいです。
そのため、どういった操作をすれば必ずサーバーと通信しデータを更新できるのか、というのも決めておく必要があり、 storeLive では、画面遷移をするときには その画面に必要な情報自動更新に関するデータ(ライブ配信情報など) を更新する API を必ず叩くようにしました。

また、ネットワークに接続出来ていない場合には全画面上右上に ネットワークに接続していません という表示を出し、ひと目でネットワークに接続出来ているかどうかを判断できるようにしています。
ネットワーク接続判断は以下のように、接続状況が変わったら通知がくる LiveData を作り各画面で observe しています。

class NetworkCallbackLiveData(private val connectivityManager: ConnectivityManager) : LiveData<Boolean>() {

    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            postValue(true)
        }

        override fun onUnavailable() {
            super.onUnavailable()
            postValue(false)
        }

        override fun onLost(network: Network) {
            super.onLost(network)
            postValue(false)
        }
    }

    override fun onActive() {
        super.onActive()
        val builder = NetworkRequest.Builder()
        connectivityManager.registerNetworkCallback(builder.build(), networkCallback)

        val network = connectivityManager.activeNetwork
        val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
        value = network != null && networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
    }

    override fun onInactive() {
        super.onInactive()
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
}

余談ですが、アプリ上ではネットワークに繋がっている判定で ネットワークに接続していません の表示がでないのですが、ルーターから先のネットワークに接続できないらしく Unable to resolve host "example.com": No address associated with hostname というエラーが出てしまってデータの更新が出来なくなる、という問題が発生していて困っています。

表示するコンテンツに掲出期間があるか

こちらはアプリやサービスの性質によりますがコンテンツには表示期間が存在することがあり、実際に storeLive では各動画に表示期間を設けています。 何度も言う通り、オフライン機能ではローカルにデータを保存するため、一度データを取得したあとはネットワークに繋がらなくてもコンテンツの表示が可能です。
逆に言うと、ネットワークに繋がらなければデータの更新は一生行われません。
そのため、表示期間が定められていると期間を過ぎてアプリ上に表示されてしまうとまずい状況になってしまう可能性があり、いつまで表示していいのかという情報も一緒に保存しておく必要があります。

Room ではデータを取得する時に where 句を書くことができるので、テーブルに表示期間のカラムを入れておくことでデータ取得時にフィルタリングできるので便利です。

@Entity(tableName = "movies")
data class Movie(
    @ColumnInfo(name = "id") val id: Int,
    @ColumnInfo(name = "movie_url") val movieUrl: String,
    @ColumnInfo(name = "starts_at") val startsAt: String,
    @ColumnInfo(name = "ends_at") val endsAt: String
)
@Dao
abstract class MovieDao {
    
    @Query("SELECT * FROM movies WHERE starts_at <= :date AND :date <= ends_at")
    abstract fun selectMovieList(date: String): DataSource.Factory<Int, MovieListItem>
}

1つ注意したいのが、この時に何も考えずに端末時刻を比較に使用してしまうと Android では端末時刻は容易に変更できるため表示期間のチェックとしては不十分な場合があります。
storeLive の場合では、アプリや端末自体の操作はユーザーには行われない想定なので、端末時刻は変わらないという前提の元比較に使用しています。もし、この方法を参考に表示期間のチェックをする場合には注意してください。

まとめ

storeLive の主にポーリング機能やオフライン機能の開発についてご紹介しました。
storeLive はまだまだサービスのあり方を模索している段階で機能追加や仕様変更などがたくさんあります。また大きな機能の実装がありましたご紹介したいともいます。

興味がある方いらっしゃいましたら、気軽にお声がけください。一緒に色々チャレンジしていきましょう。

info.cookpad.com

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/