React Native でアプリ開発！初心者のための実装 Tips

[エイチームライフスタイルアドベントカレンダー2017](https://qiita.com/advent-calendar/2017/ateam-lifestyle)、3日目です。

本日は[株式会社エイチームライフスタイル](http://life.a-tm.co.jp/)の、エンジニア兼ソース顔担当の @maa0984 がお送りします。
入社間もない頃に先輩社員から「顔がアルパカに似てる」と言われて以来、トレードマークにしています。

最近英語を勉強しようと、英語のフレーズをリスト化して表示するアプリを作りました。
せっかくなので、興味がありつつも手を出せていなかった React Native に挑戦しました。

初めて使用するため手探り状態でしたが、その中で『やりたかったこと』と『それを実現させた方法』をいくつかピックアップしてお伝えします。
React Native でアプリ作りを始めてみようとしている方に、何かの参考になれば幸いです。

# 対象バージョン

- react-native: 0.49.3
- react-native-swipeout: ^2.2.2
- react-native-google-signin: ^0.12.0
- react-navigation: ^1.0.0-beta.13

# 本記事のソースコード

https://github.com/alpaca0984/alpaca-flashcards

# アプリの概要

![AlpacaFlashcards_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/40676/b724056a-aacf-911f-5612-3dfc929596e9.gif)


このアプリでできることは、大体以下の通りです。

1. Google Sheets にデータを登録
2. アプリの画面を下に引くと、Google Sheets のデータを取得
3. 取得したデータをリスト表示
4. リストを各行スワイプするとステータス変更のボタンが出現

内部的にはアプリケーションの状態管理や非同期処理管理で Redux や Redux-Saga を使用しています。
こちらは長くなるので別の形で投稿しようと思います。

それでは、アプリをどのように作っていったのか、ご紹介していきます。

# 実装内容

## トップ画面

### リストビューを作りたい

<img width="270" alt="リストビュー.png" src="https://qiita-image-store.s3.amazonaws.com/0/40676/890933b2-089d-7e88-163b-8e42cf9b9d44.png">

作りたかったのは英語のフレーズをリストで表示させるアプリです。
ですのでリストビューがなければお話しになりません／(^O^)＼

これまで React Native でリストを作成する時は、コアコンポーネントの ListView が使われていたようです。
しかし現在では Deprecated となり、[FlatList](https://facebook.github.io/react-native/docs/flatlist.html) 及び [SectionList](https://facebook.github.io/react-native/docs/sectionlist.html) が推奨されています。
今回セクショニングは必要ないため FlatList を使用しました。

```html:components/Phrases.js
<!-- ソースコードは簡略化しています -->
<!-- そしてJSXのシンタックスハイライトがなかったので、仕方なくHTMLを設定しています。。 -->

export default class Phrases extends React.Component {
  import { FlatList, View, Text } from 'react-native';

  render() {
    return (
      <View style={styles.container} >
        <FlatList
          data={this.props.phrases.data}
          renderItem={this._renderItem.bind(this)}
          keyExtractor={(item, index) => item.key}
          ItemSeparatorComponent={() => <View style={styles.separator} />}
        />
      </View>
    );
  }

  _renderItem({ item, index }) {
    return (
      <View style={styles.phraseSentenceBodyView} >
        <Text
          style={styles.phraseText}
          ellipsizeMode='tail'
          numberOfLines={1} >
          {item.key}
        </Text>
      </View>
    );
  }
}
```

FlatList の基本的なプロパティは以下の通りです。

プロパティ|型|必須|初期値|説明
-----|-----|-----|-----|-----
data|array|Yes| |リスト表示するデータの配列
renderItem|function|Yes| |リストの各行を描画するための関数
keyExtractor|function|No| |各行の識別子を返す関数。React内部でデータのキャッシュ等に使われる
ItemSeparatorComponent|component|No| |行間を描画をするコンポーネントを設定する
refreshControl|component|No| |プルリフレッシュの定義。RefreshControlコンポーネントを設定する（詳しくは後述）

### プルリフレッシュを実装したい

<img width="231" alt="リフレッシュ.png" src="https://qiita-image-store.s3.amazonaws.com/0/40676/ca644a4f-e239-41ac-0a8e-4b2b30d2f697.png">

データは外部(Google Sheets)で管理しています。
同期するために、アプリでおなじみのプルリフレッシュ(下に引っ張って更新するやつ)を実装します。

使用するのはコアコンポーネントの`RefreshControl`です。
FlatList など ScrollView 系のコンポーネントが持っている`refreshControl`プロパティに設定します。

```html:components/Phrases.js
export default class Phrases extends React.Component {
  import { FlatList, View, Text } from 'react-native';

  render() {
    return (
      <View style={styles.container} >
        <FlatList
          data={this.props.phrases.data}
          renderItem={this._renderItem.bind(this)}
          keyExtractor={(item, index) => item.key}
          ItemSeparatorComponent={() => <View style={styles.separator} />}
          refreshControl={
            <RefreshControl
              refreshing={this.props.phrases.refreshing}
              onRefresh={this.props.onRefreshPhrases}
            />
          }
        />
      </View>
    );
  }
}
```

必要なプロパティはこちらです。

プロパティ|型|必須|初期値|説明
-----|-----|-----|-----|-----
refreshing|bool|Yes| |リフレッシュ中の🌀の表示・非表示を判定するBool値
onRefresh|function|No| |リフレッシュ時に実行する関数

### リストでスワイプボタンを表示させたい

<img width="271" alt="スワイプ.png" src="https://qiita-image-store.s3.amazonaws.com/0/40676/b00331f1-822a-37f8-ce2c-161a57488b35.png">

アプリのリストって、大体左にスワイプできますよね。そして大体削除ボタンが出てきます。
このアプリではリストにある英語を実際に使った時に、スワイプして『Done!』させるためのボタンを実装しました。

使用したのは、[react-native-swipeout](https://github.com/dancormier/react-native-swipeout) というプラグインです。
スワイプを実現するための `Swipeout` コンポーネントを提供してくれます。

初めににスワイプした時のボタンを定義します(下の例だと swipeBtns)。
それを表示したい場所に応じて、 Swipeout の right もしくは left プロパティに登録します。

```html:components/Phrases.js
export default class Phrases extends React.Component {
  import { FlatList, TouchableHighlight, View, Text } from 'react-native';
  import Swipeout from 'react-native-swipeout';

  _renderItem({ item, index }) {
    const swipeBtns = [{
      text: (item.isCompleted()) ? 'Revert' : 'Complete',
      backgroundColor: 'blue',
      underlayColor: 'rgba(0,0,0,1)',
      onPress: () => { this._completePhrase({ item, index }) },
    }];

    return (
      <Swipeout
        right={swipeBtns}
        autoClose={true}
        backgroundColor='transparent' >
        <TouchableHighlight
          underlayColor='rgba(192,192,192,1)'
          onPress={() => { this._showPhraseFor(item) }} >
          <View style={styles.phraseSentenceBodyView} >
            <Text
              style={styles.phraseText}
              ellipsizeMode='tail'
              numberOfLines={1} >
              {item.key}
            </Text>
          </View>
        </TouchableHighlight>
      </Swipeout>
    );
  }
}
```

Swipeout のプロパティはこちらです。

プロパティ|型|必須|初期値|説明
-----|-----|-----|-----|-----
autoClose|bool|No|false|ボタンタップ時にスワイプを閉じるかどうか
right|array|No|[]|リスト右側に表示させるボタン
left|array|No|[]|リストの左側に表示させるボタン
backgroundColor|string|No|'#dbddde'|背景色

そしてボタンのプロパティはこちらです。

プロパティ|型|必須|初期値|説明
-----|-----|-----|-----|-----
text|string|No|'Click Me'|ボタンの文言
backgroundColor|string|No|'#b6bec0'|背景色
underlayColor|string|No| |ボタンをタップした時の色
onPress|function|No| |タップ時に実行する関数

## ログイン画面

### Google認証を通してユーザー情報を取得したい

<img width="271" alt="ログイン.png" src="https://qiita-image-store.s3.amazonaws.com/0/40676/a00189d1-8b20-ae0c-c171-5e30940ba3eb.png">

データをどこに保存するか、すごく悩みました。
DBライクなクラウドストレージで最もお手軽なのは何だろうかと色々考えた結果、行き着いた先は Google Sheets でした。

ただ、それにはアプリから Google の認証を通してアクセストークンを取得する必要があります。
少々骨が折れるだろうと、覚悟していました。
しかし救世主現る。

[react-native-google-signin](https://github.com/devfd/react-native-google-signin)

素晴らしいプラグインですです。
簡単に Google のアカウント情報をアプリから取得することができます。
これが開発を加速させてくれました。

使い方はいたって簡単。
ログインしてアカウントのオブジェクトを取得するまでを見ていきましょう。

```js
import GoogleSignin from 'react-native-google-signin';

class Signin extends React.Component {
  signIn() {
    // GooglePlayService がインストールされているか確認 for Android
    GoogleSignin.hasPlayServices({ autoResolve: true }).then(() => {
      // クライアントIDの設定など
      GoogleSignin.configure(config).then(() => {
        // ログインページを立ち上げる
        GoogleSignin.signIn().then((user) => {
          this.setState({ user });
        })
      })
    })
  }
}
```

まず初めに `GoogleSignin.hasPlayServices` をコールします。
これは Android 専用の処理で、アプリに GooglePlayService がインストールされているかをチェックします。
iOS端末の場合は常に true を返します。
`autoResolve: true` を渡すと、インストールされていない場合に『インストールしてね』という旨のプロンプトが表示されます。

次に `GoogleSignin.configure` で、Google の Auth 認証のクライアントIDや、クラウド上のファイルへのアクセスコントロール（読み込み専用か、書き込みも出来るかなど）を設定します。

そして `GoogleSignin.signIn` をコールすると、ログインページが立ち上がります。
メールアドレスとパスワードを入力して認証を通すと、Google アカウントのユーザーオブジェクトを取得できます。

取得できるアカウントのオブジェクトのフォーマットはこちらです。

```js
  {
    id: <user id. do not use on the backend>
    name: <user name>
    givenName: <user given name> (Android only)
    familyName: <user family name> (Android only)
    email: <user email>
    photo: <user picture profile>
    idToken: <token to authenticate the user on the backend>
    serverAuthCode: <one-time token to access Google API from the backend on behalf of the user>
    scopes: <list of authorized scopes>
    accessToken: <needed to access google API from the application>
  }
```

このオブジェクトがアクセストークンを持っているため、それを使って Google の API を叩くことができるようになります！
余談ですが、上のサンプルソースだとコールバックがネストされて扱いづらくなっています。
それを避けたい場合は [async](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function) 及び [await](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/await) を使うと、通常の同期処理と同じように書けるためおすすめです。

## その他

### ページ間のナビゲーションを作りたい

<img width="272" alt="ナビゲーション.png" src="https://qiita-image-store.s3.amazonaws.com/0/40676/0e69a2a0-40ca-6013-4789-f55835aeb118.png">

表示させたい画面をいくつか作成したら、あとはそれらをメニューから行き来できるようにする必要があります。

ナビゲーションの実装には [react-navigation](https://github.com/react-community/react-navigation) を使用しました。
[公式ページのドキュメント](http://facebook.github.io/react-native/docs/navigation.html)でも、こちらのプラグインでの実装例が紹介されています。

ドロワーメニューを作りたかったので、`DrawerNavigator` を使用しました。
英語フレーズの一覧ページ(Phrases.js)と Google へのログインページ(Signin.js)をドロワーに追加しています。

```js:App.js
import React from 'react';
import { StackNavigator, DrawerNavigator } from 'react-navigation';
import Icon from 'react-native-vector-icons/FontAwesome';

import Phrases from './containers/Phrases';
import Signin from './containers/Signin';
import SettingsList from './containers/SettingsList';
import SettingsListChild from './containers/SettingsListChild';

const Drawer = DrawerNavigator({
  Phrases: {
    screen: Phrases
  },
  Signin: {
    screen: Signin
  },
});
```

ドロワーメニューは画面の左端から右にスワイプするだけで表示されます。
しかしわかりやすいよう、ヘッダーの左上にハンバーガーメニューを載せてみます。

```js:App.js
Drawer.navigationOptions = ({ navigation }) => {
  return {
    headerLeft: (
      <Icon.Button
        name='bars'
        color='blue'
        backgroundColor='transparent'
        onPress={() => navigation.navigate('DrawerToggle')}
      />
    ),
  }
};
```

`Icon` コンポーネントは [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) というプラグインが提供してくれているものです。
あらゆるアイコンをアプリ上で簡単に表示させることができます。
タップイベントにはドロワーの表示・非表示のトグルアクションを設定しています。

これでメニューから画面を行き来できるようになりました！

因みにナビゲーションのプラグインだと、React Native に特化した [react-native-navigation](https://github.com/wix/react-native-navigation) もあります。
使ったことはありませんが、デモやドキュメントを見たところ、とても柔軟な設定ができそうな印象を受けました。
もし使ったことがある方がいらっしゃいましたら、ご感想等いただけますと幸いです！

# おわりに
React Native は HTML, CSS, JavaScript の知識をベースにクロスプラットフォームのアプリを開発することができます。
Webエンジニアにはとても親和性が高いと感じました。
クロスプラットフォームアプリ開発には Xamarin もありますので、それぞれの違いを理解し、プロダクトやプロジェクトに最適な選択ができると良いと考えています！

ちなみに作ったアプリ名は AlpacaFlashcards ですが、アルパカもフラッシュカードも特に関係ありません。
当初の想定とは全然違う形に仕上がりました。
リネームしようにも変更箇所が膨大なため、そのままにしています。

[エイチームライフスタイルアドベントカレンダー2017](https://qiita.com/advent-calendar/2017/ateam-lifestyle)、明日は[寿司言語](http://s2terminal.hatenablog.com/entry/2016/01/11/225420)の開発者 @suzuki_sh さんが数学について書いてくれるようです。お見逃しなく！

---

株式会社エイチームライフスタイルでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
http://www.a-tm.co.jp/recruit/