React Native + Expo + react-navigationで、アプリの画面構成を作成する

この記事は、「[【連載】初めてのReact Native + Expo開発環境構築入門](https://qiita.com/hitotch/items/5142fff638c7805d84d5)」の子記事です。環境などの条件は、親記事をご覧ください。

---

　[前回](https://qiita.com/hitotch/items/0ceca6eb440c06512c51)react-navigationが動作するところまで作りましたが、これを実際に小さいアプリを計画して画面構成を実現し、取り急ぎ画面遷移ができる状態までアプリを作ります。各画面コンポーネントはプロジェクトメンバーで分担して作ることを考え、コンポーネントごとにファイルに分割して独立させます。

　**この記事はReact Navigation V4を前提に書かれています。2020年10月時点最新のV5対応は、[こちら](https://kyokara.com/expo-navigation-screens-mac/)の兄弟記事にお任せしました。**



# アプリの画面構成全体像を計画する

　まずはアプリの画面構成をおおよそで計画します。[Fluid UI](https://www.fluidui.com/)などでサクッとお絵描きしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/658e26cb-98cd-ffbf-8274-4fbbce15c160.png)

　今回はInvoice（請求書）アプリを想定します。簡単なCRUD機能を持つ端末アプリです。

- 初期画面はInvoiceのリスト画面。
- Invoiceの新規作成または編集ができるが、新規作成画面と編集画面は同一のコンポーネントで作る。
- Invoiceを保存するときは、支払い方法を選択するポップアップが出る。
- 支払い方法を選択した後、印刷ポップアップが出る。
- すべてのInvoiceのサマリー画面で集計結果をみることができる。

　その他機能（設定画面、データのインポート・エクスポート、全削除確認、など）もきっと必要ですが、まずは基本の画面だけ作っていくことにします。
　今回はStack Navigatorを使いますが、react-navigationでは他にもフッターにタブを並べて画面を選択させるTab navigatorなどもサポートしています。

　フォルダ構成はこれに基づき以下のようにします。componentsフォルダ内にコンポーネントを配置するのは、Reactの行儀作法に従っています。また、App.jsにあったスタイル情報はstyles.jsにまとめることにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/393aa821-fd1c-ae67-8152-2c75b1477400.png)

# Stack Navigatorの準備

　まずはStack Navigatorを更新します。

```App.js
const RootStack = createStackNavigator(
  {
    Home: HomeScreen,
    InvoiceEdit: InvoiceEditScreen,
    Summary: SummaryScreen,
  },
  {
    initialRouteName: 'Home',
  }
);
```

　こうするためには、componentsフォルダに配置された各コンポーネントファイルからReact.Componentクラスをimportしなくてはいけません。App.js上部に以下を追記します。

```App.js
import HomeScreen from './components/HomeScreen';
import InvoiceEditScreen from './components/InvoiceEditScreen';
import SummaryScreen from './components/SummaryScreen';
```

# スタイル情報をファイルに独立させる

　WebでいうCSSのように、スタイル情報はコンポーネント共通にしたいので、新設したstyles.jsにcontainerのスタイル定義を入れるよう以下のようにし、App.jsからstyles変数定義部分を削除します。
　後で使うmodalのスタイル定義も入れました。

```styles.js
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#fff',
        alignItems: 'center',
        justifyContent: 'center',
    },
    modal: {
        flex: 1,
        backgroundColor: 'rgba(0,0,0, 0.8)',
        alignItems: 'center',
        justifyContent: 'center',
    },
});
```

# Home画面を独立ファイル化する

```components/HomeScreen.js
import React from 'react';
import { StyleSheet, Button, Text, View } from 'react-native';
import styles from '../styles.js';

class HomeScreen extends React.Component {
  render(){
    return (
      <View style={styles.container}>
        <Text>Open up App.js to start working on your app!</Text>
        <Text>Hello World!</Text>
        <Button
          title="Go to InvoiceEdit"
          onPress={() => this.props.navigation.navigate('InvoiceEdit')}
        />
        <Button
          title="Go to Summary"
          onPress={() => this.props.navigation.navigate('Summary')}
        />
      </View>
    );
  }
}

export default HomeScreen;
```

# Summary を同様に作成する。

```components/SummaryScreen.js
import React from 'react';
import { StyleSheet, Button, Text, View } from 'react-native';
import styles from '../styles.js';

export default class SummaryScreen extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Summary Screen</Text>
      </View>
    );
  }
}
```

# Invoice画面のModal（ポップアップダイアログ）にはreact-navigationを使うべきか否か

　react-navigationでは、Modalを作るときはStack Navigationをもう一つ作って、通常画面と同様にコールせよ、と[Opening a full-screen modal](https://reactnavigation.org/docs/en/modal.html)のページに書いてあります。
　この考え方はポップアップをアプリ内のコンポーネントで共通して使う場合には便利そうですが、特定のコンポーネント内でしか使わない場合は記述があまり素直ではないと感じられます。
　したがって、コンポーネント内で独立して組み込める[React Native自体のModal](https://facebook.github.io/react-native/docs/modal.html)のほうが今回はベターであると判断し、react-navigationの画面構成設計からは除外することにします。


# Invoice は Summary同様に作成し、modalの動作を加える。

```components/InvoiceEditScreen.js
import React from 'react';
import { Button, Text, View, Modal, Alert } from 'react-native';
import styles from '../styles.js';

export default class InvoiceEditScreen extends React.Component {
  state = {
    modalPaymentVisible: false,
    modalPrintVisible: false,
  };

  setModalPaymentVisible(visible) {
    this.setState({modalPaymentVisible: visible});
  }
  setModalPrintVisible(visible) {
    this.setState({modalPrintVisible: visible});
  }
  
  render() {
    return (
      <View style={styles.container}>
        <Text>Invoice Edit Screen</Text>
        <Modal
          animationType="slide"
          transparent={true}
          visible={this.state.modalPaymentVisible}
          onRequestClose={() => {
            Alert.alert('Modal has been closed.');
          }}>
          <View style={styles.modal}>
            <Text style={{color:'#fff'}}>Payment popup</Text>
            <Button
              title="Hide modal"
              onPress={() => {
                this.setModalPaymentVisible(false);
              }}>
            </Button>
            <Button
              title="Go to Print"
              onPress={() => {
                this.setModalPaymentVisible(false);
                this.setModalPrintVisible(true);
              }}>
            </Button>
          </View>
        </Modal>
        <Modal
          animationType="slide"
          transparent={true}
          visible={this.state.modalPrintVisible}
          onRequestClose={() => {
            Alert.alert('Modal has been closed.');
          }}>
          <View style={styles.modal}>
            <Text style={{color:'#fff'}}>Print popup</Text>
            <Button
              title="Hide modal"
              onPress={() => {
                this.setModalPrintVisible(false);
                this.props.navigation.goBack();
              }}>
            </Button>
          </View>
        </Modal>
        <Button
          title="Show modal"
          onPress={() => {
            this.setModalPaymentVisible(true);
          }}>
        </Button>
      </View>
    );
  }
}
```

　Modalの表示・非表示コントロールはstateで簡単に表現でき、なおかつアニメーションが自動的に入ってかっこよく動きます。半透明の背景は、先程設定したstyles.jsで実現しています。

念のため、変更後のApp.jsは：

```App.js
import React from 'react';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

import HomeScreen from './components/HomeScreen';
import InvoiceEditScreen from './components/InvoiceEditScreen';
import SummaryScreen from './components/SummaryScreen';

const RootStack = createStackNavigator(
  {
    Home: HomeScreen,
    InvoiceEdit: InvoiceEditScreen,
    Summary: SummaryScreen,
  },
  {
    initialRouteName: 'Home',
  }
);

const AppContainer = createAppContainer(RootStack);

export default class App extends React.Component {
  render() {
    return <AppContainer />;
  }
}
```

　これでInvoiceのポップアップも設計通りに動くようになりました。以下は動作させた場合の画面イメージで、上から順にHome、InvoiceEdit、InvoiceEdit内のPaymentポップアップ、InvoiceEdit内のPaymentポップアップから遷移するPrintポップアップです。

<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/88253803-2c59-9025-f4b6-c28d982a5301.png">
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/97814b52-4314-b15c-d8dc-bc8e2d7be033.png">
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/7593b525-30fe-1bec-3182-915c65595426.png">
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/dba61cff-4328-c6de-314a-6922d914b391.png">