前置き
僕が所属しているソニックガーデンではRemottyというツールを自社で開発し、普段の業務に使っています。リモートワークをするためにオフィスと言ってもいいような場所で、業務中は常にオンラインとなり、リアルタイムチャット(対面の会話の代替)とトピックベースの掲示板(メールなど非同期コミュニケーションの代替)の両方を備えたオフィスとなるようなツールです。
オフィスにいられない間にもチャットや掲示板に反応できるように、iOSとandroidそれぞれにネイティブアプリのクライアントアプリがあり、それぞれを別の開発者が担当していました。iOS版はその当時新卒2年目だった若者が作ったものを僕が引き継ぐ形でメンテしていました。
もともと自社向けに開発していたプロダクトではありますが、働き方改革の流れなどの関係もあってここ1年〜2年ぐらいの間に有償で使ってくださるお客さまが出てきました。
そうなってくると自社では必要のない機能も、お客さんの組織体制によってはないと困るものがでてきます。オフィスを代替するようなツールなので働き方にモロに影響するんですよね。
そういった経緯から本体であるWeb側に大きめの変更が入るようになります。アプリに影響のないように開発をするものの、全く新しい機能については流石に影響なしというわけにはいかなくなりました。
とは言え、Remottyのスマホアプリはそれを主な業務として開発しているわけではなかったこともあって、iOS/android両方に大幅な変更をするのはとても時間がかかるだろうということがわかりました。
そんなとき、React Nativeに興味を持ってアプリを書き始めていたCTOにそそのかされる形で、僕がReact Nativeを習得して一人で両対応版を開発していこうということになりました。
前置き終わり。
Getting Started
ES5系しか知らないし、WebのReactならほんのちょっとわかるって状態だったので、ほんとにできるかなーという気持ちでした。
というわけでいくつかチュートリアルを進めつつ、慣れていくことにしました。基本は公式ドキュメントのはずなので、このGetting Startedから進めていきました。
ここを一通りやる主な標準コンポーネントを使うとどんな画面になるのかイメージがわきます。画面単位では大体どんな風にすればいいのかはわかってくるようになりました。
ここからアプリとして体をなすまでに乗り越えるべきハードルはまだ幾つかあります。その中で特に画面遷移やデータ管理が特に大きいものなんじゃないかと思います。
画面遷移
今でこそReact NativeのドキュメントにNavigation Between Screens
という項目があります、
が、アプリを書き換え始める時期にはこのようなページはなかったと記憶しています。ここに登場するreact-navigation
は当時ベータ版の初期でした。
画面遷移系のライブラリとしてはreact-native-router-flux
がよく使われていた印象でした。他にもairbnbやwixのような企業のライブラリがありました。
- https://github.com/aksonov/react-native-router-flux
- https://github.com/airbnb/native-navigation
- https://github.com/wix/react-native-navigation
それぞれどのような使い方ができるのかを知るためにスパイクを行うことにしました。スパイクはアジャイルの文脈に登場する言葉で、事前にリスクや仕様を把握したりする作業のことです。
結果どれも使ってみてしっくりこず、ベータ版だったreact-navigation
を使ってみました。そしたらこれがなぜか手に馴染む。使いたかったタブとスタックのナビゲーションがシンプルに書けたこと、遷移のコードがシンプルだったこと、などからベータだけどこれでいいんじゃね?となりました。
結局react-navigation
が公式のようになっていることを考えると良い選択をしたなと思います。ただまだベータ版使ってるんだよなあ。更新に追随するのが少し大変です。
データ管理
次はデータ管理です。WebでReactを使ったちょっとしたサイトを作ったことがあり、パラメータの引き回しやデータ更新の反映が面倒なことはわかっていました。ですのでこのアプリを開発する前からReduxを使ってみようと考えていました。
Reduxはその当時でもReactのプロダクトでよく使われているような雰囲気がありました。関連するQiita記事なども多くみられたため躓くことはないだろうと採用に至りました。
こちらも最初は小さめのサンプルアプリを書くなどして手触りを知ってから使い始めました。素のReactでプロパティを引き回していたことを考えると劇的に簡単になったなぁと当時は思ったと記憶しています。
しかし今思えばReduxは大きすぎるフレームワークだったなと思います。やりたかったのはデータを一元管理して更新があったら再描画することだけです。最近ではその役目にはMobXのほうが適しているだろうという声もあります。
この辺。
その他必要になった技術要素
さて、ここからは個別の話なのでダイジェストでお送りし、気が向いたら個別に詳細を書いてみようと思います。
APIリクエスト
Web版Remottyとやり取りをするためのアプリなので当然RemottyのWeb APIを叩きます。API呼び出しをまとめてApiClient
を作って中でfetch
を呼ぶようにしました。
class ApiClient {
getComment(args) {}
postNewComment(args) {}
}
OAuth
Web側のRemottyはOAuthに対応しています。過去のiOS/androidのアプリもそれぞれOAuthで認証してアクセストークンを取得していました。
OAuthにはこちらのライブラリを利用しました。
2017年4月頃の時点ではiOSは動く状態だったものの、android側のネイティブ実装が不足している状態で、自前のOAuthプロバイダーでは認証できない状態でした。
そこでこんなissueを立てて質問したところやりとりをしてくれて、無事対応したバージョンを使えるようになりました。
データ永続化
RemottyはWebが主体となるサービスであるため、端末に情報を保存しておく必要はほとんどありません。都度最新の情報をWeb APIで取得するのを基本としています。
ただ、認証情報だけは別です。毎度ログインが必要なのでは使い物になりませんので、Remottyではアプリ内の領域にアクセストークンのようなものを保存しています。
具体的にはRedux管理下にあるstoreを保存できる仕組みである、redux-persistを利用しました。redux-persistは環境に合ったストレージを指定することで色々な環境で使えます。React NativeではAsyncStorageが使われます。
Push通知
Remottyではアプリを起動していない状態やバックグラウンドのときのプッシュ通知はもちろん、アプリがフォアグラウンドにいるときにコメントなどをリアルタイムに同期する仕組みにおいてもプッシュ通知の仕組みを使います。
iOS、androidはそれぞれ異なる仕組みなのですが、JS層でうまいことラップしてくれるライブラリがあり、それを導入してアプリ側の処理を上手いこと一本化できました。
リッチテキスト
Remottyはマークダウン形式に対応しています。また、本文中のURLはリンクになるなど、プレーンテキストよりはリッチな表現ができます。
これをアプリで表現するには実はまだ課題があるものの、最低限のラインを満たすためにRemottyではWeb APIでHTMLに変換済みの文字列を返し、アプリ側ではHTMLとして描画するようにしました。
その際こちらを使うようにしました。
最近また新しくなっていそうなので、アップデートしてみようかな、、。
メンション
Remottyでは@ユーザーid
で宛先を指定すると通知を飛ばす機能があります。Slackにあるようなアレですね。
アプリからもメンションする際にメンバーのidが補完されないと不便なので、テキスト入力欄にはメンション補完が欲しいなあと。ちょうどよさそうなライブラリがあったので入力に応じてインクリメンタルに絞り込んでいく機能をつけました。
レイアウトを整えるために必要なプロパティを受け付けてくれないコンポーネントだったので、プルリクをなげて取り込んでもらいました。
Add a prop for top View style by pi-chan · Pull Request #10 · harshq/react-native-mentions
画像添付
Remottyのチャットや掲示板には画像を添付できます。アプリからは画像のみを許可しています。
画像をアップロードするのはなかなか複雑で、色々なライブラリを組み合わせて実装しました。
画像選択
リサイズ
ファイルアクセス
アップロード
流れとしては
- react-native-image-pickerで画像選択
- リサイズするかしないからの選択シートを表示(リサイズする場合3へ、そうでなければ5へ)
- react-native-image-resizerでリサイズし、リサイズ済画像をアプリ領域に保存
- react-native-fsを使ってファイルからリサイズ済の画像をbase64として読み込む
- react-native-fetch-blobで
multipart/form-data
としてAPIにPOST
としてます。アップロード範囲を切り取りできるようにもしたいなーと思いつつただでさえ複雑な処理がパンクしそうなので保留しています。
CodePush
CodePushはMicrosoftが提供するVisual Studio App Center
の機能の一部で、React Nativeのアプリの中に埋め込まれているJS側のコードを置き換えられる仕組みを提供するものです。
React Nativeアプリはネイティブコードからなる部分とその上で動くJSのコードがあり、CodePushにより後者のコードを置き換えられます。つまりネイティブコードを変更しない限りアプリストアに新しいバージョンをリリースせずにアプリを更新できるようになります。
UIの微調整などはCodePushでとても簡単に修正できますし、リリース後に見つかった不具合を審査を待たずして修正することなんかもできます。
個人的な課題感
さて最後に個人的に課題に思っていることがあるので、書いてみます。
android版の苦労と課題
開発するうえでの苦労という観点と、ユーザー体験という意味での課題という観点があります。
まず開発するうえでの苦労です。 僕個人がiPhoneユーザーということもあり、iOSファーストで開発していました。するといざandroidで動かそうにもなかなか動かなくて困ることがありました。起動しないとかビルド通らないとかね。 またReact Native自信が提供する機能や、各種ライブラリの対応状況などをみた限りでもiOSの対応状況のほうが充実していたように思えます。
そのためandroid版の開発はハマりが発生することが多かったような気がします。android側に対応するためにプルリクを出す、forkして一時的にそちらを使う、そもそも動かないからライブラリを捨てる、などということをよくしました。
もう1つのユーザー体験という意味では、これはまあほぼ僕の責任なんですが、androidを常用していないiPhoneユーザーが一人で作ったアプリなので、androidアプリっぽくありません。
戻るボタンの挙動など、androidユーザーに指摘されるまで気づきもしませんでしたし、キーボードが出たときのアプリの表示なんかもiOSと違う癖もあったりして、androidらしくは全然できませんでした。
かといってUIを分けきってしまうと両対応する旨味がなくなってしまうので悩ましいです。フロント側を特に凝りたい、こだわりたいようなアプリでは、ReactNativeに対応するにしても画面は別にしていくんでしょうね。と思っています。
JSワカラン
ES2015等新しい仕様への不慣れがありました。書きながらだんだん慣れていきましたが、ベストプラクティスから程遠いみたいなところもまだまだありそうです。
Reduxが、、
Remottyのアプリで使うにはReduxはオーバースペックでした。前述の通りMobXが適切だったかなと。これは時期的なものもあるとは思うので失敗とは思っていませんが、次に作るアプリはMobXにします。
終わり
できあがったRemottyのアプリはこちらです。
弊社社長のリモートワークに関する書籍も出ていますよ。