mobxjs/mobx
Simple, scalable state management. Contribute to mobxjs/mobx development by creating an account on GitHub.
https://github.com/mobxjs/mobx
このたび、スノウロビンでは React Native でのアプリ開発にチャレンジしました。
本記事では入門レベルを超えてプロダクションに採用してわかった知見を紹介します。
医者と患者に分かれて、ビデオ通話とチャットができる、医療系ビデオ通話アプリを開発しました。ビデオ通話とチャットにはTwilioの Programmable Video と Programmable Chat というサービスを使っています。
以下の4種類のアプリを開発しました。
4種類のアプリでしたが、 iOS / Android ともにデザイン・基本的な挙動は同じということで、効率よく開発するためにクロスプラットフォーム技術の採用を検討しました。
React Native, Xamarin が候補に出ましたが、React Native を採用することになりました。
社内メンバーの技術スタックでは JavaScript のほうが得意だったということ、さらに採用当時国内でもチラホラと React Native 採用事例がでており興味を持っていたことが理由です。
案件開始当初はプレーンな JavaScript で書いていました。しかし、アプリの肥大化が想定されたため、型チェック機構の必要性を感じ、TypeScript を採用しました。
TypeScript には本当に助けられており、複数箇所にわたる変更に臆することがなくなりました。また、型によるサポートは乱暴な言い方をすると「より頭を使わずコードを書くことができる」ように感じます。型の調べにのせてスムーズにコードを書けます。
個人的には1ファイルほどの書き捨てスクリプト以外は基本的に TypeScript を採用するようにしています。
Redux と同じように状態管理を行うライブラリです(Redux と同じ Flux ライブラリではありません)。
使ってみて感じる MobX の特徴は以下です。
今回の案件もスタートこそ Redux だったものの、TypeScript と組み合わせたときの記述量の多さがどうしても気になっていました。Action まわりで Action の型定義、Flux Standard Action における actionType を定義、各 Action を Pipe でつないだ各モジュールごとのアクション。以下の画像のようになります。
上が Redux の Action, Action Creator 定義に比べ MobX では Action, Action Creator, Reducer 相当の定義を以下の量で記述できます。
class YearVersionStore {
@observable
availableVersions: YearVersion[] = []
@observable
selectedVersion?: YearVersion
@action
loadYearVersions(availableVersions: YearVersion[], selectedVersion: YearVersion): void {
this.availableVersions = availableVersions
this.selectedVersion = selectedVersion
}
}
React Native アプリ開発におけるライブラリ選定でもっとも重要なのはナビゲーション管理ライブラリではないでしょうか。React Native パッケージには iOS, Android 両方をサポートしたナビゲーション機構が含まれておらず、なんらかのサードパーティライブラリを選定することになります。
本案件では react-native-router-flux を採用しました。
react-native-router-flux の強みは「ナビゲーション構成を JSX で書けること」です。コードの見た目が react-router 採用時と似たようなものになり、とっつきやすいと考えました。
代表的なナビゲーション管理ライブラリに react-navigation があります。react-navigation と比較したときの react-native-router-flux のメリットとして、react-native-router-flux ではひとつの render 内でアプリ中のルーティングをすべて表現することができます。
以下が react-navigation のとき。
//
// コードは https://reactnavigation.org/docs/en/tab-based-navigation.html から抜粋
//
const HomeStack = createStackNavigator({
Home: HomeScreen,
Details: DetailsScreen,
});
const SettingsStack = createStackNavigator({
Settings: SettingsScreen,
Details: DetailsScreen,
});
export default createBottomTabNavigator(
{
Home: HomeStack,
Settings: SettingsStack,
},
{
/* Other configuration remains unchanged */
}
);
以下が react-native-router-flux のときです。
render() {
return (
<Router>
<Stack key="root">
<Stack key="homeStack">
<Scene key="home" component={HomeScreen} />
<Scene key="details" component={DetailsScreen} />
</Stack>
<Stack key="settingsStack">
<Scene key="settings" component={SettingsScreen} />
<Scene key="details" component={DetailsScreen} />
</Stack>
</Stack>
</Router>
)
}
しかし Qiita や他社テックブログの React Native 記事を読むと React Navigation を採用するのが定番のようです。React Native 公式でも推されています。
実は react-native-router-flux では内部では React Navigation が使われています。そのため、バグが React Navigation に依存していた場合、React Navigation の修正後、react-native-router-flux に取り込んでリリースとなり、対応が遅れてしまいます。
本案件では react-native-router-flux を使いましたが、今後 React Native アプリを作成することがあれば、React Navigation の採用を検討するでしょう。
本案件のモバイルアプリ向け部分の開発者は4人です。内訳は以下。
React Native エンジニアは本開発のメインメンバーであり、基本的に JavaScript を書き、ネイティブモジュール導入時、ビルドまわりで Xcode, Android Studio をたまにさわる、という感じです。筆者もその1人です。
前述したように本案件では、医者アプリと患者アプリがあります。React Native エンジニアのうち 1人は医者アプリの開発、1人は患者アプリの開発を行いました。
モバイルアプリエンジニアは iOS / Android 担当を1人づつアサインしました。役割としては React Native と iOS / Android プラットフォームを結ぶブリッジ(ネイティブモジュール)の開発、および技術相談役です。
モバイルアプリエンジニアには医者・患者アプリをまたいでそれぞれ iOS / Android を担当してもらいました。
ここでいうバージョンとは iOS / Android ではありません。医者・患者アプリのことを指します。
医者・患者の2つのアプリをモバイルアプリエンジニアが作るとなると、 iOS エンジニアが iOS 向けに医者・患者アプリを、 Android エンジニアが Android 向けに医者・患者アプリを開発することになります。これは一人の開発者が医者・患者アプリ 両方の仕様について知る必要があり、困難です。
React Native を採用したことで医者・患者アプリ、どちらか片方のアプリの知識だけで開発することができました。
また、React Native の大きなメリットであるプラットフォーム間のコード共通化ですが、今回のアプリは iOS / Android とも基本的に見た目は同じだったため、ほとんどのコードを共通化させることができました。具体的には *.ios.tsx, *.android.tsx
というファイルは存在せず、コンポーネントのプロパティで iOS / Android 特有の値を渡したり、パーミッションまわりで条件分岐が発生したくらいでしょうか。
React Native およびその周辺ライブラリについては本当にバグが多いです。開発期間中、GitHub の issue を見る機会がとても多くなりました。
例えばバグで iOS で日本語入力を行っても、変換対象とならずに確定されてしまう問題がありました(React Native v0.53.0 で発生していた。記事執筆時点の最新版 v0.57.0 で発生するかは未確認)。わかりやすく説明すると、「あ」と入力したとき、「あ」として確定されてしまい「亜」や「有」などに変換することが不可能となっていました。これは issue に「React Native パッケージ中の objective-c のコードを編集すれば解決する」と書いてあり、従うとたしかに修正されました。しかし React Native パッケージに未マージだったため、React Native をフォークしその修正を取り込んでいます。
記事執筆時点、2018年9月25日時点で React Native の最新バージョンは v0.57.0 である、つまり v1.0.0 を超えていないことからもうかがえるように、なかなか動作が安定しておりません。
React Native は「Web フロントエンド技術だけでアプリが作れる」と紹介されることが多いですが、それをプロダクション採用できるかどうかは話が別です。
とくにビルドまわり(Xcode, Android Stuidio および Gradle )は基本的には避けて通ることができないかつ、未経験にはなかなか難しく、何度もモバイルアプリエンジニアの力を借りました。iOS, Android 環境に依存するライブラリを導入するとビルドがコケることが何度かありました。
パーミッションまわりも筆者は当初苦戦しました。Android のランタイムパーミッションや iOS でパーミッション文言を多言語化することについてなどです。
また、どうしても React Native 標準で実現しづらい内容についてはネイティブモジュール(各プラットフォームと JavaScript を結ぶブリッジ)をモバイルアプリエンジニアに作ってもらいました。
以下、今回のアプリで作成されたネイティブモジュールです。
以下では React Native アプリを開発する際の小ネタを紹介します。小ネタではありますがどれも重要です。
React Native アプリにはスプラッシュスクリーンが一瞬だけホワイトアウトするよろしくない挙動があります。スプラッシュスクリーンの直後、一瞬だけチラつくのが確認できますでしょうか。
これがなぜかというと React Native アプリの仕組みに原因がります。React Native アプリの初期ロードは以下のような流れになっています。
ここで 3. のロードのとき、ビューの情報は JavaScript で書いているため何も描画するものがなく、真っ白な画面がチラつくことになります。上の Gif は、React Native プロジェクト作成直後で読み込む JS ファイルが小さいため、ホワイトアウトはほんの一瞬ですが開発が進むにつれその時間は長くなっていきます。
対策は crazycodeboy/react-native-splash-screen を採用することです。内容は以下が詳しいです。
crazycodeboy/react-native-splash-screen を適用する前が以下。
crazycodeboy/react-native-splash-screen を適用したあとはこちらです。
デバッグ中だけ動作するコードがある
シミュレーターで動作確認したコードが実機になった途端、動かないということがありました。例えば String.prototype.padStart です。
というのも、React Native のデバッグモード中は Chrome の V8 エンジンで実行されます。通常モードは JavaScript Core と呼ばれる iOS / Android 内部のランタイムで実行されます。
つまり、デバッグ中の V8 エンジンでは動作して、通常モードのランタイムである JavaScript Core ではサポートされていないコードは、デバッグを切ると動作しなくなってしまうのです。
これは React Native 特有ではなく、 JavaScript / TypeScript について適用できる内容です。
import 時のファイルパスは相対パスで記述されますが、これはリファクタリングに弱いです。 例えばsrc/screens/HomeScreen/HomeScreenHeader.tsx
を src/components/HomeScreenHeader.tsx
へと移動させる場合、 HomeScreenHeader の import 部分のパスが変わってしまいます。エイリアスを設定し、絶対パスにすることで import パスを変更する必要がなくなります。(もっとも最近では VSCode や WebStorm のリファクタリングで import のパスを変更してくれるようになっています)
このような相対パスでの記述から
import { HomeScreenHeader } from '../../screens/HomeScreen/HomeScreenHeader'
以下のような src ディレクトリから
絶対パスでの記述にするということです。
import { HomeScreenHeader } from '@/screens/HomeScreen/HomeScreenHeader'
本案件の JavaScript ビルドまわりの構成は以下です。
このときエイリアスを実現するために以下の設定をしています。
tsconfig.json
// tsconfig.json
{
// ...
"paths": {
"@/*": ["src/*"]
},
// ...
}
.babelrc
{
"presets": ["react-native"],
"plugins": [
["module-resolver", { "alias": { "@": "./build" } }]
]
}
TypeScript の path はあくまでパス解決のためだけに使われ import のパスを絶対パスから相対パスへと変換してくれません。tsc から出力されたコードは依然以下のようになっています。
import { Foo } from '@/utils/foo'
このとき以下のように変換するため、 babel で module-resolver を使っています。
import { Foo } from 'build/utils/foo'
これまた React Native とは関係なく、JavaScript についてです。
よく争点となっている「export は default を使うか名前付き export のみにするか」ですが、個人的にはexport default を禁止にしています。
理由は以下です。
詳しくは下のページがまとまっています。
また、 import / export まわりのルールとして、 export するモジュール名はプロジェクト中で一意となるようにしています。
これはエディタと相性をよくするためです。モジュール名が一意であれば自動 import の候補が少なくなり、スムーズにコードを書くことができるからです。
ただこのルールと export default 禁止した場合に 1 つ問題があります。
たとえば react-redux で connect する FooComponent
があったとします。
export const FooComponentComponent = (props) => {
return (
<Text>foo <Text>{props.text}</Text></Text>
)
}
export const FooComponent = connect((state) => ({
text: state.foo.text
}))(FooComponentComponent)
外からは connect しているかどうかは知りたくないので connect したコンポーネントを ConnectedFooComponent
といったような名前にするのは嫌です。そのため、素朴な FooComponent が FooComponentComponent
とったような苦し紛れの名前になっています。connect してないFooComponentComponent
はコンポーネントカタログやテスティングで使用したいため、export する必要がありますし、 Component
という名前で export するとエディタの自動 import に同様のモジュールが大量にひっかかってしまいます。
みなさんはどうされているのでしょうか。知見ある方は @karur4n までお願いします。
React Native は採用するかどうかの見極めが大変むずかしいです。React Native を採用する基準をミニマムにまとめると以下のようになるのではないでしょうか。
以上、React Native でアプリを開発した知見を共有いたしました。
React Native はたしかに Web 技術でアプリを作ることができる面白さがある反面、まだ完成されていないことによるバグなどのつらさもある興味深いツールです。
株式会社スノウロビンでは、価値提供のため新しい技術に積極的にチャレンジしていくメンバーを募集しています!