React Navigationで画面遷移してみる
React Nativeで画面遷移したかったのでまとめました。
内容としてはcreate-react-native-app
で作ったアプリで、2つのナビゲーター(StackNavigator
、TabNavigator
)を使ってみた勉強記事です。
注意としてはDrawerNavigator
は使わないことと、redux
等との連携も書いてないことです。
記述方法等は公式ドキュメントをご確認ください。
概要
公式ドキュメント で紹介されている画面遷移用のライブラリはいくつかありますが、 クロスプラットフォームで動作するものと考えると選択肢に入るのは以下の二つかなと思いました。 二つとも綺麗なドキュメントが用意されています。
どちらを選んでもよかったのですが、
create-react-native-app
で使ったアプリを書きたいと思ったので、
javascript
だけで書かれているReact Navigationで書くことにしました。
React Navigation
React NavigationはReact Nativeで画面遷移を実装するために、いくつかのナビゲーター(navigator)というコンポーネントを利用します。
ナビゲーターはルーティングしたり画面に引数をあげたりジェスチャーを制御したり、画面遷移に必要なものを提供してくれるものです(ざっくりです)。
標準で使えるナビゲーターとしては、StackNavigator
、TabNavigator
、DrawerNavigator
です。それぞれ以下のような特徴があります。
- StackNavigator: 画面を積んでいくタイプのナビゲーター。Screen1 -> Screen2とかScreen1 -> Screen3とか画面を積んでツリーを作っていき、行きつ戻りつしていくという感じです。描画した画面内のコンポーネントを押したら別の画面に遷移させる、といったWebのリンクと同じようなケースで使う感じです。
- TabNavigator: タブタイプのナビゲーター。[Screen1、Screen2、Screen3]のように並列に画面を依存させずに扱い、切り替えていく感じです。画面遷移はTabNavigator自身が責任を持つ感じです。
- DrawerNavigator: ドロワータイプのナビゲーター。自分的にはタブが横なら、ドロワーは縦という程度の位置が違うくらいの認識です。ただ、タブに比べて登録できる画面数が多い(多くても比較的わかりにくくならない)ので、例えばユーザーによって画面数が変化するタイプのものであれば、これを使うのかなと思います。
通常はこれらをネストさせるなどしてアプリケーションの画面遷移を作っていきます。カスタムしたナビゲーターを作ることもできるようですが、まだいいかなと思います。(丸いナビゲーターとかARっぽくてカッコ良さそう)
初心者的にはネストさせ方がちょっと不明で困ります。よく見るアプリケーションでは、
- TabNavigator(下タブ) or DrawerNavigatorをルートの親ナビゲーターとして選ぶ
- それぞれの子にScreen(普通の画面)、StackNavigator、TabNavigator(上タブ)を任意に登録
- そのそれぞれの子に画面を登録
のネストが2段くらいの構成になっていると思いますのでそれを真似します。
疑問: 認証、設定などの画面はどのように制御するのがいいのでしょうか?設定画面はモーダルを使って表示するのはよく見る気がします?認証画面もモーダルを使った表示を見ることがありますが、一瞬アプリが表示されてからシュルッと画面を乗っけるとかだと、裏側で画面が動かないように制御しなければいけない気がします。トップレベルのに認証用のナビゲーターをかませてネストを3段にするとか?
追記 2018/05/07: SwitchNavigator 使うといいかもしれないです
準備
ドキュメントを参考にインストールします。
$ create-react-native-app demo-application $ cd demo-application $ npm install --save react-navigation
StackNavigator
単純な使い方として、リストを表示して、押したらその詳細画面に遷移する例を書きます。
自動生成されるApp.js
に以下のコードを記述します。
という感じになりました。今回は単純な例なので遷移先のDetail
は使い回しです。
TabNavigator
2つの画面を持つTabNavigatorを作成します。
- App
- MathematicsList: 先ほど作った数学リスト
- AddMathItemScreen: 数学項目追加画面
やることは3つです、
- TabNavigatorのコンポーネントを作成
- 項目追加機能の実装
- 項目削除機能の実装
Step1 - TabNavigatorのコンポーネントを作成
App.js
を修正してTabNavigatorを描画するコンテナーコンポーネントを作成していきます。
... import { TabNavigator } from 'react-navigation'; /* 追加 */ /* * TabNavigatorを作成 * StackNavigatorと基本は同じ * 第二引数で画面下タブに表示されるアイコン色とラベル非表示を設定 */ const Tab = TabNavigator({ List: { screen: MathematicsList }, AddItem: { screen: AddMathItemScreen }, }, { tabBarOptions: { activeTintColor: '#037aff', inactiveTintColor: '#737373', showLabel: false, }, }); /* * 二つのタブでデータを共有するためにAppコンポーネントをコンテナコンポーネント化 * 今の所stateを持っている以外はTabNavigatorをラップしているだけ */ export default class App extends React.Component { constructor(props) { super(props); this.state = { mathematics, }; } render() { /* screenPropsで各子供にmathematicsを渡している */ return <Tab screenProps={{ mathematics: this.state.mathematics }} />; } }
TabNavigator
、StackNavigator
はコンポーネントなのでjsx
記法でrender
に書くことができます。screenProps
に値を入れると各タブでデータを受け取ることができるようです。ここでは各タブにmathematics
データを渡すために、TabNavigator
の設定をして一旦Tab
と名付けた後に、Appコンポーネントをコンテナ化し内部でscreenProps
を渡しました。
TabNavigator
で登録された画面にnavigationOptions
を設定していきます。AddMathItemScreen
は以下のように作成しました。
... import { Entypo } from '@expo/vector-icons'; ... /* * 項目追加画面を作成 * 現在はタブアイコンの設定のみのモック */ const AddMathItemScreen = () => ( <View style={styles.container}> <Text style={styles.paragraph}>This is AddMathItemScreen</Text> </View> ); AddMathItemScreen.navigationOptions = { tabBarIcon: ({ tintColor }) => <Entypo size={24} name="add-to-list" color={tintColor} />, };
tabBarIcon
のtintColor
はTabNavigator
のオプションで設定していて、active
、inactive
に応じて異なる値が送られてきます。Entypo
はcreate-react-native-app
で作成すると初めから使えるexpo
ライブラリに含まれているカスタムフォントのコンポーネントです。
AddMathItemScreen
と同様にMathematicsList
にもタブアイコンの設定を行います。
/* * StackNavigatorを変数に格納 */ const Stack = StackNavigator({ Detail: { screen: DetailScreen }, List: { screen: ListScreen }, }, { initialRouteName: 'List', }); /* * StackNavigatorをラップするコンポーネント * screenPropsにより親からもらったpropsを子にそのまま流している(手抜き) * mathematicsリストをAppコンポーネントに持たせているため、 * ここで設定したものがListScreenに渡っていく。 */ const MathematicsList = ({ screenProps }) => ( <Stack screenProps={screenProps} /> ); /* * タブアイコンの設定 */ MathematicsList.navigationOptions = { tabBarIcon: ({ tintColor }) => <Entypo size={24} name="list" color={tintColor} />, };
また、親からscreenProps
が渡されてくるのでそれをListScreen
に渡すためにここでも一旦StackNavigator
を変数に格納して、MathematicsList
内で受け渡しを行います。(もっといい方法があると思うんですが。。。ちゃんとやるときは多分reduxとか使うのでこの辺りはなあなあにしておきます)
ちなみにListScreen
は次のように値を受け取ります。
/* 引数screenPropsが親から渡ってくる */ const ListScreen = ({ navigation, screenProps }) => ( <FlatList data={screenProps.mathematics} /* mathematicsを取り出す */ ...
上記の変更をまとめたのが以下になります。
2つのタブが画面下に表示され、片方の画面は先ほどのStackNavigator
がネストされている状態になっています。
追記: タブアイコンの設定箇所をTabNavigator
の定義箇所に変更することも可能なようです。StackNavigator
に関しても同様で、タイトルなど動的に変更する必要があるかどうかで分けるという感じです。
const Tab = TabNavigator({ List: { screen: MathematicsList, navigationOptions: { tabBarIcon: ({ tintColor }) => <Entypo size={24} name="list" color={tintColor} />, }, }, AddItem: { screen: AddMathItemScreen, navigationOptions: { tabBarIcon: ({ tintColor }) => <Entypo size={24} name="add-to-list" color={tintColor} />, }, }, }, { initialRouteName: 'List', tabBarOptions: { activeTintColor: '#037aff', inactiveTintColor: '#737373', showLabel: false, }, });
Step2 - 項目追加機能の実装
項目追加機能を実装するために、App
コンテナコンポーネントからstateをいじれる関数を、TabNavigator経由でAddMathItemScreen
コンポーネントまで渡します。
export default class App extends React.Component { constructor(props) { super(props); this.state = { mathematics, }; this.addNewMathItem = this.addNewMathItem.bind(this); } /* 子供に渡す関数を作成 */ addNewMathItem({ title, detail }) { this.setState({ mathematics: [...this.state.mathematics, { title, detail }], }); } render() { /* screenPropsで各子供にmathematics、addNewMathItemを渡す */ return <Tab screenProps={{ mathematics: this.state.mathematics, addNewMathItem: this.addNewMathItem }} />; } }
受け取ったAddMathItemScreen
側でタイトル、詳細のテキスト入力、追加リクエスト処理を記述します。追加が完了したらListScreen
に遷移するようにAlert
のOK
ボタンをトリガーに設定します。見栄えに関して多少のスタイルも追加しています。
class AddMathItemScreen extends React.Component { constructor(props) { super(props); this.state = { title: '', detail: '' }; this.handleOnPress = this.handleOnPress.bind(this); } handleOnPress() { const { title, detail } = this.state; if (!title) return Alert.alert('Error', 'titleは必須です'); this.props.screenProps.addNewMathItem({ title, detail }); Alert.alert( 'Notice', '項目を追加しました!', [{ text: 'OK', onPress: () => this.props.navigation.navigate('List') }], ); this.setState({ title: '', detail: '' }); } render() { return ( <TouchableWithoutFeedback onPress={Keyboard.dismiss}> <View style={styles.container}> <View style={styles.inputGroup}> <Text style={[styles.paragraph, styles.label]}>title</Text> <TextInput blurOnSubmit onChangeText={title => this.setState({ title })} style={[styles.textInput, styles.heading]} value={this.state.title} /> </View> <View style={styles.inputGroup}> <Text style={[styles.paragraph, styles.label]}>detail</Text> <TextInput blurOnSubmit multiline onChangeText={detail => this.setState({ detail })} style={[styles.multiTextInput, styles.paragraph]} value={this.state.detail} /> </View> <Button onPress={this.handleOnPress} title={'Add item to list'} /> </View> </TouchableWithoutFeedback> ); } }
まとめると次のようになっています。
Step3 - 項目削除機能の実装
項目追加機能同様、Appコンテナコンポーネントからstateをいじれる関数を渡します。削除の方法としては、配列のインデックスをキーに各詳細画面の削除ボタンから実行します(画面遷移で操作を限定させないと容易にバグりそうですね、理詰めでバグらせられそう)
削除メソッドを追加し、screenProps
で渡します。
export default class App extends React.Component { ... removeMathItem(index) { this.setState({ mathematics: this.state.mathematics.filter((_, i) => i !== index), }); } ... render() { /* screenPropsで各子供に渡している */ return ( <Tab screenProps={{ mathematics: this.state.mathematics, addNewMathItem: this.addNewMathItem, removeMathItem: this.removeMathItem, }} /> ); }
次にDetailScreen
のヘッダー右上に削除ボタンを設置する設定を書きます。index
はListScreen
からの遷移時にnavigation.state.params
経由で渡すようにし、removeMathItem
はscreenProps
経由で渡したのでそれらから取得できます。(ListScreen
の変更は最後のまとまったもので確認できます)
DetailScreen.navigationOptions = ({ navigation, screenProps }) => ({ headerRight: ( <TouchableOpacity style={{ marginRight: 8 }} onPress={() => { Alert.alert( 'Warning', '項目を削除しますか?', [ { text: 'Delete', onPress: () => { screenProps.removeMathItem(navigation.state.params.index); navigation.goBack(); }, }, { text: 'Cancel' }, ], ); }} > <Entypo size={24} name="trash" color={'red'} /> </TouchableOpacity> ), });
以上で削除機能が実装できました。コード汚いよ、コンポーネントに切り出せよ、と思いますが指針が決まらないのでとりあえず画面単位の分割に抑えています。
完成
完成しました。こんな感じで動いています。
感想
引数の受け渡しはRedux
とかでやりたいと思いました。なのでRedux
ない状態でのプロパティの渡し方は勉強不足です。すみません。
あと書き捨てのつもりだったのでgit管理していなくて、diffだけ見せるとかいう方法もあったかもという気がします。さらによくよく考えればgistの差分でもよかったです。記事が読みにくい感じですね。