React Native + Expo アプリに Native Base 導入で楽々画面デザイン

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

※ 最新のExpo＋Native Baseでこの記事のとおりに実施すると、若干デザインの問題が発生します。最新版対応は、[こちらの記事](https://kyokara.com/expo-native-base-mac/)におまかせしました。

---

　[前回](https://qiita.com/hitotch/items/36dcc678976bd4ebe006)までに、おおよそのアプリのテスト動作は作ったので、今回は画面デザインをそれっぽくしていきます。

Native Baseを使うと、こんなデザインができるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/4221d847-7edc-b020-15a3-5520e81047ee.png)


今回の改善前　→　改善後
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/82b36126-8a10-7518-1d13-3b5914f31b23.png">　<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/fbc76fce-ffe9-ee2f-254c-af916c6ef509.png">


# Native Baseのインストール

　Visual Studio CodeでReact Nativeのプロジェクトを開き、``Ctrl + @``でPowerShellを開いて、インストールコマンド実行。

```
npm install native-base
```

　ExpoでNative Baseを使う場合、Native Baseがカスタムフォントを使うので、Expo Font（[詳しくはこちら](https://docs.expo.io/versions/latest/sdk/font/)）を使えるようにしていなくてはいけません。Expo Fontをインストールします。

```
expo install expo-font
```

# Native Baseで使うフォントを読み込む処理を入れる

　Native Baseでは、アイコンなど一部で特殊なフォントを使います（[詳しくはこちら](https://docs.expo.io/versions/latest/sdk/font/)）。このため、アプリのどこかでFontをロードしなくてはいけません。通常、App.jsで読み込みします。
　App.jsがかなり大きくなったように見えますが、やってることは至って簡単です。

App.jsの変更点

```jsx:App.js
import { View, Text } from "react-native";
// expo-fontとアイコンをimportする
import * as Font from "expo-font";
import { Ionicons } from "@expo/vector-icons";
...
export default class App extends React.Component {
  // ロードが終わるまでは「loading...」を表示するため、state「isReady」で制御
  constructor(props) {
    super(props);
    this.state = {
      isReady: false
    };
  }
  // DidMountのタイミングでフォントリソースをメモリ上に読み込み。終わったらisReadyをオン。
  async componentDidMount() {
    await Font.loadAsync({
      Roboto: require("native-base/Fonts/Roboto.ttf"),
      Roboto_medium: require("native-base/Fonts/Roboto_medium.ttf"),
      ...Ionicons.font
    });
    this.setState({ isReady: true });
  }
  render() {
    //Wait for font loading... フォントの読み込み中なら、「loading...」を表示
    if (!this.state.isReady) {
      return (
        <View>
          <Text>loading...</Text>
        </View>
      );
    }

    let globalState = new InvoiceContainer({ initialSeeding: true });
    return (
      <Provider inject={[globalState]}>
        <AppContainer />
      </Provider>
    );
  }
}
```

　フォントの読み込み部分、ファイルのパスの指定に注意してください。もしApp.js以外で読み込む場合、現在編集しているファイルから見た正しいパスを入れないと、以下のようにエラーになります。

```
ENOENT: no such file or directory, scandir 'C:\ExpoProjects\hello-world\components\node_modules\native-base\Fonts'
Failed building JavaScript bundle.
```

　たとえば、``/components/Hoge.js``で実施するなら、以下のように書きます。``node_modules``を忘れないでください。

```jsx:/components/Hoge.js
    //これは例です
    await Font.loadAsync({
      Roboto: require("../node_modules/native-base/Fonts/Roboto.ttf"),
      Roboto_medium: require("../node_modules/native-base/Fonts/Roboto_medium.ttf"),
      ...Ionicons.font
    });
```


# ボタンデザインをNative Base版にする＋調整

　では、HomeScreenの``Button``と``Text``を、react-native版からNative Base版に置き換えてみます。


変更前

``` jsx:/components/HomeScreen.js
import { Button, Text, View } from "react-native";
...
class HomeScreenContent 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")} />

        {invoiceList}
      </View>
    );
...
```

変更後

``` jsx:/components/HomeScreen.js
import { Container, Text, Button, Icon } from "native-base";
...
    return (
      <Container style={styles.container}>
        <Text>Open up App.js to start working on your app!</Text>
        <Text>Hello World!</Text>

        <Button onPress={() => this.props.navigation.navigate("InvoiceEdit")}>
          <Icon type="FontAwesome5" name="file-invoice-dollar" />
          <Text>Go to InvoiceEdit</Text>
        </Button>
        <Button onPress={() => this.props.navigation.navigate("Summary")}>
          <Icon type="FontAwesome5" name="poll-h" />
          <Text>Go to Summary</Text>
        </Button>

        {invoiceList}
      </Container>
    );
...
```

結果
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/fee87f87-6df6-bc3b-5888-967d70db5c02.png">

　アイコンフォントのテストもかねて、ボタンにアイコンを追加しています。Native Baseで使えるアイコンの一覧は[こちら](https://oblador.github.io/react-native-vector-icons/)。

　Buttonの``title``は互換性がないので、\<Text\>を使います。これで楽にアイコンも入れられるようになります。HTMLっぽくて楽ですね。

# リストデザインをNative Base版にする

　リストをNative Base版にしますが、同時にNative Baseのコンポーネント構造ルールに従って、\<Content\>コンポーネントの中にコンテンツを入れるようにします。
　ついでに、ボタンの配置をそれっぽくします。

変更前

```jsx:components/HomeScreen.js
  render() {
    let globalState = this.props.globalState;
    let invoiceList = <Text>No invoice</Text>;
    if (globalState.state.data.invoices.length) {
      invoiceList = globalState.state.data.invoices.map(invoice => {
        return <Text key={invoice.id}>{invoice.id + " : " + invoice.date}</Text>;
      });
    }
    return (
      <Container style={styles.container}>
        <Text>Open up App.js to start working on your app!</Text>
        <Text>Hello World!</Text>

        <Button onPress={() => this.props.navigation.navigate("InvoiceEdit")}>
          <Icon type="FontAwesome5" name="file-invoice-dollar" />
          <Text>Go to InvoiceEdit</Text>
        </Button>
        <Button onPress={() => this.props.navigation.navigate("Summary")}>
          <Icon type="FontAwesome5" name="poll-h" />
          <Text>Go to Summary</Text>
        </Button>

        {invoiceList}
      </Container>
    );
  }
```

変更後

```jsx:components/HomeScreen.js
import { View, Container, Content, Text, Button, Icon, List, ListItem, Left, Right } from "native-base";
...
class HomeScreenContent extends React.Component {
  render() {
    let globalState = this.props.globalState;
    let invoiceList = <Text>No invoice</Text>;
    if (globalState.state.data.invoices.length) {
      invoiceList = (
        <List>
          {globalState.state.data.invoices.map(invoice => {
            return (
              <ListItem key={invoice.id} noIndent>
                <Left>
                  <Text key={invoice.id}>{invoice.id + " : " + invoice.date}</Text>
                </Left>
                <Right>
                  <Icon name="arrow-forward" />
                </Right>
              </ListItem>
            );
          })}
        </List>
      );
    }
    return (
      <Container>
        <Content>
          <View style={{ flexDirection: "row" }}>
            <Left>
              <Button onPress={() => this.props.navigation.navigate("InvoiceEdit")}>
                <Icon type="FontAwesome5" name="file-invoice-dollar" />
                <Text style={{ paddingLeft: 0 }}>InvoiceEdit</Text>
              </Button>
            </Left>
            <Right>
              <Button onPress={() => this.props.navigation.navigate("Summary")}>
                <Icon type="FontAwesome5" name="poll-h" />
                <Text style={{ paddingLeft: 0 }}>Summary</Text>
              </Button>
            </Right>
          </View>

          {invoiceList}
        </Content>
      </Container>
    );
  }
}
```

結果
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/fbc76fce-ffe9-ee2f-254c-af916c6ef509.png">

　Native Baseでは、``Container``の中に、``Header``, ``Content``, ``Footer``を入れる前提となっています。そのため、コンテンツは``Content``に入れないと正しく（あるいはまったく）表示されないなどの問題が発生します。
　今回ヘッダーはnavigationがすでに描画してくれているので、Native Baseの``Header``は使わないことにします。デザインの都合で多機能ヘッダーにしたくなったら、navigationのヘッダーをオフにして、Native Baseのヘッダーを使うのもいいですね。ただしその場合はバックボタンなどは自分で描画することになります。
　フッターは画面の下に張り付いてくれる使いやすいフッターです。必要になったらつけることにしますが、今はつけません。

　ボタンの配置変更は、まず``<View style={{ flexDirection: "row" }}>``で横方向配置モードにして、``<Left>``と``<Right>``で配置しています。
　アイコンとテキストの間が空きすぎないように、``<Text>``の``paddingLeft``をゼロにして、おおよそOKくらいに調整しています。

　このあたりのStyleの調整は、本来ならstyles.jsにまとめて入れるべきです。