agri-note inside

ウォーターセル株式会社 スマート農業システム開発部のブログです。

AndroidのProduct FlavorsっぽいものをCreate React Appで再現したかった

こんにちは、モバイルチームの中川です。今回はJavaScript文化圏の記事になります。モバイルチームはユーザーの手のひらに価値を届けるためなら何でもするチームなので、必要ならJavaScriptだってやるんです(自分に言い聞かせるように)。それでは始めます。

アプリの細部を切り替えたい

ソースコードとしてはほとんど同じなんだけど、次のような振り分けをしてアプリとしては別々のものにしたいことってありますよね。

  • ビジネス上の都合でテーマを切り替えた版を作りたい
  • ボタンを出したり出さなかったりしたい
  • 接続先を切り替えたい

Androidアプリの開発では、build.gradleの中でBuild VariantsのProduct Flavorsとして定義することで、無料版と有料版を切り替えるみたいなことが簡単に行えます。iOSでも(本来の使い方からは外れるかもしれませんが)SchemeやTargetを切り替えることで似たようなビルド体験を実現できます。

Webでもやりたい

以前にも少し触れましたが、弊社にはCordova+ReactなiOSアプリがあります。最近は少しビルド環境を整理して、webpack2でゴリゴリ書いていたのを、Create React Appに閉じ込めることに成功しています。webpack.config.jsをメンテしなくていい生活は最高です。

さて、このたび、このアプリにもProduct Flavors的な実装をすることになりましたが、少し困ったことになりました。webpackならwebpack.config.jsごと差し替えるなり、内部で動的にエントリーポイントを差し替えるなりすればよいのですが、CRAではビルド環境に手を加えられる場所はほとんどありません。

早めの結論

Androidと全く同じくProduct Flavorsと同名のフォルダにコードを入れておけば自動で切り替えてくれるような、そういったものはできませんでした。webpackなら頑張ればできそうな気もしますが、少なくともCRAでは無理です。

ソースコードやリソースごと切り替えるというのは諦めて、アプリケーション内部で動的に差し替える方法を模索することになりました。

伝統と信頼の環境変数

動的に差し替えるにしても、アプリケーションに自分がどちらの環境で動いているのか教えてあげなければいけませんね。

そんなときこそ環境変数です。ビルド時点で外から依存性を突っ込んであげましょう。Androidから名前を借りて、今回も依存性の名前をflavorと呼んでいきたいと思います。

package.jsonにflavorを定義する

まずは変更前の状態です。dev serverをhttpsで動かしたかった関係で、 HTTPS=true が入っていたりしますが、至って普通のCRAです。

"scripts": {
  "start": "HTTPS=true react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test --env=jsdom",
  "eject": "react-scripts eject"
}

ここに、appAとappBという2つのflavorを環境変数で定義します。REACT_APP_から始まる環境変数はビルドに取り込んでもらえる機能があるので、これを活用した形になります。

 "scripts": {
-  "start": "HTTPS=true react-scripts start",
+  "start:appA": "REACT_APP_FLAVOR=appA HTTPS=true react-scripts start",
+  "start:appB": "REACT_APP_FLAVOR=appB HTTPS=true react-scripts start",
-  "build": "react-scripts build",
+  "build:appA": "REACT_APP_FLAVOR=appA react-scripts build",
+  "build:appB": "REACT_APP_FLAVOR=appB react-scripts build",
   "test": "react-scripts test --env=jsdom",
   "eject": "react-scripts eject"
 }

実用してみる

REACT_APP_ な環境変数は process.env に生えているので、次のように呼び出せます。

class App extends Component {
  render() {
    return (
      <div className="App">
        {/* 省略 */}
        <p>
          This is {process.env.REACT_APP_FLAVOR}
        </p>
      </div>
    );
  }
}

それでは、まずはappAとして実行してみましょう。

$ yarn start:appA # or npm run start:appA

f:id:Nkzn:20180307123746p:plain

ちゃんとappAとして表示されましたね。

次はappBとして実行してみましょう。

それでは、まずはappAとして実行してみましょう。

$ yarn start:appB # or npm run start:appB

f:id:Nkzn:20180307123848p:plain

上手くいきました。

余談: デバッグ・リリース・テストで環境を切り替えたい

デバッグ時とリリース時とテスト時で切り替えたい値としては、接続先のホスト名などがありますね。今回の記事の内容を踏まえると、これもnpm scriptsに環境変数を仕込めばいいように思えますが、実はこのへんには専用の機能が用意されています。

.env(.development|.production|.test)?(.local)? みたいな名前のファイルに REACT_APP_ な環境変数を書いておくことで、ビルドの種別に応じてreact-scriptsが勝手に採用する環境変数を切り替えてくれるのです。ファイル名の全パターンはこちらを見てください。

Androidアプリ開発ではBuild Typeに近い概念で分かりやすかったので、やはり開発時には重宝しています。

使い方

例として、接続先のホストを切り替える場合を考えてみましょう。

開発用の .env.development には次のような設定を書いておきます。

REACT_APP_HOST=https://dev.your-domain.com

そして、プロダクション用の .env.production には次のような設定を書いておきます。

REACT_APP_HOST=https://your-domain.com

これで、アプリ内では process.env.REACT_APP_HOST を参照して接続先を決定できるようになりました。

やはり便利なので使っていきたい機能です。

まとめ

REACT_APP_ な環境変数は、アプリケーション内でグローバルに参照できて、環境を振り分けるには便利なので、使っていきたい機能ですね。

DroidKaigi 2018にウォーターセルのエンジニアが登壇します

こんにちは。ウォーターセルでモバイルエンジニアとして働いている@Nkznです。

droidkaigi.jp

Android開発のお祭り、DroidKaigi 2018が明日に迫ってまいりました。2/8(木), 2/9(金)の2日間開催されるこのカンファレンスに、ウォーターセルからエンジニア(筆者)が登壇しますので、セッション内容について紹介します。

React Native Androidはなぜ動くのか / @Nkzn

2/9(金) 12:50-13:40にRoom 2で講演します。

マルチプラットフォームなツールは世に数多ありますが、モバイルアプリを作っている皆さんの感覚としては「謎の魔法で動いている」「何かあったときに原因が分からない」といった不安から、採用を躊躇することが多いと思います。

残念ながら、その懸念を完全に払拭することは難しいです。しかし、軽減することはできます。

このセッションでは、React NativeがAndroid上で動くとき、Android Javaのエンジニアにとってはどこからどこまでは魔法で、どこからはAndroidやJavaの理屈で理解できるのかをお話したいと思います。Androidエンジニアにとっては、React Native Androidは(ほぼ)魔法ではない、手のひらに収まる技術である。このセッションを聞いた後、そう思っていただけば幸いです。

Reactの基本のキにあたる部分は説明しますので、Reactや近代JavaScriptについての知識が無い状態で来ていただいてもある程度は理解できると思います。

それでは、皆さんのお越しをお待ちしております。

合わせて読みたい

ウォーターセル株式会社では、農業の役に立つアプリの開発にReact Nativeを活用しています。

watercelldev.hatenablog.jp

データバインディングの観点からiOSアプリ開発にReact Nativeを選択する

モバイルチームの中川@Nkznです。

弊社では、小さなプロジェクトから、少しずつReact Nativeのプロダクション投入を試しています。

watercelldev.hatenablog.jp

DroidKaigi 2017では、2016年2Q頃にReact Nativeを選択した経緯や成果についてのお話をしました。2017年2Qのタイミングでもそれは変わらず、「できる範囲でWrite Once, Run Anywhereしたいなー」といったモチベーションで採用しています。

一方で、異なる観点からも採用のモチベーションがチーム内に生まれていることに気がついたので、紹介しようと思います。

免責事項

この観点は、チームの歴史と密接したものなので、技術の話というよりは思い出話やポエムの類になってしまうことをご了承ください*1

Androider meets ReactJS

ウォーターセルのモバイルチームは、なかなかiOSエンジニアに恵まれない時期が長かったことなどから、アグリノートのiOS版をCordovaで開発することを選択しました*2

当初はIonicでAngularによる開発を想定していたのですが、WebフロントエンドのチームがReact+Reduxを採用しているため、テクノロジを合わせたほうがノウハウを共有しやすいだろうということで、React+Reduxにmaterial-uiを組み合わせる形になりました*3

堅牢なJavaに守られながら生きてきたメンバーたちは、最初こそおっかなびっくりといった様子で慣れないES201x(Babel)に触っていましたが、ふと気づけば皆、ラムダやLodashやPromiseがないと生きていけない近代JSerな身体になっていました*4。後述しますがこれが嬉しい誤算を生み出しました。

また、当時は気づかなかったのですが、Reactの特徴のひとつである「レイアウトを表すXML(JSX)とそこに埋め込んだ変数が相互に作用する」というデータバインディングの世界観も、着々とチームに浸透していきました。

Androider meets DataBinding

さて、Cordova製iOSアプリの開発が一段落付いたら、Androidアプリのメンテにも人を戻していかないといけません。

各メンバーの中で自然と、JSやReactでの開発で出会ったパラダイムをAndroidアプリ開発にも取り入れたいという意識が高まっていました。RetrolambdaLightweight Stream APIRxJavaなどを採用する際に心理的な障壁が非常に低くなっていたのは嬉しい誤算です。この頃にはAndroidにもデータバインディングライブラリが登場しており、早い段階で採用されていました。

「複雑に状態が変わる画面を、以前はデータバインディング(React含む)無しでどうやって開発していたのか思い出せない」

こんなセリフが聞こえるようになるまで、そう長くはかかりませんでした。

iOSには(俺たちの求める)データバインディングがない

さて、そんなこんなをしているうちに、iOSネイティブ開発ができる人材の都合が付きはじめまして、新規事業のアプリを、AndroidはJava、iOSはSwift3で作ることになりました。

しかしここでつらいことが。iOSには、レイアウトに変数を埋め込むタイプのデータバインディングがないのです。React脳になっていたメンバーがアサインされていたため、非常につらそうでした。

結局、RxSwiftを使って、 myTextField.rx.text のようなObservableを起点にしたRxのストリームで連鎖的にビューを更新する方式を取りました。「違うんだ……俺が欲しかったデータバインディングはこれじゃないんだ……」という怨嗟の声が聞こえました。

iOS meets React

そして2017年春。DroidKaigiでの筆者の発表を見たり、実際に触ってみたメンバーから、こんな意見が聞こえてきました。

「iOSアプリだけでもReact Nativeで開発するのもアリなんじゃないか」

そのときは「それだと結局AndroidとiOSのUI仕様を合わせる手間がかかって、React Nativeの旨みが減るじゃないか」と突っぱねたのですが、一ヶ月ほど経って、これが実は面白い観点を含んでいたことに気がついたのです。

それは、「俺たちがやりたかったデータバインディングをiOSアプリ開発でやれる」というものでした。

どうせReact NativeでiOSアプリを開発していれば、なんだかんだとObjective-CやSwiftを書くことになります。iOS SDKを触ることになります。それはもう仕方ないのです。書くべきところはObjective-CだろうとSwiftだろうと書くしかないです。

ただ、UIだけはデータバインディングが使えるフレームワークで書かせてほしい。そういう理由だけでiOSアプリ開発に使うのも、技術選択のひとつの選択肢として、アリなのかもしれないと思いました。

まとめ

Androider上がりのエンジニアが、どうしてもInterface BuilderやStoryboardがどうしても気に食わない、レイアウトに変数を埋め込むタイプのデータバインディングを扱いたい、というモチベーションを持ってiOSアプリを開発するときには、選択肢のひとつにReact Nativeがあってもいいのでは、という提案でした。

*1:これが理由でQiitaに書くのを断念しました

*2:「素直にiOS SDK覚えなさいよ」と思われてしまいそうですが、当時は「最近はWebViewもパフォーマンス悪くないし、全部Webに寄せたら効率いいんじゃないか」というチャレンジをしてみたかったのです

*3:CSSフレームワークの美しさやTypeScriptという観点ではIonic2を使う案もあったのですが、2015年末のIonic2はプロダクションで使っていいものではなかったため、断念しました

*4:実は堅牢さについても、ESLintを厳しくしすぎてJavaより構文に厳しい環境になっていました

SimpleAppsシリーズ(仮称)始めました

お久しぶりの投稿になります。中川@Nkznです。

近年は弊社メンバーの主戦場がQiitaになってきており、こちらの更新をする頻度が減ってきておりました。

qiita.com

(EXCEL・R勢の更新頻度が高いのでWeb・モバイル勢も頑張ろうな……!)

今回は会社色の強い内容になったため、久々にこちらを稼働させることにしました。

SimpleAppsシリーズ(仮称)とは

※React Nativeにしか興味が無い人は読み飛ばしてください

特定の小さいドメイン(使用目的)に寄り添った小機能なアプリです。社内的には「単機能アプリ」と呼んでいることが多いのですが、味気ないので一部ではSimpleAppsと呼ばれています。

費用対効果の高い新事業を模索する一環として始まった、社長直轄プロジェクト*1です。

今回は次の3つのアプリをAndroidとiOSそれぞれに向けてリリースしました。

  • 水門アプリ
  • 面積計算アプリ
  • 距離計算アプリ

これらのアプリがどんなモチベーションで作られたのか、ご紹介していきます。

*1:実は社長が付かない"代表取締役"なので当人は社長と呼ばれると嫌がるんですが、このほうが通りがいいので勘弁してください

続きを読む

僕らのデータ同期プラクティス #DroidKaigi

こんにちは。モバイルチームの中川[twitter:@Nkzn]です。

droidkaigi.github.io

Androidエンジニアのためのカンファレンス「DroidKaigi」に登壇する機会をいただきまして、サイバーエージェントのセミナールームでお話してきました。

本エントリは、発表原稿としてスライドの元にした文章です。技術的には同一の内容になっています。

スライド

www.slideshare.net

はじめに

f:id:water-cell:20150426203711j:plain

ユーザーがどんな場所にアプリを持っていこうと、私達はそれを制止できません。自分たちのWebサービスを携帯網もWi-Fiもない場所でも使ってほしいと思ったとき、私たち開発者には何ができるでしょうか。

GmailアプリやEvernoteアプリのように、オフライン時に閲覧・作成・編集されたデータをサーバーと同期させるための仕組みが、Androidには用意されています。

ただ、サーバーと同期する際のアルゴリズムについては開発者任せです。

本エントリでは、データ同期を実装するにあたって参考にしたものや気にしたことを紹介していきます。

自社アプリにオフライン機能を実装しよう

弊社サービスについて

  • アグリノート
  • 農業生産者向けの農作業管理システム
  • 費用は4万円/年
  • Webブラウザ版、Androidアプリで提供
    iOS版は開発準備中
  • 農業版Redmineに近づいてる

f:id:water-cell:20150426153820p:plain

業務システムにはよくある話ですが、マスタデータとトランザクションデータの組み合わせにより農作業記録のデータ構造が作られています。

オフライン機能への要望の高まり

2012年3月にAndroidアプリのファーストバージョンをリリースしました。この頃は、画面を表示する時点でデータをfetchしてくる方式でしたが、しばらくすると、似たような要望が散見されるようになりました。そう、「圏外の地域でも利用したい」という要望です。

近年では人が住んでいる場所がエリア外ということは減ってきているようですが、山の中だったり、広大な北の大地のど真ん中に行けば、容易に圏外になるそうです。

f:id:water-cell:20150426153935j:plain

「現場で使うアプリ」を標榜しているのに、現場で使えないというのも辛いところです。この問題は、長いこと課題として残っていました。

また、アグリノートの導入を機に法人契約で社員にスマホを持たせている法人農家で、通信費のほうが高く付いている事例も聞こえるようになったことも気になっていました。オフライン動作ができる分には、Wi-Fi接続のみのタブレットという選択肢が生まれるので、これもまた大きなモチベーションになりました。

現場では電波が無くてもデータの保存ができて、電波がある場所に戻ってきたときにデータを同期できる。2013年の終わり頃からアプリの全面リニューアルを始めたタイミングで、そんなオフライン機能の検討を始めました。

気合で同期機能を実装する

さて、自動同期の機能を実現しようとしたら、何が必要でしょうか。

  • 定期的に、または何らかのキックにより
  • バックグラウンドで通信を行い
  • アプリ内のDBを更新する

こんなところですね。

実は2013年にも仕事で同期機能を持ったAndroidアプリを作成したことがありました。当時の私がなんとか知恵を絞って考えたのが下記の構成です。

  • android.app.AlarmManager で定期的にインテントを発行
  • android.app.IntentService でバックグラウンドプロセスを作成
  • ORMLiteでDB処理

f:id:water-cell:20150426154046j:plain

実際にこの方針で実装していく中で、色んな問題が発生しました。

  • AlarmManagerがときどき消える
  • IntentServiceが連続で走って止まらない
  • 画面からの更新も混じえてマルチスレッドでDB叩く状態に突入
  • synchronized祭り
  • ネットワーク有無の検知のために割と頻繁に起動

並行処理プログラミングに強くない人間が首を突っ込んではいけない領域に足を突っ込んでいたような感じでした。

ちなみに同期アルゴリズムとしては、朝イチでログインさせてそのときに前回データを全部消して、それから最近のデータを丸々引っ張ってくるという感じだったので、同期というかただのダウンロードでした。

Androidが提供するデータ同期機能

まずはAndroid側が用意してくれている仕組みの概要や経緯についてお話します。

Android 2.0(API Level 5)で、アカウント管理の機能が導入されると同時に、データ同期のためのAPIが提供されました。

Developers can create sync adapters that provide synchronization with additional data sources.

http://developer.android.com/about/versions/android-2.0-highlights.html

もちろん、この機能は現在でも利用できます。

バックグラウンドでデータの送受信を行いたい場合、スレッドの管理や、ネットワーク有無の検知などを自分で管理するのは手間ですが、このSyncAdapterを利用することでAndroid側にそういった管理を任せられるようになるのです。

導入が面倒

便利には便利なのですが、このSyncAdapterという仕組み、なかなか導入が面倒です。結構な数のファイルを作って適切に配置・設定してあげないと、動き出してくれません。

雰囲気としては、AndroidManifest.xmlを起点として、下記のように各機能を繋いであげる必要があります。

f:id:water-cell:20150426154014j:plain

それぞれの役割としては下記のような感じです。

  • AccountAuthenticatorで認証情報を管理する処理を行う
    • バックグラウンドで動かすためのServiceも実装する
  • SyncAdapterで同期処理を実装する
    • バックグラウンドで動かすためのServiceも実装する
  • ContentProviderでデータを管理する
  • それぞれの処理をAndroidManifest.xmlで繋ぐ
    • Authority文字列やAccountType文字列を揃える

非常に面倒ですね。とはいえ、サンプルがあれば充分に把握可能なものだと思います。

しかし・・・?

ドキュメンテッドになったのが割と最近

GmailやGoogleカレンダーなどはSyncAdapterの便利さを享受していると思われますが、デベロッパーの間での利用が増えてきたのは割と最近のように思われます。

調べてみると、2009年にAndroid 2.0が出てから約4年後、2013年のGoogle I/Oくらいの時期まで、リファレンスの方には断片的に使い方が書いてあったものの、使い方の全体像になる公式ドキュメントが存在しなかったらしいことが見えてきました。

Transferring Data Using Sync Adapters をインターネットアーカイブサービスで調べてみると、初出が2013年7月13日ということになっています。

f:id:water-cell:20150426181835j:plain

http://web.archive.org/web/20130601000000*/https://developer.android.com/training/sync-adapters/index.html

また、同年のやんざむ氏のGoogle I/Oレポを覗いてみると、SyncAdapterが紹介されているのが伺えます。

前述のとおり、それなりに複雑な設定をしないと動かないものですので、公式ドキュメントの登場により、ようやくデベロッパーが「どんなものか試してみようか」と取り組める下地ができたのではないでしょうか。

補足

[twitter:@vvakame]氏もやんざむ氏の記事と同じことを言ってるので、I/Oのときにそういう話があったのは確かみたいです。

2013〜2014にかけての資料の充実

Google I/Oでの言及や公式ドキュメントの整備を皮切りに、少しずつまとまった資料が増えてきたように思います。

時期を同じくして、2013年6月に出版された「50 Android Hacks」の中でも、SyncAdapterの使い方が紹介されました。この書籍は江川さんらにより翻訳され、2013年11月に日本語版が出版されています。

tatsu-zine.com

この後、2014年の初めにmixi-inc/AndroidTrainingの中で基礎編の中に組み込まれたりしたのを初め、Web上での情報が増えていったような気がします。

補足

AndroidTrainingのSyncAdapter項を書いた [twitter:@KeithYokoma] さんによると、社内の知見をかき集めて何とかしていたようです。

同期アルゴリズムの検討

50 Android Hacksのお陰でSyncAdapterを導入すること自体には大きな問題はありませんでしたが、その一方で、どんなアルゴリズムで同期を実現するかという思想的な部分では悩まされました。

結果的に、参考にした資料は下記の2つです。

  • 50 Android Hacksのサンプルコード
  • Evernote Synchronization via EDAM

50 Android Hacks

github.com

SyncAdapterはHack 23として紹介されています。紙面では要点を押さえた感じで紹介されていますが、サンプルコードに入っているTODOアプリがかなりしっかりしており、基本を掴むための助けになりました。

サンプルにはPython製のサーバーやブラウザ版クライアントも同梱されており、データ同期をより実践的な形で体験できます。

Evernote Synchronization via EDAM

Evernoteのクライアント・サーバー間データ授受のプロトコル "Evernote Data Access and Management" の仕様書PDFです。全15ページ。

サーバー、クライアントにそれぞれどんな要素を持たせることで "state based replication" を実現しているのかが解説されています。サーバー側にも手を入れるリソースがあれば、この資料の内容をそのまま模倣するのもアリだと思います。

なお、運営の [twitter:@ninjinkun] さんが日本語訳を用意してくれていたりするので、こちらを読んだほうが楽かもしれません。(発表前日に見つけました)

github.com

採用した仕組み

前述の資料を参考にしながら、仕組みとして取り入れたのが、下記の要素です。

  • Full Sync, Incremental Sync
  • StatusFlag("dirty" flag)

Full Sync, Incremental Sync

f:id:water-cell:20150426182345j:plain

Evernoteからもらってきた考え方です。

Full Syncは完全同期と訳せそうです。その時手に入る情報を全部落としてきてしまう種類の同期です。差分について考えなくてもいい代わりに、データ量が多くなりがちです。

一方、Incremental Syncは逐次同期とでも訳せばよいでしょうか。クライアントがどの時点までのデータを持っているのかをサーバーに伝えることで、それ以降の差分となるデータだけを請求できます。一度の同期に関わるデータの量は少なくなりますので、定期的に同期する際にはこちらの方法のほうが有効です。

差分をもらうためのパラメータ

f:id:water-cell:20150426182421j:plain

アグリノートでは、最終同期時刻 last_fetched をアプリ内に持っています。API呼び出しのパラメータにこの時刻を載せることで、それ以降に変更のあったデータのみを受け取ることができるのです。

サーバー側の仕組みとしては、各APIに対応するテーブルのmodified_at列を見に行って、パラメータに指定した時刻以降の変更を返すようにしてもらっています。

StatusFlag ("dirty" flag)

Full SyncやIncremental Syncはどちらかというと取得するデータに主眼を置いた仕組みでしたが、こちらは送信するデータに主眼を置いています。

50 Android HacksではStatusFlag、Evernoteでは "dirty" flagと呼ばれていましたが、未同期のクライアント環境でデータに変更があったことを表すフラグという点では同じ性質のものです。

私達は50 Android Hacksに倣い、CLEAN, ADD, MOD, DELETEの4つのステータスを持つフラグを切り替えることで、各データの状態を表すことにしました。

status_flag 意味
CLEAN サーバーから受け取ったままの状態
ADD 新規に作成された, まだクライアント側にしかない
MOD サーバーから受け取ったものに変更を施した
DELETE サーバーから受け取ったものを削除した

同期の流れ

それでは実際に同期の流れを見て行きましょう。基本的には50 Android Hacksでの流れを踏襲しました。

  1. データのダウンロードを行う
    • 端末内に last_fetched が保存されていないとき = 初めて同期するときはFull Syncを行う
    • 端末内に last_fetched が保存されている時 = 既に前回データがある場合はIncremental Syncを行う
  2. サーバーで削除されていたデータをクライアントでも削除する
  3. サーバーで更新されていたデータをクライアントでも更新する
  4. クライアント側で作成(ADD)・更新(MOD)・削除(DELETE)されたデータをサーバへ送信する
  5. 送信が済んだデータのStatusFlagをCLEANにする
  6. last_fetched に1の時刻を保存する

これで同期が成立しているはずです。

競合問題

同期の理屈はできましたが、もう一つ考えないといけないことがあります。サーバーが行なったデータ操作と"dirty"なデータの間で競合があった場合です。

具体的には、以下の2つの段階で問題になります。

2. サーバーで削除されていたデータをクライアントでも削除する
3. サーバーで更新されていたデータをクライアントでも更新する

クライアント側のデータが変更されていて、なおかつサーバーからもデータを貰ってしまったとき、取れる選択肢は限られそうです。

  • 常にサーバー側が勝つ(送信しない)
  • 常にクライアント側が勝つ(サーバーからのデータを捨てる)
  • クライアント側でマージしてからサーバーへ送る(なんとか全部生かす)

せっかく手元で作ったデータですから、送信したいですよね。サーバーに勝たれるのは困ります。クライアント側のデータをそのまま送るか、サーバーから貰ったデータとなんとかマージしてから送るか、といったところになりそうです。

アグリノートアプリではクライアントが勝つことにしてあります。マージといってもどこからどこまでマージしたものか検討するコストが高そうだったからです。

なお、Evernoteで以前試したときには、マージしてから送信する方法を取っていました。EDAMにもそう書かれています。

まあそもそも、ブラウザ対ブラウザでも画面の表示タイミングや保存を押すタイミングによっては起こることですし、そのときには後勝ちになる場合が多いと思うので、この件もクライアント側が勝つのが自然なんじゃないかと思うのです。

同期パターンの確認

さて、流石にそろそろ話がごちゃごちゃしてきたので、頭の中を整理するために同期のパターンを表にまとめてみました。

f:id:water-cell:20150426182528p:plain

  • 元々のstatus_flagはADD, MOD, DELETE, CLEANのどれだったか
  • 更新されたものが降ってきたのか、削除されたものが降ってきたのか、何も降ってこなかったのか
  • 結果としてクライアント側のデータがどう更新され、サーバーにはどのデータが送られるのか

このへんについてまとめられています。チーム内での認識合わせに役立ちました。

まとめ

同期が必要になるような要件が出てきたら、この話を思い出してください。

  • mixi-inc/AndroidTrainingでSyncAdapterを勉強して
  • 50AHでSyncAdapterのサンプルを知って
  • EDAMの理屈を参考にして同期の仕組みを考える

こんな流れで乗り越えられる山があるはずです。

最後に

ウォーターセル株式会社では、地球人口100億の時代に見合う食料生産のための農業革命を一緒に引っ張っていってくれるAndroid/iOSエンジニアを探しています。

弊社事業に興味のある方は、是非ご連絡ください。

おまけ:50AHの注意点

50 Android Hacksのサンプルは読み応えがあり良い資料なので是非読むべきですし、本の内容も概ね良いのですが、SyncAdapter章に致命的な誤訳があるので気をつけましょう。

  • 誤 :23.2.2 サーバの中でデータベースを扱う
  • 原著:23.2.2 Hitting a database instead of the server
  • 直訳:23.2.2 サーバーの代わりにデータベースを叩く

原著がサンプルとして公開されているので、英語版と読み比べながら読むくらいでちょうどいいかもしれませんね。(Sample chapter5に該当の章があります)

正誤表が見つからなかったので、この場を借りて紹介しました。もし正誤表のページがあれば教えていただければ幸甚です。

Android 5.0からSVG準拠のdrawableが書けるようになりました

こんにちは、Androidチームの中川@Nkznです。

Android Studio 0.8.14のリリースノートを見ていて知ったのですが、LollipopのAPI Level 21では<vector>などのdrawable系タグが拡充されたのですね。

名前的にベクターイメージが書けそうですが、実際何ができるのだろうと思って調べてみました。

Vector images are represented in Android as VectorDrawable objects. For more information about the pathData syntax, see the SVG Path reference.

<vector>の中に書く<path>タグのpathDataの記法が、SVG準拠のようです。サンプルコードがこちら。

 <path android:fillColor="#8fff"
     android:pathData="M20.5,9.5
                       c-1.955,0,-3.83,1.268,-4.5,3
                       c-0.67,-1.732,-2.547,-3,-4.5,-3
                       C8.957,9.5,7,11.432,7,14
                       c0,3.53,3.793,6.257,9,11.5
                       c5.207,-5.242,9,-7.97,9,-11.5
                       C25,11.432,23.043,9.5,20.5,9.5z" />

なるほどとてもSVGでした。

何が嬉しいのか

f:id:water-cell:20141030155856p:plain

http://developer.android.com/design/style/iconography.html

SVGをはじめとしたベクターイメージの強みの1つとしてよく云われるのは、拡縮しても綺麗さが変わらないという点です。

Androidでは(最近ではiOSも)対象端末の解像度ごとに別々の大きさの画像を用意するのがそれなりに手間です。また、同じアイコンでもアプリ内で使う場所(左上用、ボタン用、見出し用など)が変われば、それぞれに別々のサイズを用意しなければいけません。

SVGで画像リソースを作成できるようになれば、1種類のリソースで様々なサイズの要求に応えることができるようになりますので、嬉しい事になりそうです。

余談:公式Material Designアイコン

そういえば先日、Googleがマテリアルデザイン向けのシステムアイコンをリリースしたことが話題になっていました。

このニュースを見た時には「SVGが用意されていても、どうせWebでしか使えない」と考えていたのですが、Lollipop以降のバージョンでは<vector>によってSVG(っぽいもの)が利用できますので、転用の可能性が広がります。

試してみた

実際にSVGはどのように表示されるのでしょうか。前述のマテリアルデザインアイコンから下記のic_add_to_photos_48px.svgを拝借して、表示してみたいと思います。

SVGから変換(手作業)

  • ic_add_to_photos_48px.svg
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
    <path d="M0 0h48v48h-48z" fill="none"/>
    <path d="M8 12h-4v28c0 2.21 1.79 4 4 4h28v-4h-28v-28zm32-8h-24c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4v-24c0-2.21-1.79-4-4-4zm-2 18h-8v8h-4v-8h-8v-4h8v-8h4v8h8v4z"/>
</svg>
  • drawable/ic_add_to_photos_48px.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="256dp"
    android:width="256dp"
    android:viewportWidth="48"
    android:viewportHeight="48">
    <path
        android:fillColor="#000000"
        android:pathData="M8 12h-4v28c0 2.21 1.79 4 4 4h28v-4h-28v-28zm32-8h-24c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4v-24c0-2.21-1.79-4-4-4zm-2 18h-8v8h-4v-8h-8v-4h8v-8h4v8h8v4z"/>
</vector>

それっぽく似たような雰囲気にしてみました。width, height周りは適当です。

この時点で、レイアウトエディタがプレビューをしてくれるようになりました。

f:id:water-cell:20141030163257p:plain

数値を適当に弄ってみるとちゃんと反映されます。

f:id:water-cell:20141030163626p:plain

画面に組み込む

一度drawableリソースとして取り込んでしまえば、使い方としてはいつもの<shape>と似たようなものです。

  • activity_main.xml
<!-- 略 -->
<FrameLayout
    android:layout_width="24dp"
    android:layout_height="24dp"
    android:background="@drawable/ic_add_to_photos_48px" />
<!-- 略 -->

上記のようなFrameLayoutを何サイズか並べてみたものがこちらになります。

f:id:water-cell:20141030153359p:plain

一手間かければSVGアイコンがAndroidでも使えるようになったことがお分かりいただけたかと思います。

まとめ/雑感

ということで、長らくAndroidアプリ開発者を悩ませてきた画面の断片化問題が1つ解決するのではないかという期待が持てる機能を紹介しました。

最後に少しだけネガティブなことを言うと、パフォーマンスとか大丈夫なのかなーと思ったりはしました。それから、

これは思いました。わざわざ直すのは面倒です。drawableフォルダにSVGファイルそのまま置かせてもらえると一番嬉しいのですが・・・しばらくは動的にSVGをVectorDrawableに変換するような手法を使うのが現実的でしょうか。

まあ、日本でLollipopが十分に普及するまではまだしばらくかかると思いますので、周辺環境が整うのを見守りたいと思います。(support-v7あたりに取り込まれないかなと)

それでは今回はこのへんで。

宣伝

ウォーターセル株式会社では、農業生産者に嬉しい情報の扱い方を一緒に考えてくれるWebフロントエンドエンジニア、Railsエンジニア、Androidアプリエンジニアを探しています。

興味のある方は、是非一度新潟まで遊びに来てみてください。

GCMのUser Notificationsの使い方

こんにちは、Androidチームの中川@Nkznです。

Google Cloud Messaging for Androidの勉強をしていたところ、User Notificationsのドキュメントに分かりづらい部分があってハマったので、備忘録として書き残しておこうと思います。

というわけで、User Notificationsがどんな機能なのかは説明しません。知りたい方は、ドキュメントをお読みください。

User Notificationsを使わずにpush配信する

まずは、普通のpush配信のやり方。公式資料でも丁寧に紹介されていますし、日本語記事も多くあります。

curl \
--header "Authorization: key=<API_KEY>" \
--header Content-Type:"application/json" \
https://android.googleapis.com/gcm/send \
-d "{\"registration_ids\":[\"<REGID>\"],\"data\":{\"message\":\"Hello\"}}"

registration_idsをJSON配列として列挙すれば、IDに紐付いた端末に{"message":"Hello"}というデータが配信されます。

User Notificationsを使ってpush配信する

同じユーザーが持っている複数の端末に対してPush通知を送りたい場合は、registration_idsを列挙するよりも、User Notificationsを使ったほうが便利そうです。

シンプルな流れとしては、下記の手順になります。

  1. 複数のregistration_idをまとめたnotification_keyを発行する
  2. notification_keyを使ってpush通知を行う

notification_keyを発行する

まずはnotification_keyを発行しましょう。各種ID, KEYを適切に配置していけば、特に難しいことはありません。

Request

curl \
--header "project_id: <PROJECT_NUMBER>" \
--header "Authorization: key=<API_KEY>" \
--header Content-Type:"application/json" \
https://android.googleapis.com/gcm/notification \
-d "{\"operation\": \"create\",\"notification_key_name\": \"appUser-Hogehoge\",\"registration_ids\":[\"<REGID>\",\"<REGID>\",\"<REGID>\"]}"

Response

{"notification_key":"<NOTIFICATION_KEY>"}

上記のようなリクエストを送ることで、無事にnotification_keyを取得できました。

push通知を行う(仮)

それでは、notification_keyを使って実際にpush通知を行ってみます。

registration_idsの説明を見ると、必須項目についての話題がありました。

A request must include a recipient—this can be either a registration ID, an array of registration IDs, or a notification_key. Required.

registration_idsnotification_keyのどちらかを必須で含めればよいようです。ということはregistration_idsを使ったパターンをそのまま差し替えればいけるはず。

Request

curl \
--header "Authorization: key=<API_KEY>" \
--header Content-Type:"application/json" \
https://android.googleapis.com/gcm/send \
-d "{\"notification_key\":\"<NOTIFICATION_KEY>\",\"data\":{\"message\":\"Hello\"}}"

Response

Missing "registration_ids" field

・・・あれっ?

困ったときのStackOverflow

StackOverflowを探してみると同じ悩みを持った人が見つかりました。

ベストアンサーがこちらになります。

The documentation is buggy, you have to use this request:

// 略
{ 
   "to": "<NOTIFICATION-ID>",
   "data": {},
}

notification_keyの記述にはtoを使うそうです。

この回答者の方はドキュメントのバグと言ってますが、現在のドキュメントを改めて見てみたところ、notification_keyの行の端にこんな記述がありました。

HTTP. This feature is supported in CCS, but you use it by specifying a notification key in the "to" field.

なるほど、notification_keyフィールドがサポートされているのはCCS方式でリクエストするときだけで、あとの場合はtoに入れてねということでしょうか。

しかしこの書き方だと、結局サポート先がHTTPなのかCCSなのか分かりづらいですね。また、当のtoの行を見ると、CCSのみのサポートでHTTPはサポートしていないことになっているので、なるほどこれはbuggyと言われても仕方がない感じがします。

push通知を行う(真)

そんなわけで、最終的にnotification_keyを付与したリクエストは、下記の形になりました。

curl \
--header "Authorization: key=<API_KEY>" \
--header Content-Type:"application/json" \
https://android.googleapis.com/gcm/send \
-d "{\"to\":\"<NOTIFICATION_KEY>\",\"data\":{\"message\":\"Send with User Notification\"}}"

手元の端末が一斉に震える様は壮観でした。

まとめ

GCMを触り始めてみて、push通知が思ったより簡単に、そして無料でできることに驚いています。

ほしい情報をほしい時に通知する、という理想を実現するためには、push通知は大きな役割を果たしてくれます。Android Wearな腕時計型デバイスやGoogle Glassなど、アプリからの通知を受け取ってユーザーの見やすい場所に表示してくれる強力なツールも今後増えていくことでしょう。

通知周りはまたしばらく勉強していきたいと思います。

宣伝

ウォーターセル株式会社では、農業生産者に嬉しい情報の扱い方を一緒に考えてくれるWebフロントエンドエンジニア、Railsエンジニア、Androidアプリエンジニアを探しています。

興味のある方は、是非一度新潟まで遊びに来てみてください。