React
reactnative
redux

React Native の画面遷移・管理をオープンソースのアプリを見ながら学ぶ

最近趣味のアプリ開発で React Native を使っています。いつもは React Redux を使った SPA の Web アプリ とその Cordova アプリを書いています。

Web アプリだと、画面は react-router で管理するのが定番ですが、 React Native の場合は、 react-router を使わず他の手法で実装している様なので、オープンソースのアプリで使われている実装など、調べて学んだ結果をご紹介したいと思います。


参考にしたアプリ一覧

React-Native-Apps に React Native で作られているオープンソースのアプリケーションがいろいろ公開されているので、参考にしてみました。

アプリ名
説明

git-point
github を見るアプリ。割とサクサク動くのでコード読むのにもオススメ

gas-oil-mixture-mobile
オイルの計算機

HackerBuzz-ReactNative
ハッカーニュース的なアプリ

f8
facebook のカンファレンスのアプリ


シンプルなスワイプの遷移

git-point で使われていた、スワイプの遷移。

racord5.gif

git-point/blob/master/src/auth/screens/login.screen.js に全体の実装があります。react-native-swiper というスワイプのライブラリを利用しているようです。

使い方だけもとてもシンプル。 SwiperView を囲うだけ。(ライブラリ本家の方のサンプルです)


swiper-sample.js

import Swiper from 'react-native-swiper';

import { Text, View } from 'react-native';

...

return (
<Swiper
showsButtons={false}
dotColor="#FFFFFF55"
activeDotColor="#FFFFFFFF"
>
<View>
<Text>Hello Swiper</Text>
</View>
<View>
<Text>Beautiful</Text>
</View>
<View>
<Text>And simple</Text>
</View>
</Swiper>
)



戻るボタン付き遷移

gas-oil-mixture-mobile にて実装されていたシンプルな戻るボタン付き遷移。

recortde3.gif

react-navigation の StackNavigator を使っています。StackNavigator で遷移させると、デフォルトで前に戻るボタンが遷移先の左上に現れます。

このアプリの動作では、右上ボタンのクリックでオプションページに遷移します。(縦三点のボタンでアクションリストが開くのではなく、画面遷移なのはちょっと変な気もしますが。。) gas-oil-mixture-mobile/blob/master/src/router.js にある通り、実装自体もシンプルです。(以下一部抜粋)画面遷移とは関係でないですが title なども I18n で多言語対応しているところが素敵ですね。


router.js

import { StackNavigator } from 'react-navigation';

import Calculator from './screens/CalculatorHoc';
import Options from './screens/OptionsHoc';
import { OptionsButton } from './components';
...

const Stack = StackNavigator(
{
Calculator: {
screen: Calculator,
navigationOptions: ({ navigation }) => ({
title: I18n.t('oilRatioCalculator'),
headerRight: (
<OptionsButton onPress={() => navigation.navigate('Options')} />
),
}),
},
Options: {
screen: Options,
navigationOptions: () => ({
title: I18n.t('options'),
}),
},
}
);


画面の管理も遷移数が少ないので、router.js に全部の状態をもたせて /screens/xxx から、各ページのデータを取ってくる構成なのでシンプルですね。


タブ遷移

HackerBuzz-ReactNative

にて実装されているタブの遷移です。上部のタブ遷移と、下部のタブ遷移の二つがあります。

recortde3.gif


上部のタブ遷移

上部のタブは react-native-scrollable-tab-view で実現されています。タブクリックで、画面がスワイプします。

RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/containers/main/StoriesContainer.js より、一部抜粋。


StoriesContainer.js

import ScrollableTabView from 'react-native-scrollable-tab-view';

export default class StoriesContainer extends Component {
render() {
return (
<ScrollableTabView>
{/* タブコンテンツ1 */}
{/* タブコンテンツ2 */}
...
</ScrollableTabView>
)
}
}



下部のタブ遷移

次に、下部のタブ遷移です。このアプリも react-navigation を使っていますが、addNavigationHelpers という機能を使って画面遷移を実装しています。

HackerBuzz-ReactNative/blob/master/js/containers/Router.js の実装を一部抜粋


Router.js

...

import { addNavigationHelpers } from 'react-navigation';
import { TabBar } from './navigationConfiguration';
...

class Router extends Component {
render() {
const { dispatch, navigationState } = this.props;
return (
<View style={{ flex: 1 }}>
<TabBar
navigation={addNavigationHelpers({
dispatch: dispatch,
state: navigationState
})}
/>
</View>
);
}
}
const mapStateToProps = state => {
return {
navigationState: state.tabBar,
storiesState: state.storyList
};
};

export default connect(mapStateToProps)(Router);


Redux の Container Component(redux の state を受け取る層) として、Router を定義しています。どのページにいるのかを状態と捉え、 Redux の state として扱っているようです。 また <TabBar> というコンポーネント自体は navigationConfiguration.js のファイルで定義しており、その中で react-navigation の TabNavigator を利用して、複数のタブに遷移できるようになっています。


navigationConfiguration.js

import { TabNavigator } from 'react-navigation';

import TabMainNavigation from './main/TabMainNavigation';
import TabAskNavigation from './ask/TabAskNavigation';
...

const routeConfiguration = {
TabMainNavigation: { screen: TabMainNavigation },
TabAskNavigation: { screen: TabAskNavigation },
...
};

const tabBarConfiguration = {
tabBarOptions: {
showIcon: true,
tabBarPosition: 'bottom',
...
}
};

export const TabBar = TabNavigator(routeConfiguration, tabBarConfiguration);


TabMainNavigation, TabAskNavigation など、タブごとの Container Component を作成して、それぞれその中で、StackNavigator などを呼び出して、複合的な画面遷移を実現しています。

HackerBuzz-ReactNative/blob/master/js/containers/ask/TabAskNavigation.js


TabAskNavigation.js


import React, { Component } from 'react';
import { addNavigationHelpers } from 'react-navigation';
import { NavigatorTabAsk } from './navigationConfiguration';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/FontAwesome';

class TabAskNavigation extends Component {
static navigationOptions = {
tabBarLabel: 'Ask',
tabBarIcon: ({ tintColor }) => (
<Icon size={20} name={'question'} color={tintColor} />
)
};

render() {
const { dispatch, navigationState } = this.props;
return (
<NavigatorTabAsk
navigation={addNavigationHelpers({
dispatch: dispatch,
state: navigationState
})}
/>
);
}
}

const mapStateToProps = state => {
return {
navigationState: state.tabAsk
};
};

export default connect(mapStateToProps)(TabAskNavigation);


HackerBuzz-ReactNative/blob/master/js/containers/ask/navigationConfiguration.js

タブ遷移した画面からさらに StackNavigator を呼び出して、戻るボタン付き遷移をできるようにもしているようです。


navigationConfiguration.js

import { StackNavigator } from 'react-navigation';

import AskStoriesContainer from './AskStoriesContainer';

const routeConfiguration = {
AskStoriesContainer: { screen: AskStoriesContainer },
};

const stackNavigatorConfiguration = {
initialRoute: 'AskStoriesContainer',
navigationOptions: {
headerBackTitle: null
},
gesturesEnabled: true,
};

export const NavigatorTabAsk = StackNavigator(
routeConfiguration,
stackNavigatorConfiguration
);


HackerBuzz-ReactNative/blob/master/js/reducers/index.js

画面のそれぞれの状態管理に関しては reducer 側で getStateForAction を利用することで、指定したアクションに応じてナビゲーションの状態を決めることができます。(正直自分もこの遷移方法を完全に理解できてないです。)


reducers/index.js

import { combineReducers } from 'redux';

import { NavigatorTabMain } from '../containers/main/navigationConfiguration';
import { NavigatorTabAsk } from '../containers/ask/navigationConfiguration';

import { tabBarReducer } from '../containers/navigationConfiguration';

const rootReducer = combineReducers({
tabBar: tabBarReducer,
tabMain: (state, action) =>
NavigatorTabMain.router.getStateForAction(action, state),
tabAsk: (state, action) =>
NavigatorTabAsk.router.getStateForAction(action, state),
});

export default rootReducer;



facebook のカンファレンスのアプリのタブ遷移

facebook のf8 カンファレンスで利用されていたアプリです。

recortde3.gif

全体の画面遷移は F8Navigator.js と言うファイルで、管理されていました。名前からして 非推奨のパッケージ(?)を使っているようです。routing の管理も if 文で出し分けしています。


F8Navigator.js

...

import { Navigator } from "react-native-deprecated-custom-components";

...

renderScene: function(route, navigator) {
if (route.allSessions) {
return <SessionsCarousel {...route} navigator={navigator} />;
} else if (route.session) {
return <SessionsCarousel session={route.session} navigator={navigator} />;
} else if (route.filter) {
return <FilterScreen navigator={navigator} {...route} />;
} else if (route.friend) {
...


下にあるタブの遷移には react-native-tab-navigator を使っています。 この記事の上の方で紹介した react-native-scrollable-tab-view と似ていますね。(スター数的には react-native-scrollable-tab-view の方が4倍くらい多い)

f8app/blob/master/js/tabs/F8TabsView.js


F8TabsView.js

...

import TabNavigator from "react-native-tab-navigator";
...

return (
<TabNavigator tabBarStyle={styles.tabBar}>
<TabNavigator.Item title="Schedule">
{/* タブコンテンツ1 */}
</TabNavigator.Item>
<TabNavigator.Item title="My F8">
{/* タブコンテンツ2 */}
</TabNavigator.Item>
</TabNavigator>
)
...


GIF 画像中の Demo と言う名前のページ画面のスワイプには android と ios で別の実装がしてありました。

android 版では ViewPagerAndroid を利用して、左右のフリック動作で移動できるようにしているようです。一方 iOS では ScrollView の horizontal プロパティを利用して、左右の遷移を実現していました。

f8app/blob/master/js/common/ViewPager.js ざっくり抜粋すると以下のような実装です。


android

...

import { ViewPagerAndroid } from "react-native";
...

renderAndroid() {
return (
<ViewPagerAndroid
onPageSelected={this.handleHorizontalScroll}
>
{this.renderContent()}
</ViewPagerAndroid>
);
}



ios

...

import { ScrollView } from "react-native";
...

renderIOS() {
return (
<ScrollView
horizontal={true}
pagingEnabled={true}
onScroll={this.handleHorizontalScroll}
>
{this.renderContent()}
</ScrollView>
);
}


(facebook のアプリということもあってスター数が多いリポジトリですが、機能が多すぎるためか、中身は結構見にくい印象でした。。。)


その他 画面遷移系のライブラリ


react-native-router-flux

よく聞くライブラリなので、参考のアプリがサクッと見つかるかなと思ったんですが、React-Native-Apps に最近追加されたアプリは、ほとんど react-navigation を利用していたので、今回は深く紹介しません。

サンプルを見る限り、 react-router と似た構成で書けるようなので、WEB 開発をしている人にはとっつきやすそうなライブラリに思えます。管理方法も、ルーティングとコンポーネントを書くスタイルでわかりやすいです。


sample

const App = () => (

<Router>
<Stack key="root">
<Scene key="login" component={Login} title="Login"/>
<Scene key="register" component={Register} title="Register"/>
<Scene key="home" component={Home}/>
</Stack>
</Router>
);


react-native-navigation

wix が作っているライブラリで動きが活発。github の README を読むと v2 への書き換えを行なっている模様。

React Native Advent Calendar 20175日目の記事で、 @hotchemi さんがQuipperにおけるReact Native活用事例 で書かれていますが、遷移のアニメーションなどが豊富なようなので、その辺りをこだわりたいならこちらを使うのが良さそうです。


まとめ

画面遷移一つとっても、バリエーション豊富な実装がありました。どれを使うかはアプリの規模や目的によって最適なものを選ぶ必要がありそうです。最近のアプリでは react-navigation での実装が多いようでしたが、react-router-flux は v4 のバージョンアップで MobXフレンドリーにします。的なことが README に書かれていたりするので、状態管理のライブラリと合わせて、選定するのが良い気がします。

個人的には HackerBuzz-ReactNative の構成がパッと見スッキリしているのと react redux の構成で作っているので好きです。


余談: WEB の SPA 開発と React Native の画面遷移を比べて

ネイティブアプリはいきなり特定のページにランディングして来ることが、WEBよりはるかに少ないためか、どこからでも入れることを考慮しなくて良いのは楽な気がしました。しかしその反面、画面遷移のロジックが WEB より複合的(スタックの遷移、タブの遷移)かつバリエーション豊富なため、少々難解な印象を受けました。(ちなみに、WEBの SPA 開発でもネイティブの開発と同じように戻るボタンを上部の AppBar に取り付けて history back のアクションで戻ったりすると、悲惨な結果になります。WEBはメールからのリンクや、 検索流入など URL を通して他の世界からくるので、戻る ≒ 他の世界に戻る。になる。特に自分は Cordova でアプリを作っているので、その辺でめっちゃハマりました。)