React Nativeで地域SNSアプリを開発している話
この記事は React Native Advent Calendar 2017 20日目です。
PIAZZAという地域SNSのandroidアプリをReact Nativeで開発するに至った経緯と知見です。
最初に
私がReact Nativeでの開発をはじめた当初は、日本語での情報がまだ多くありませんでした。React Nativeのセットアップやライブラリの紹介が多く、開発を進めていくと参考になる情報は少なくなっていきました。
私が知りたかったのは、実際にアプリを最後まで作った方の経験談でした。ちょっと触っただけではなく、実際にアプリをリリースされた方の生きた声を聴きたかった。
そこで自分がアプリをリリースしたら、開発で得た知見をどこかに書きたいと思っていました。 2016年10月にアプリをリリースし、すぐに書きたいと思っていたのですが、忙しさにかまけてどうしても後回しになり…あっという間に1年が過ぎてしまいました。
今回、Advent Calendarに登録して締め切りを設定することにより、何とか重い腰をあげて書くことが出来ました。
React Nativeでのアプリ開発を考えてる方に、少しでも参考になれば幸いです。
ここに書かれている事は、全てandroidアプリ開発時の情報です。また、作業した時点での古い情報も含まれているので、必ず最新の情報を確認してください。
React Nativeを使った理由
React Nativeに対する印象は、その人ができる or 好きなプログラムであったり、開発環境の好みであったりによって大きく変わるのでは?と感じています。そこで私がReact Nativeで感じるメリットを書く前に、私がどんな開発を行なってきたかを少し書きます。
過去の開発経験
PIAZZA株式会社でサーバーサイド全般、androidアプリ、分析全般を行なっています。 webサービスに関わるようになったのはここ4年ほどで、その前はc/c++でゲームや組み込み寄りの仕事がメインでした。
サーバーサイドの仕事は、phpを少々とrailsを3年ぐらい。
androidアプリはandroid 1.6 〜 2.3 あたりの頃にカメラ/画像処理を行う簡単なデモアプリを作った経験が。iosは経験無し。
社会人になってからほとんどの仕事でemacsを使ってます。emacs歴は18年ぐらいでしょうか。
Reactの経験
2015年9月から2016年8月まで、開発に参加したプロジェクトでReact/Fluxを使っていました。
ReactNativeに至るまで
2015年9月
PIAZZAのandroidアプリは、AndroidStudioを使ってjavaでプロトタイプを作っていた時期がありました。(2015年9月〜10月頃)
この時に以下のような不満がありました。
- AndroidStudioが重い。
- レイアウト調整がだるい。
- API覚えるのもだるい。
- ストアにリリースした事は無いが、昔やった事がある開発の延長なので新鮮味が無い。
その後忙しくなり、また上記不満から気分も乗らずandroidアプリの開発はストップしました。
2016年2月
React Native for Androidをはじめて試したのは、2016年2月頃。0.21のリリース前だったのを覚えています。私には以下が大きなメリットでした。
- AndroidStudioを起動せず、 emacsだけで作業がほぼ完結する。
- ホットリロードが素晴らしい。UIの調整がラク。
- React経験があったので、新しく覚えるべき事柄がほとんどない。
ここで大きく気持ちはReact Nativeへ傾きました。 導入するにあたり、ネックになりそうな機能の下調べをしたのですが、この時点ではandroid向けのライブラリがまだ出揃ってなかったり対応されてても動作が不安定/不十分なものが多かった記憶があります。
不安はあったのですが、当時うたよみんがReact Nativeで開発/リリースされていたので、同じようなSNS機能を持つPIAZZAもイケるのでは?と思っていました。
2016年6月
必要な機能についてのライブラリを再度調査した結果、3ヶ月前に比べると選択肢が大分増えていました。これならやれるとReact Nativeで行くことを決断。
調査したものは
- redux
- immutable.js
- flow/typescript
- 非同期処理
- カメラロール(ギャラリー)へのアクセス
- ピンチズームによる画像の拡縮
- ViewPager
- スプラッシュ画面
- メッセージ画面
情報源
開発スタート時点(2016年7月)
参考にしたページ
- https://qiita.com/advent-calendar/2015/react-native
- https://github.com/jondot/awesome-react-native
- http://twins-tech.hatenablog.com/entry/2016/06/05/101916
- http://dackdive.hateblo.jp/entry/2016/06/01/123000
- https://qiita.com/shohey1226/items/0de513a8b5b863286eb2
- https://qiita.com/pullphone/items/d28baeb296666a4847b8
- https://qiita.com/kuy/items/716affc808ebb3e1e8ac#_reference-f9a32ecf7f4c6bdbe609
- http://blog.bokuweb.me/entry/redux-tutorial
- https://github.com/chentsulin/react-native-counter-ios-android
とにかくreduxがすんなり頭に入ってこなかった。 色んな記事を見たが最終的には、公式のドキュメントやサンプルを丁寧に読んだ上で、手を動かすとしっくりきました。
2016年8月以降
ある程度コードを書き始めると、必要な情報は英語でしか得られなくなってきました。
- React NativeのRelease Note
- エラーやトラブルでググると大体、ReactNativeや関連ライブラリのissueへたどり着く事が多くなる。
- facebookグループ: React Native Community
- twitter, medium
開発について
ここからはPIAZZAのandroidアプリをどう実装していったかについて書きます。
開発時のメモをがあるので、そこから当時苦労した事も載せます。
今は必要ない情報ですが、今後何かあった時に過去のトラブルが頭に入ってると、解決のヒントになると思うので。
開発態勢
- メンバーは自分ひとり。
- デザインは既にリリース済みのiosアプリに合わせる。
- apiも自分で書く。(既にios向けに使ってるものと共用。)
開発環境
- ES6
- Airbnbのスタイルガイドに合わせる
emaceの設定
こちらを参考にしました。
主要ライブラリ
flow
独自に型定義は作らず、単純に変数と引数の型チェックのみ。
redux
action, reducerを書くのがだんだん面倒になってきて手を入れたくなってきたが、独自仕様を追加すると今後人が増えたときに混乱するかなーと思って愚直に書き続けている。
Immutable.js
reducerでしか使っていない。
redux-saga
こちらの記事で知って、公式のドキュンメント読んだらすぐしっくり来たのでそのまま使ってます。
react-native-router-flux
v3.40.1を使用中。 2016年7月時点では、これがrouterの第一候補でした。クセはあるが、 動かすまでが早かった。今後はv4にするか他のライブラリへ移行するか考え中…
navbarが非表示の画面からnavbarが表示される画面にダイレクトに遷移するとnavbarが表示されないという謎仕様があり、それを回避するためにSceneを必ず1段wrapして使っている。
React Native FBSDK
2016年8月の導入時は、コンパイルエラーが発生して自力で解決した。
facebookログイン、投稿のシェア、招待機能に使用。
招待機能はドキュメントが見つからなくて、こちらを参考にした。
superagent
Reactでの開発時に使っていたので、そのまま使用。
画像のpostが動かなかったので、こちらを参考に投稿やプロフィール編集のみ、XMLHttpRequestを使っている。
デバッグ
- reactotronを使ってます。
- デバッグprintはconfファイルを作ってconsole.logとReactotron.logを切り替えられるように。
- version名に開発verと分かる特定の文字列を入れて、開発verの場合はapi接続先を選択可能に。
ディレクトリ構成
プロジェクトルートにsrcを作ってソースコードはそちらに。
── src
│ ├── Routes.js // scene定義
│ ├── actions // action定義
│ ├── apis // apiコール
│ ├── components // component置き場
│ ├── config // 設定関連
│ ├── constants // 固定値置き場
│ ├── containers // reduxと接続する画面を管理するcomponent。
│ ├── decls // 未使用
│ ├── reducers // reducer置き場
│ ├── sagas // redux-saga
│ ├── store // createStore
│ └── utility // 雑多
各画面毎に、container, action, api, reducer, sagaのファイルを作成。
component化の判断は複数画面で使いまわすかどうか。 constantsにある固定値はアプリ全体で共通化したい定数。Color, Font等。後は画像のpathもこちらに。
現在のソースコードは、約23000行。画面数は約40。一番大きいファイルで800行ぐらい。
ソースファイル
component/containerの典型的なコード構成は以下のような感じです。
HogeView.js
/* -*- mode: web -*- */
import React from 'react'
// importいろいろ// @flowconst styles = StyleSheet.create({
// style定義いろいろ
});type Props = {
// props定義
};type State = {
// state定義
}export default class HogeView extends React.Component {
constructor(props) {
super(props);
// 初期化処理
} state State; // react lifecycle method
componentWillReceiveProps(nextProps) {
// setStateしたり
} // callbacks
onPress() {
} // その他関数 render() {
return (
// 表示
)
}
}
propsの全渡しは何が使われてるかわからなくなるので使っていない。
開発初期の実装の流れ
実装系統を以下のようにわけて、それぞれの系統から1つずつ先に実装。
その後、同じ手順で残りの画面を実装していった。
- リスト表示系(タイムライン等)
- ログイン周り
- 投稿/プロフィール編集(post系画面)
- その他
実装のポイント
画面への反映
遷移状況によって、同じ画面が違うパラメータで呼ばれしまうケース。
例えば、投稿詳細A ▶ あるユーザーのタイムライン ▶ 投稿詳細B
このとき、投稿詳細Bで通信が走ると投稿詳細用のreducerでstateが更新されるため、投稿詳細Aでも、componentWillReceiveProps()が呼ばれる。
何も考えずにnextPropsを受け取って更新してしまうと、投稿詳細Aと投稿詳細Bが同じ内容になってしまうので、投稿詳細で表示する内容は画面内のstateに持ち、nextPropsを受け取った際は、内部stateと(例えば)idが一致した場合のみ更新するようにした。
いいねを複数画面へ反映
タブが複数あり、それぞれのタブでタイムラインや投稿が表示される。
タイムラインにも投稿詳細画面にもいいねボタンがあり、どこでいいねをしても、自分のアクションが反映されている必要がある。
いいねの実行後、通信結果を受け取った後に各画面毎の投稿をupdateするactionにいいねされた投稿idを渡して、該当する投稿があればupdateすることで対応。
deep link
- 通常のandroidアプリと同様に、AndroidManifest.xmlにintent-filterを追加
- routeとなるスクリーンの componentDidMount() で Linking.addEventListener(‘url’, this.handleDeepLink)
- 同様にcomponentWillUnmount() で Linking.removeEventListener(‘url’, this.handleDeepLink)
- handleDeepLinkの実装は以下のような感じ
handleDeepLink(event: Object) {
// event.urlにpathが入ってるので、それに合わせて画面遷移
}
文字列のtruncate
最初はsubstr使って文字数で制御してたが、こちらのブログでText compoentに標準で用意されてるの知りました。行数で指定できるので便利。公式ドキュメントをちゃんと読まねばダメだなと思った次第。
通信と画面遷移のanimationが重なると処理落ち
componentWillMount()でfetchを呼ぶ画面が、通信状況によってどうしても引っかかるような挙動になる。
対策として以下を試しているが、解決に至っていない。
- 画面遷移animation終了後にfetchを呼ぶ。(体感ではかなり遅く感じる…)
- componentWillMount()でfetchを呼ぶ。取得した値はcacheして、画面遷移animation終了後にsetState()して画面更新かける。
React Navigationベースになるreact-native-router-flux@4系へ移行したら改善するんだろうか。
push通知
- react-native-fcmを使用
- サーバー側はAWS sns。api keyはFirebaseのダッシュボード▶project settings▶CLOUD MESSAGINGにあるServer key (legacy token)というのを使う。
- FCMへ送るjsonはこちらを参考に
クリッピボードにコピー
React Nativeに用意されている。
https://facebook.github.io/react-native/docs/clipboard.html
animated header
こちらを参考に実装。
汎用ScrollViewの作成
タイムライン内で横スクロールリストを表示する必要があるため、ListViewは使わずScrollViewを使っていた。
開発が進むうちにリスト系の表示に使う処理をまとめるために汎用ScrollViewを作成して全てのリスト表示はこれを使うようにした。主な機能は、
- 縦/横リストの指定
- 引っ張って更新
- スクロールしたら自動追加fetch
タブレットでのレイアウト崩れ
画像を幅一杯に表示する場合に高さを数値で指定すると意図した比率にならない。(当たり前か。。) 基準の画面サイズから比率を割り出し、スクリーンサイズに応じて高さを計算するようにした。
modal
こちらのコードを参考に自作した。
その他使用ライブラリ
React Native AutoLink
文字列への自動リンクに使用。
自動でリンクが押せるようになるが、当たり判定がやや上にずれてる?
react-native-lazyload
遅延描画component。
react-native-datepicker
イベント投稿の開始/終了日時に使用。
当初はプロフィール編集の生年月日入力にも使っていたが、android純正のdatepickerは年の選択が分からりづらいとの声があり、そちらは自前実装に。
react-native-image-zoom
androidのみ対応の画像拡縮Comnponent。
modalでラップして使用してます。
react-native-gifted-chat
メッセージ画面に使用。
素晴らしいライブラリです。1日で画面実装できた。
react-native-action-sheet
投稿のmore menu等で使用。
react-native-collapsible
ライセンス画面で使用。
react-native-swiper
walkthroughで使用。
react-native-animatabl
♡をタップした際のanimationに使用。
react-native-device-info
versionの取得に使用。
react-native-vector-icons
検索用のicon等に使用
react-native-icon-checkbox
チェックボックス
react-native-image-picker
ギャラリー
react-native-modalbox
上から下に降りてくるmodalが欲しかったので使用。
react-native-scrollable-tab-view
ViewPager
react-native-shared-preferences
SharedPreferences
rn-splash-screen
スプラッシュ画面
過去のハマり
React Native
0.33.0
onActivityResultのinterface変更
プロジェクト内の全てのライブラリで新しい書式に対応していないと落ちる。最初fbログインで落ちてたのでReact Native FBSDKをずっと調べてたが、結局違うライブラリを最新版にupdateしたら解決した。
0.34.0
TextのlineHeightが1行目のみ正しく表示されない(0.34.1で解決)
0.39.0
css-layoutのバグ修正のため、子compoenetからflex: 1を削除しないとレイアウトが崩れるように
0.42.0
TextInputで改行が入力できない
※現在は修正された模様
回避するためのwrapperを作ってくれた方がいるので、こちらを使わせてもらってます。
使用してるライブラリ内でも一部TextInputを使ってるものがあるので、ローカルでpatch作って対処。
0.44.0
updagra時にトラブル。 https://github.com/facebook/react-native/issues/13390 https://github.com/facebook/react-native/issues/13314
依存するReactのversionが、16.0.x.-alphaになったあたり。
私の環境では、
"react-addons-pure-render-mixin": "^15.5.0",
"react-static-container": "^1.0.1",
"react-native-router-flux": "3.39.1"
で起動するようになった。約半日のハマり。 https://github.com/aksonov/react-native-router-flux/issues/1816 これに限らずreact-native-router-fluxは細かいトラブルが多かったです。
react-native-autocomplete-input
入力補完用component。(現在はPIAZZAの仕様変更により未使用)
ScrollView内でこのComponent使う場合は、keyboardShouldPersistTapsをScrollViewにセットしないといけない。
スプラッシュ画像がresizeされてクラッシュ
1920*1080しか画像を準備していなかったため高解像度の端末で拡大される際にメモリ不足でクラッシュした。
drawable-xxxhdpiに大きいサイズのスプラッシュ画像を置いて解決。
react-native-gifted-chat
huaweiの一部端末でメッセージが表示されない
根本の問題はこれ。RN0.48.0では修正されてるとのこと。
PIAZZAはRN0.45.1なので以下で対処
- react-native-gifted-chat@0.28.0にupdate
- native-invertible-scroll-viewにpatchをあてる
React Native FBSDK
ビルドエラー(リンク先を参照してください)
emailが取れなくなった
これはFacebook api側の挙動が変わったせいだろうか? 最初に実装した時は問題無かったが、ある時から
LoginManager.logInWithReadPermissions(['public_profile', 'email'])
と指定しないとemailが取れなくなっていた。
リリース
コマンドラインから、
cd android && ./gradlew assembleRelease
して、できたパッケージをgoogle playへアップロードするだけです。
終わりに
以上のような感じで、React Nativeによるandroidアプリを開発/運営しています。 振り返ってみると、多少なりともjavaでのandroid開発経験があること、そしてReactも経験済みだった事が導入を決心する上で大きな要因になったのかなと思います。どうしてもダメだったらjavaで何とかなるだろうと。
また、スタートアップという環境だからこそ、今回のように自分の好きな/気になる技術を実践投入できたと思っています。