こんにちは、モバイルチームの中川@Nkznです。
社内でテックトークイベントを開いたので、その様子をレポしたいと思います。
🍕や🍺を嗜みながら、和気あいあいと発表を行いました。
割と急にやることが決まり、週初めに告知、週末に実施という短い期間の中、9名が発表する良い規模のイベントとなりました。
発表ダイジェスト
各発表を簡単に紹介します。
続きを読むこんにちは、モバイルチームの中川[twitter:@nkzn]です。
5月22日にプレスリリースがあった提携で紹介されていたアプリでは、React Native及びReact Native for Webを採用しています。こちらについて技術的な側面から(当たり障りのない範囲で*1)事例を紹介します。
5/22に、農業総合研究所さんとの業務提携契約が公開されました。
農業総合研究所さんは、7000件以上の農家さんから野菜を集荷し、全国各地のスーパーなどに設置された直売コーナー「農家の直売所」に野菜を出荷している、農産物の流通・販売・コンサルティングを手がける農業ベンチャーです。
(上記のスクリーンショットは2018年7月18日現在のものです)
今回の業務提携により、共同でシステム開発を行っていくことになりました。プレスリリースにある通り、第一弾として農薬使用履歴を管理するアプリを開発しています。
様々な要件や今後のウォーターセルとしての方針を加味したところ*2、Android, iOS, Webに向けてマルチプラットフォームで開発することになりました。
今回は特にAndroid, iOS, Webでプラットフォームごとに要件が違うということもなく、むしろ農総研さんの中での教育コストや、農家さんへの指導のコストを考えると、UIは揃っていたほうがよい、ということで、UIレベルでのソースコード共有を行う下地が整っていました。
AndroidとiOSについてはReact Nativeを使えばよいのですが、Webも、となると話が違ってきます。React NativeにはWeb向けのターゲットが用意されていません*3。
ここで目をつけたのがReact Native for Webです。
React Native for Webは「React Nativeと同じ名前のコンポーネントに同じ名前のpropsが生えていて、スタイルの再現性が極めて高い、Web向けのUIコンポーネントライブラリ」です。次のように使います。
import { View, Text, StyleSheet } from "react-native-web";
このままでは「コンポーネントの種類とスタイルのクセがReact Nativeとよく似ているUIライブラリ」でしかありません。
実際に使う場合は、BabelやWebpackを使ってパッケージ名にエイリアスを付けることで、React Native向けに書かれたコードに対してパッケージを誤認させます。
import { View, Text, StyleSheet } from "react-native"; // Web環境向けにビルドした場合はWeb向けのViewが出てくる
このへんのやりかたについては公式チュートリアルで紹介されていますので、興味がある方はご覧ください。
パッと見ではかなり眉唾感のあるライブラリだと思います。が、検討していくうちに、外堀を見ている限りでは信頼に足るものなのでは?と思えるようになりました。
眉唾物のネタプロダクトと切って捨てるには、あまりにも素性が良かったのでした。
というわけで、触ってみることにしました。ExpoとReact Native for WebでUIコードを共有しつつ、画像や音声などのリソースも管理できるプロトタイプを作成しました。
React Native for WebとExpoを組み合わせてピコピコさせてみたよ - Qiita
感想としては次のようなものになりました。
技術選択は事業とチームにフィットするものであるべきです。ということで、どういったチームがあったお陰でこの選択をする気になったのか、という話もしなければなりません。
以前にも記事にしたことがありましたが、弊社モバイルチームはiOSアプリをCordova+webpack+Reactで運用しており、Webアプリの開発については多少の経験があります。また、もともとAndroidエンジニアとしてゴリゴリ書いてきた人たちなので、Android側の挙動についてもトラブルシューティングは容易です。
iOSネイティブのトラブルシューティングだけは不安が残りましたが、こちらは外部の方にお手伝いしていただくことで解決できました。ここは今後の課題です。
各プラットフォームでネイティブな手段でも開発が可能なチームを作って、トラブルシューティングができる体制を整えられたのも、Webとネイティブの同時開発という選択ができた要因になりました。
React Native for Web、けっこう行けるやん。ということで、採用と相成りました。*6
実際に開発をしてみて、React Native for Webを採用して良かったこと、困ったことを挙げていきます。環境は次のとおりです。
モジュール | バージョン |
---|---|
react / react-dom | 16.0.0 |
react-native-web | 0.5.4 |
react-native | 0.51.1 |
最も開発効率に寄与してくれたのが、Storybookの採用でした。React Native向けのコンポーネントをブラウザ上で動作確認するイテレーションを高速に回すことができました。
Storybookは前述のプロトタイプにも組み込んでありますので、興味がある方は御覧ください。
Storybookを使ったことによるもう一つのメリットとして、普段WebフロントエンドのチームでReactコンポーネントの作成を担当しているマークアップエンジニアをアサインするのが、容易に実現できました。
くらいのスペックです。
実はStorybookにはReact Native向け実装がありまして、Androidエミュレータを立ててネイティブUIによる動作確認を行う方法もあります。しかし、今回彼をアサインするにあたり「今回のためだけにAndroid StudioやXcodeを入れてもらうのは申し訳ないな〜」という気持ちがありました*7。
というわけで、今回は思い切って、彼にはブラウザのStorybookだけでコンポーネントの動作確認をしてもらうことにしました。
色々ありましたが(後述)、概ね上手くいったと思っています。当人も「 <View>
と <Text>
はdivとspanみたいなものだし、この2つのスタイルのクセを把握すれば、あとはその派生だったから問題なかった。React Nativeのコンポーネントづくりは、十分にマークアップエンジニアの土俵だった」という主旨の話をしており、手応えはあったようです。
というわけで、マークアップエンジニア氏にはかなりの量のコンポーネントを作ってもらいました。そんな中で初期に困ったのが、「Webでしか使えないスタイル」の存在です。
React NativeのレイアウトシステムはYogaが使われているわけですが、これはCSSを再現するものではなく、Flexboxを再現するものです。そのため、Webでは一般的な記法でも、React Nativeでは使えないものがあります。当時に遭遇したものは width
や margin
の値でした。Webでは良くてもReact Nativeではダメな例を挙げます。
// 値に単位をつけるのはNG { width: '100px', marginLeft: '16px' } // 値を直接指定するのはOK { width: 100, marginLeft: 16 }
// %やautoを使うのはNG(だった。現在では使えるらしい。後述) { width: '100%', } { marginLeft: 'auto', marginRight: 'auto' } // Flexboxを使うのはOK { flexDirection: 'row', flexGrow: 1 } { justifyContent: 'center' }
// ViewにText系のスタイルをつけるのはNG <View style={{ color: "#F00" }}> <Text>ここには色がつかない</Text> </View> // 直接指定するのはOK <View> <Text style={{ color: "#F00" }}>ここには色がつく</Text> </View>
最後のTextスタイルの話は、違いを如実に表しています。CSSは内側に向かって再帰的に効きますが、React Nativeの装飾系のスタイルは適用したコンポーネントそのものにしか効きません。
当初、この問題は発覚しませんでした。ブラウザ上で動かしている分には普通のReactとReactDOMによる挙動に準拠するため、ちゃんと動いてしまっていたのです。その後、「そろそろAndroidでも動作確認するかー」と言い出したときにレイアウト崩れが起きて発覚した次第です。
発覚して以降は「Flexboxしか使えないならそれはそれで」ということで納得して書いてもらうことで、ほとんど問題は起きなくなりました。React Native公式ドキュメントでViewとTextのスタイルを見ながらマークアップしていく様はとても頼もしいものでした。
検証した当時は marginLeft: 'auto'
や %
表記をするとエラーが出ていた記憶があるのですが、本記事を書くにあたって再度検証してみたところ、問題なく使えました。私たちは幻を見ていたのか……
v0.52.0でYogaのmarginLeft: auto
や%
の扱いに手が入ったようなので、このときに直ったのかもしれません。
先程、マークアップエンジニア氏に報告したところ、強くガッツポーズをしておられました。
ネイティブとWebで挙動が逆で戸惑ったパターンとして、flex-direction
のデフォルト値が違う、というものがありました。
プラットフォーム | flex-direction |
---|---|
React Native (for Web) | column |
Web | row |
これに大きくハマったのがマークアップエンジニア氏で、align-items
やjustify-content
の効き方が逆になって、当初は大混乱していました(すぐに慣れました)。
普通のReact Nativeに慣れていた筆者にとってはいつもどおりだったので、まったく気になりませんでした・・・(逆に、たまに普通のWeb開発をするときにブラウザのデフォルトがrowなことに戸惑っています)
React Nativeの頻出課題として、画面遷移をどうやって実現するかという問題があります。AndroidとiOSで画面遷移の考え方が違っていて、どちらかというとiOSよりの考え方で作られていることが多いので、Android出身の筆者はよく混乱しています。
さて、Webの、しかもシングルページアプリケーションの画面遷移となると、ネイティブのそれとは輪をかけて違います。内部的にURIを定義したり、History APIでpushStateするくらいなら、ネイティブもWebも大差ない動きをするのですが、決定的に違う点として、Webでは直リンクやURL削りがカジュアルに行われます。どんなURLで来られても対応できるような対策がWebでの画面遷移には求められます。
画面遷移に関する考え方が違いすぎるので、画面遷移ライブラリはネイティブとWebで別々に用意して、Screenコンポーネントもネイティブ用とWeb用を用意し、Screenの中に配置するコンポーネント(or コンテナ)だけをネイティブとWebで共有することにしました。
それぞれのプラットフォーム向けに採用したライブラリは次のとおりです。
プラットフォーム | ライブラリ |
---|---|
ネイティブ | React Navigation |
Web | React Router |
実はReact NavigationはWebで使えないこともないですし、React RouterはReact Nativeに正式対応しているのですが、下手に共通化するとまずそうな気配を感じたので避けました。もし今後、1つのライブラリで済ませる日が来るとしても、Screenコンポーネントはプラットフォームごとに分けると思います。
React Native for Webのコンポーネントは、順次拡充されています。そのため、たまにコンポーネントが実装されていないことがあります。
一番困ったのはFlatListが未実装だったことです。リスト表示のパフォーマンスを向上させるためにも、できればあってほしかったのですが・・・
無いものは仕方ないので、内部実装を分岐することにしました。
// ChemicalList.tsx export function ChemicalList(props: { chemicals: Chemical[] }) { return Platform.OS === "web" ? <ChemicalListWeb chemicals={props.chemicals} /> : <ChemicalListNative chemicals={props.chemicals} />; } function ChemicalListNative(props: { chemicals: Chemical[] }) { return <FlatList ... /> } function ChemicalListWeb(props: { chemicals: Chemical[] }) { return <ScrollView> { chemicals.map(chemical => ...) } </ScrollView>; }
Web版の実装はデータ数が少ないので何とかなっているのですが、データが増えるとパフォーマンスに影響が出るので、時間を見つけてreact-virtualizedあたりに置き換えたいなあという気持ちはあります。
また、ChemicalList.native.tsx
と ChemicalList.web.tsx
にそれぞれ実装すれば勝手に切り替わる、くらいまでビルド環境が整備できればよかったのですが、時間切れで諦めてしまいました。そもそも native.ts
や native.js
という接尾辞をReact Nativeそのままでは認識できないはずなので、難易度は高そうです。サードパーティバンドラーのHaulにts-loaderを食わせる例ではnative.tsxを認識させようとしているので、今度はこの方向でチャレンジしてみたいです。
前述のとおり、NativeBaseやreact-native-elements*8やreact-native-paperなどのUIライブラリがWeb側では使えませんでした。ボタンやヘッダーなどの共通コンポーネントはすべて自作しています(ここでもマークアップエンジニア氏にはめちゃくちゃ活躍してもらいました)。
困ったといえば困ったのですが、UIライブラリが使えたとしてもかなりのカスタマイズが入っていたことが予想されるので、もしかすると自作で良かった部分もあるのかもしれません。
こちらについても、.(android|ios|native).tsx
と.web.tsx
の切り替えによって、各プラットフォーム向けのUIライブラリを採用できるビルド環境の整備を研究しているところです。
結果的に、ソースコードのかなりの部分が共有できました。実際のファイル数を見ていきましょう。
今回のアプリケーションでは、Lernaでmonorepo化して依存性の方向を整理した、レイヤードアーキテクチャライクな構造になっています。プレゼンテーション層、アプリケーション層、ドメイン層、インフラストラクチャ層の4層になっています。ディレクトリ構造は次のとおりです。
packages/
presentation/src
: UI周り。アプリとしての本体。
native/
: ネイティブにしか関心がない。shared/
: 共有するコンポーネントやコンテナ。⭐web/
: Webにしか関心がない。application/src
: 処理の交通整理を担当。⭐domain/src
: 仕様を直訳した型定義やアルゴリズムを担当。⭐infrastructure/src
: 外界との通信を担当。⭐これらのうち、⭐が付いているディレクトリはネイティブからもWebからも参照される共有コードになっています。手元の最新リリースバージョンでファイル数をカウントしてみます。*9
$ find packages/presentation/src/native -type f | wc -l 49 $ find packages/presentation/src/shared -type f | wc -l 251 $ find packages/presentation/src/web -type f | wc -l 65 $ find packages/application/src -type f | wc -l 21 $ find packages/domain/src -type f | wc -l 59 $ find packages/infrastructure/src -type f | wc -l 87
(251+21+59+87)/(49+251+65+21+59+87) = 0.78571...
で、約78.6%が共有できているようです。
共有したことに由来する問題は今のところチーム内でも聞いていませんし、まずまず共通化については大きな成果が出ているといえそうです。
ネイティブ開発の文脈だと「Flutterに比べて充実してないなあ」という気持ちになるReact Nativeのコンポーネントのラインナップですが、Webで使ってみると普通に嬉しいものがいくつかありました。
keyboardDismissMode
)が便利今回の開発をしながら気付いたのですが、任意のUIフレームワークを自作していく際の材料としては、React Nativeのコンポーネント群は良いバランスをしているんじゃないかと思いました。Webしか開発しない場合にも使ってみたいです。
ScrollViewのスクロール部の内部にパディングをつけたい場合は、styleに指定するのではなく、 contentContainerStyle
に付ける、というReact NativeのTIPSがあります。
なんとこのTIPS、React Native for WebでScrollViewを使っている場合にも有効です。
この挙動に気付いたときは「nicolasさんはどこまで再現しとるんや・・・」と戦慄したものでした。
あまりReact Native for Webの導入事例を聞かないので、プレスリリースが出た記念にこれまでの開発を振り返ってみました。
一昔前までは眉唾または相当の無茶をしないと得られないと思っていた「Webとネイティブのコード共有」というテーマでしたが、そこまで無理をせずに実現できてしまい、戸惑いつつも嬉しく思っています。
「画面のサイズが同じなら、Webとネイティブでまったく同じ動きをしていい」という要件があったからこそ成り立ったものだったので、他所でも同じような選択ができるとは限らないと思いますが、参考になれば幸いです。
今後の保守や機能改善を行なっていく中で、また課題が見つかることもあるかと思いますが、各個撃破していきます。
ウォーターセル株式会社では、一緒にクロスプラットフォームなアプリケーションを作ってくれる、ReactエンジニアやAndroidエンジニアやiOSエンジニアを募集しています。
*1:弊社・農総研さんともに広報チェック済みです
*2:諸事情によりふわっとした言い方にしています
*3:などと言っていたら本当にそれっぽいものをシマンテックの人が作ってしまったのが5月半ばの話なのですが、これについては、個人ブログで紹介しましたので、興味のある方は御覧ください https://blog.nkzn.info/entry/2018/05/26/020312
*4:https://twitter.com/necolas/status/913877194199359488
*5:なお、ピコピコサンプルで音を出したときは、予想に反してWebのほうが実装は楽でした
*6:内容的に本記事と被るところも多いですが、導入の意義については個人ブログにもまとめさせてもらいました https://blog.nkzn.info/entry/2018/05/29/210030
*7:ディスク容量を結構持っていかれるのです
*8:Webへの対応を進めてはいるそうです
*9:本来であればandroid.js, ios.js, web.jsがshared内にある可能性を考慮すべきですが、今回は存在しないことを確認できているので、このカウント方法を採用しています
こんにちは、モバイルチームの中川です。今回はJavaScript文化圏の記事になります。モバイルチームはユーザーの手のひらに価値を届けるためなら何でもするチームなので、必要ならJavaScriptだってやるんです(自分に言い聞かせるように)。それでは始めます。
ソースコードとしてはほとんど同じなんだけど、次のような振り分けをしてアプリとしては別々のものにしたいことってありますよね。
Androidアプリの開発では、build.gradleの中でBuild VariantsのProduct Flavorsとして定義することで、無料版と有料版を切り替えるみたいなことが簡単に行えます。iOSでも(本来の使い方からは外れるかもしれませんが)SchemeやTargetを切り替えることで似たようなビルド体験を実現できます。
以前にも少し触れましたが、弊社にはCordova+ReactなiOSアプリがあります。最近は少しビルド環境を整理して、webpack2でゴリゴリ書いていたのを、Create React Appに閉じ込めることに成功しています。webpack.config.jsをメンテしなくていい生活は最高です。
さて、このたび、このアプリにもProduct Flavors的な実装をすることになりましたが、少し困ったことになりました。webpackならwebpack.config.jsごと差し替えるなり、内部で動的にエントリーポイントを差し替えるなりすればよいのですが、CRAではビルド環境に手を加えられる場所はほとんどありません。
Androidと全く同じくProduct Flavorsと同名のフォルダにコードを入れておけば自動で切り替えてくれるような、そういったものはできませんでした。webpackなら頑張ればできそうな気もしますが、少なくともCRAでは無理です。
ソースコードやリソースごと切り替えるというのは諦めて、アプリケーション内部で動的に差し替える方法を模索することになりました。
動的に差し替えるにしても、アプリケーションに自分がどちらの環境で動いているのか教えてあげなければいけませんね。
そんなときこそ環境変数です。ビルド時点で外から依存性を突っ込んであげましょう。Androidから名前を借りて、今回も依存性の名前を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
ちゃんとappAとして表示されましたね。
次はappBとして実行してみましょう。
それでは、まずはappAとして実行してみましょう。
$ yarn start:appB # or npm run start:appB
上手くいきました。
デバッグ時とリリース時とテスト時で切り替えたい値としては、接続先のホスト名などがありますね。今回の記事の内容を踏まえると、これも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_
な環境変数は、アプリケーション内でグローバルに参照できて、環境を振り分けるには便利なので、使っていきたい機能ですね。
こんにちは。ウォーターセルでモバイルエンジニアとして働いている@Nkznです。
Android開発のお祭り、DroidKaigi 2018が明日に迫ってまいりました。2/8(木), 2/9(金)の2日間開催されるこのカンファレンスに、ウォーターセルからエンジニア(筆者)が登壇しますので、セッション内容について紹介します。
2/9(金) 12:50-13:40にRoom 2で講演します。
マルチプラットフォームなツールは世に数多ありますが、モバイルアプリを作っている皆さんの感覚としては「謎の魔法で動いている」「何かあったときに原因が分からない」といった不安から、採用を躊躇することが多いと思います。
残念ながら、その懸念を完全に払拭することは難しいです。しかし、軽減することはできます。
このセッションでは、React NativeがAndroid上で動くとき、Android Javaのエンジニアにとってはどこからどこまでは魔法で、どこからはAndroidやJavaの理屈で理解できるのかをお話したいと思います。Androidエンジニアにとっては、React Native Androidは(ほぼ)魔法ではない、手のひらに収まる技術である。このセッションを聞いた後、そう思っていただけば幸いです。
Reactの基本のキにあたる部分は説明しますので、Reactや近代JavaScriptについての知識が無い状態で来ていただいてもある程度は理解できると思います。
それでは、皆さんのお越しをお待ちしております。
ウォーターセル株式会社では、農業の役に立つアプリの開発にReact Nativeを活用しています。
モバイルチームの中川@Nkznです。
弊社では、小さなプロジェクトから、少しずつReact Nativeのプロダクション投入を試しています。
DroidKaigi 2017では、2016年2Q頃にReact Nativeを選択した経緯や成果についてのお話をしました。2017年2Qのタイミングでもそれは変わらず、「できる範囲でWrite Once, Run Anywhereしたいなー」といったモチベーションで採用しています。
一方で、異なる観点からも採用のモチベーションがチーム内に生まれていることに気がついたので、紹介しようと思います。
この観点は、チームの歴史と密接したものなので、技術の話というよりは思い出話やポエムの類になってしまうことをご了承ください*1。
ウォーターセルのモバイルチームは、なかなかiOSエンジニアに恵まれない時期が長かったことなどから、アグリノートのiOS版をCordovaで開発することを選択しました*2。
当初はIonicでAngularによる開発を想定していたのですが、WebフロントエンドのチームがReact+Reduxを採用しているため、テクノロジを合わせたほうがノウハウを共有しやすいだろうということで、React+Reduxにmaterial-uiを組み合わせる形になりました*3。
堅牢なJavaに守られながら生きてきたメンバーたちは、最初こそおっかなびっくりといった様子で慣れないES201x(Babel)に触っていましたが、ふと気づけば皆、ラムダやLodashやPromiseがないと生きていけない近代JSerな身体になっていました*4。後述しますがこれが嬉しい誤算を生み出しました。
また、当時は気づかなかったのですが、Reactの特徴のひとつである「レイアウトを表すXML(JSX)とそこに埋め込んだ変数が相互に作用する」というデータバインディングの世界観も、着々とチームに浸透していきました。
さて、Cordova製iOSアプリの開発が一段落付いたら、Androidアプリのメンテにも人を戻していかないといけません。
各メンバーの中で自然と、JSやReactでの開発で出会ったパラダイムをAndroidアプリ開発にも取り入れたいという意識が高まっていました。Retrolambda、Lightweight Stream API、RxJavaなどを採用する際に心理的な障壁が非常に低くなっていたのは嬉しい誤算です。この頃にはAndroidにもデータバインディングライブラリが登場しており、早い段階で採用されていました。
「複雑に状態が変わる画面を、以前はデータバインディング(React含む)無しでどうやって開発していたのか思い出せない」
こんなセリフが聞こえるようになるまで、そう長くはかかりませんでした。
さて、そんなこんなをしているうちに、iOSネイティブ開発ができる人材の都合が付きはじめまして、新規事業のアプリを、AndroidはJava、iOSはSwift3で作ることになりました。
しかしここでつらいことが。iOSには、レイアウトに変数を埋め込むタイプのデータバインディングがないのです。React脳になっていたメンバーがアサインされていたため、非常につらそうでした。
結局、RxSwiftを使って、 myTextField.rx.text
のようなObservableを起点にしたRxのストリームで連鎖的にビューを更新する方式を取りました。「違うんだ……俺が欲しかったデータバインディングはこれじゃないんだ……」という怨嗟の声が聞こえました。
そして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があってもいいのでは、という提案でした。
お久しぶりの投稿になります。中川@Nkznです。
近年は弊社メンバーの主戦場がQiitaになってきており、こちらの更新をする頻度が減ってきておりました。
(EXCEL・R勢の更新頻度が高いのでWeb・モバイル勢も頑張ろうな……!)
今回は会社色の強い内容になったため、久々にこちらを稼働させることにしました。
※React Nativeにしか興味が無い人は読み飛ばしてください
特定の小さいドメイン(使用目的)に寄り添った小機能なアプリです。社内的には「単機能アプリ」と呼んでいることが多いのですが、味気ないので一部ではSimpleAppsと呼ばれています。
費用対効果の高い新事業を模索する一環として始まった、社長直轄プロジェクト*1です。
今回は次の3つのアプリをAndroidとiOSそれぞれに向けてリリースしました。
これらのアプリがどんなモチベーションで作られたのか、ご紹介していきます。
*1:実は社長が付かない"代表取締役"なので当人は社長と呼ばれると嫌がるんですが、このほうが通りがいいので勘弁してください
こんにちは。モバイルチームの中川[twitter:@Nkzn]です。
Androidエンジニアのためのカンファレンス「DroidKaigi」に登壇する機会をいただきまして、サイバーエージェントのセミナールームでお話してきました。
本エントリは、発表原稿としてスライドの元にした文章です。技術的には同一の内容になっています。
www.slideshare.net
ユーザーがどんな場所にアプリを持っていこうと、私達はそれを制止できません。自分たちのWebサービスを携帯網もWi-Fiもない場所でも使ってほしいと思ったとき、私たち開発者には何ができるでしょうか。
GmailアプリやEvernoteアプリのように、オフライン時に閲覧・作成・編集されたデータをサーバーと同期させるための仕組みが、Androidには用意されています。
ただ、サーバーと同期する際のアルゴリズムについては開発者任せです。
本エントリでは、データ同期を実装するにあたって参考にしたものや気にしたことを紹介していきます。
業務システムにはよくある話ですが、マスタデータとトランザクションデータの組み合わせにより農作業記録のデータ構造が作られています。
2012年3月にAndroidアプリのファーストバージョンをリリースしました。この頃は、画面を表示する時点でデータをfetchしてくる方式でしたが、しばらくすると、似たような要望が散見されるようになりました。そう、「圏外の地域でも利用したい」という要望です。
近年では人が住んでいる場所がエリア外ということは減ってきているようですが、山の中だったり、広大な北の大地のど真ん中に行けば、容易に圏外になるそうです。
「現場で使うアプリ」を標榜しているのに、現場で使えないというのも辛いところです。この問題は、長いこと課題として残っていました。
また、アグリノートの導入を機に法人契約で社員にスマホを持たせている法人農家で、通信費のほうが高く付いている事例も聞こえるようになったことも気になっていました。オフライン動作ができる分には、Wi-Fi接続のみのタブレットという選択肢が生まれるので、これもまた大きなモチベーションになりました。
現場では電波が無くてもデータの保存ができて、電波がある場所に戻ってきたときにデータを同期できる。2013年の終わり頃からアプリの全面リニューアルを始めたタイミングで、そんなオフライン機能の検討を始めました。
さて、自動同期の機能を実現しようとしたら、何が必要でしょうか。
こんなところですね。
実は2013年にも仕事で同期機能を持ったAndroidアプリを作成したことがありました。当時の私がなんとか知恵を絞って考えたのが下記の構成です。
android.app.AlarmManager
で定期的にインテントを発行android.app.IntentService
でバックグラウンドプロセスを作成
実際にこの方針で実装していく中で、色んな問題が発生しました。
並行処理プログラミングに強くない人間が首を突っ込んではいけない領域に足を突っ込んでいたような感じでした。
ちなみに同期アルゴリズムとしては、朝イチでログインさせてそのときに前回データを全部消して、それから最近のデータを丸々引っ張ってくるという感じだったので、同期というかただのダウンロードでした。
まずは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を起点として、下記のように各機能を繋いであげる必要があります。
それぞれの役割としては下記のような感じです。
非常に面倒ですね。とはいえ、サンプルがあれば充分に把握可能なものだと思います。
しかし・・・?
GmailやGoogleカレンダーなどはSyncAdapterの便利さを享受していると思われますが、デベロッパーの間での利用が増えてきたのは割と最近のように思われます。
調べてみると、2009年にAndroid 2.0が出てから約4年後、2013年のGoogle I/Oくらいの時期まで、リファレンスの方には断片的に使い方が書いてあったものの、使い方の全体像になる公式ドキュメントが存在しなかったらしいことが見えてきました。
Transferring Data Using Sync Adapters をインターネットアーカイブサービスで調べてみると、初出が2013年7月13日ということになっています。
また、同年のやんざむ氏のGoogle I/Oレポを覗いてみると、SyncAdapterが紹介されているのが伺えます。
前述のとおり、それなりに複雑な設定をしないと動かないものですので、公式ドキュメントの登場により、ようやくデベロッパーが「どんなものか試してみようか」と取り組める下地ができたのではないでしょうか。
[twitter:@vvakame]氏もやんざむ氏の記事と同じことを言ってるので、I/Oのときにそういう話があったのは確かみたいです。
2013年のGoogle IOのAndroid Protips: Making Apps Work Like MagicというセッションでSyncAdapterについて言及されたと俺のログには記録されているな。 #DroidKaigi #DroidKaigiB
— わかめ@毎日猫がいる (@vvakame) 2015年4月25日
Google I/Oでの言及や公式ドキュメントの整備を皮切りに、少しずつまとまった資料が増えてきたように思います。
時期を同じくして、2013年6月に出版された「50 Android Hacks」の中でも、SyncAdapterの使い方が紹介されました。この書籍は江川さんらにより翻訳され、2013年11月に日本語版が出版されています。
この後、2014年の初めにmixi-inc/AndroidTrainingの中で基礎編の中に組み込まれたりしたのを初め、Web上での情報が増えていったような気がします。
AndroidTrainingのSyncAdapter項を書いた [twitter:@KeithYokoma] さんによると、社内の知見をかき集めて何とかしていたようです。
あの当時 SyncAdapter の知見はすべて社内から引っ張り出してきていたの思い出した #DroidKaigi
— KeithYokoma (@KeithYokoma) 2015年4月25日
50 Android Hacksのお陰でSyncAdapterを導入すること自体には大きな問題はありませんでしたが、その一方で、どんなアルゴリズムで同期を実現するかという思想的な部分では悩まされました。
結果的に、参考にした資料は下記の2つです。
SyncAdapterはHack 23として紹介されています。紙面では要点を押さえた感じで紹介されていますが、サンプルコードに入っているTODOアプリがかなりしっかりしており、基本を掴むための助けになりました。
サンプルにはPython製のサーバーやブラウザ版クライアントも同梱されており、データ同期をより実践的な形で体験できます。
Evernoteのクライアント・サーバー間データ授受のプロトコル "Evernote Data Access and Management" の仕様書PDFです。全15ページ。
サーバー、クライアントにそれぞれどんな要素を持たせることで "state based replication" を実現しているのかが解説されています。サーバー側にも手を入れるリソースがあれば、この資料の内容をそのまま模倣するのもアリだと思います。
なお、運営の [twitter:@ninjinkun] さんが日本語訳を用意してくれていたりするので、こちらを読んだほうが楽かもしれません。(発表前日に見つけました)
前述の資料を参考にしながら、仕組みとして取り入れたのが、下記の要素です。
Evernoteからもらってきた考え方です。
Full Syncは完全同期と訳せそうです。その時手に入る情報を全部落としてきてしまう種類の同期です。差分について考えなくてもいい代わりに、データ量が多くなりがちです。
一方、Incremental Syncは逐次同期とでも訳せばよいでしょうか。クライアントがどの時点までのデータを持っているのかをサーバーに伝えることで、それ以降の差分となるデータだけを請求できます。一度の同期に関わるデータの量は少なくなりますので、定期的に同期する際にはこちらの方法のほうが有効です。
アグリノートでは、最終同期時刻 last_fetched
をアプリ内に持っています。API呼び出しのパラメータにこの時刻を載せることで、それ以降に変更のあったデータのみを受け取ることができるのです。
サーバー側の仕組みとしては、各APIに対応するテーブルのmodified_at列を見に行って、パラメータに指定した時刻以降の変更を返すようにしてもらっています。
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での流れを踏襲しました。
last_fetched
が保存されていないとき = 初めて同期するときはFull Syncを行うlast_fetched
が保存されている時 = 既に前回データがある場合はIncremental Syncを行うlast_fetched
に1の時刻を保存するこれで同期が成立しているはずです。
同期の理屈はできましたが、もう一つ考えないといけないことがあります。サーバーが行なったデータ操作と"dirty"なデータの間で競合があった場合です。
具体的には、以下の2つの段階で問題になります。
2. サーバーで削除されていたデータをクライアントでも削除する
3. サーバーで更新されていたデータをクライアントでも更新する
クライアント側のデータが変更されていて、なおかつサーバーからもデータを貰ってしまったとき、取れる選択肢は限られそうです。
せっかく手元で作ったデータですから、送信したいですよね。サーバーに勝たれるのは困ります。クライアント側のデータをそのまま送るか、サーバーから貰ったデータとなんとかマージしてから送るか、といったところになりそうです。
アグリノートアプリではクライアントが勝つことにしてあります。マージといってもどこからどこまでマージしたものか検討するコストが高そうだったからです。
なお、Evernoteで以前試したときには、マージしてから送信する方法を取っていました。EDAMにもそう書かれています。
まあそもそも、ブラウザ対ブラウザでも画面の表示タイミングや保存を押すタイミングによっては起こることですし、そのときには後勝ちになる場合が多いと思うので、この件もクライアント側が勝つのが自然なんじゃないかと思うのです。
さて、流石にそろそろ話がごちゃごちゃしてきたので、頭の中を整理するために同期のパターンを表にまとめてみました。
このへんについてまとめられています。チーム内での認識合わせに役立ちました。
同期が必要になるような要件が出てきたら、この話を思い出してください。
こんな流れで乗り越えられる山があるはずです。
ウォーターセル株式会社では、地球人口100億の時代に見合う食料生産のための農業革命を一緒に引っ張っていってくれるAndroid/iOSエンジニアを探しています。
弊社事業に興味のある方は、是非ご連絡ください。
50 Android Hacksのサンプルは読み応えがあり良い資料なので是非読むべきですし、本の内容も概ね良いのですが、SyncAdapter章に致命的な誤訳があるので気をつけましょう。
原著がサンプルとして公開されているので、英語版と読み比べながら読むくらいでちょうどいいかもしれませんね。(Sample chapter5に該当の章があります)
正誤表が見つからなかったので、この場を借りて紹介しました。もし正誤表のページがあれば教えていただければ幸甚です。