2016.12.14

NativeBase Starter KitのUIコンポーネントをベースにReact Native開発を始める


こんにちは。F.S.です。

もうすぐ年の瀬ですね。この1年を振り返ると、我々のチームではモバイルアプリの開発案件が(流れた話も含めて)だいぶ多くなりました。新規プロダクトゆえに、短期・低コストでの立ち上げが必要なものばかりだったと感じています。
ちょっと前に海外の記事で、ネイティブ開発したiOSアプリを要員追加せずに並行でAndroid版リリースおよびメンテしなければならなくなり、React Nativeで書き換えてうまくいっているという話がありました。どこも似たような境遇だなぁと興味を持って見てました。

我々の開発チームでは2015年5月にReact.jsを始めたこともあり、Android対応がリリースされた頃にReact Nativeを調査していましたが、まだプロダクトへの導入は難しそうだと思っていました。1年以上経って改めて見てみると、だいぶモジュールが充実し、エコシステムが形成されてきているという印象を受けています。Facebookアプリでは、React Native実装の割合が増えつつあるようです。

React Nativeを使ったアプリはShowcaseにて紹介されています。Facebook関連に加えて、Airbnbなどメジャーなアプリがありますね。

Showcase – React Native
https://facebook.github.io/react-native/showcase.html


そろそろ自社プロダクトへの導入も検討できる段階だと感じ、導入にあたってベースに使えそうなボイラープレートを探してみました。

React Native ボイラープレート参考情報


フルスタックなIgnitePepperoniあたりがメジャーなようですが、今回はUIの作り込みが苦手なエンジニアでもシンプルで始めやすいNativeBaseに注目しました。
また、本家のF8 Appも実装のお手本として参考にするのが良いと考えています。


NativeBase


http://nativebase.io/

react-native-nativebase-02

GeekyAnts社が提供するiOS, Androidそれぞれの様式に適したコンポーネントをシングルソースで実現するオープンソースライブラリです。ES6で実装されています。On The Air(アプリアップデートなしにスクリプトを更新する仕組み)にMicrosoftのCode Pushを採用しており、Code Pushを使ったオススメのスターターキットにNativeBase(Pro)がリストアップされていると彼らは宣伝しています。スターターキットでは、部分的にF8 appの実装を参考にしていると思われるところがあります。

なお、NativeBaseでラップしているベンダーコンポーネントもあるのでNativeBaseが提供しているものかどうか、ぱっと見で区別がつかない場合があります。
NativeBaseのドキュメントに記載がなければReact Nativeオリジナルかベンダーコンポーネントのドキュメントを探すという感じです。

Starter Kit


NativeBaseのスターターキットはPremium版、Free版があります。
Premium版はオープンソースのNativeBaseをベースにすぐに使える画面や機能を追加したもので、ECなど何種類かのテンプレートがあり値段も$50〜$1,000と様々です。

NativeBaseのスターターキットの特徴はこのように表現されています。

A simple starter project for React Native + NativeBase + Navigation Experimental + Redux + CodePush apps on iOS and Android.


まずはこちらのFree版を試すということになると思いますが、

Native Starter Kit v5.2.1
https://github.com/start-react/native-starter-kit

最初はNativeBaseの様々なコンポーネントを紹介しているこちらのデモアプリをベースにいじってみるのが良いでしょう。

NativeBase Kitchen Sink
https://github.com/GeekyAnts/NativeBase-KitchenSink

アプリのアーキテクチャはNative Starter Kit v5.2.1と同じです。NativeBaseのドキュメント(http://nativebase.io/docs/v0.5.13/)もこちらのデモに基づいて記載されています。また、デモアプリはストアに公開されているので、最初に実機での動作を確認することができます。

現在のバージョンは以下の通りです。
NativeBase-KitchenSink 0.5.13
・react 15.3.2
・react-native 0.35.0
・native-base: 0.5.13


セットアップ


手元の開発環境は以下の通りです。
  • Mac OS X 10.11.6
  • Xcode 8.1
  • cocoa pods: 1.0.0
  • Android Studio: 2.1.3
  • Android NDK: Revision 13b
  • react-native-cli: 1.2.0
  • node: 7.1.0
  • yarn: 0.17.6

iOS, Androidの開発環境がある前提ですが、スターターキットのセットアップはGithub上のREADME通りで問題なしです。
ただし、こちらのスターターキットは、すでにプロジェクトが作成された状態ですので、任意のプロジェクト名称を設定できません。
react-native-rename でプロジェクト名を変更します。
$ yarn global add react-native-rename
$ react-native-rename --version
1.0.8
$ react-native-rename MyApp
App successfully renamed to "MyApp"

他のスターターキットだと、Igniteは新規プロジェクトを生成する形式なので問題なく、Pepperoniはプロジェクト名の変更スクリプトが付属していました。


コンポーネント作成例


データを読み込んでカード型のリストを表示し、FAB(Floating Action Button)で新規入力するような、よくあるコンポーネントのサンプルです。シングルソースでiOS, Androidに対応しています。

react-native-nativebase-04

実装(一部)はこんな感じです。
import React, { Component } from 'react';
import { Container, Header, Title, Content, Text, Button, Icon, Card, CardItem, Thumbnail, View } from 'native-base';
import ActionButton from 'react-native-action-button';
import Modal from 'react-native-modalbox';

// 中略

  render() {
    const memoListData = this.state.memoListData;

    return (
      <Container theme={myTheme}>
        <View> {/* FAB、Modalを表示させるため */}
          <Header>
            <Title>{I18n.t('memoList')}</Title>

            <Button transparent onPress={this.props.openDrawer}>
              <Icon name="ios-menu" />
            </Button>
          </Header>

          <Content padder>
            {memoListData.map((memo) =>
              <Card key={memo.id} style={[styles.mb, { flex: 0 }]} onPress={() => this.replaceAt('memo')}>
                <CardItem>
                  <Thumbnail square size={80} source={memo.thumbnail ? memo.thumbnail : noImage} />
                  <Text>{memo.note}</Text>
                  <Text note>{moment(memo.updatedAt).format('lll')}</Text>
                </CardItem>
              </Card>
            )}
          </Content>

          {/* react-native-action-buttonを使ったFAB */}
          <ActionButton buttonColor="rgba(231,76,60,1)">
            <ActionButton.Item buttonColor='#9b59b6' title="New Task" onPress={() => this.openModal()}>
              <Icon name="md-create" style={styles.actionButtonIcon} />
            </ActionButton.Item>
            <ActionButton.Item buttonColor='#3498db' title="Notifications" onPress={() => this.openModal()}>
              <Icon name="md-notifications-off" style={styles.actionButtonIcon} />
            </ActionButton.Item>
            <ActionButton.Item buttonColor='#1abc9c' title="All Tasks" onPress={() => this.openModal()}>
              <Icon name="md-done-all" style={styles.actionButtonIcon} />
            </ActionButton.Item>
          </ActionButton>

          {/* react-native-modalboxを使ったモーダル */}
          <Modal style={[styles.modal]} ref={(modal) => {this.modal = modal;}} swipeToClose={this.state.swipeToClose} onClosed={this.onClose} onOpened={this.onOpen} onClosingState={this.onClosingState}>
            <Text style={styles.modalTitle}>New</Text>
            <Button onPress={() => this.closeModal()} transparent style={styles.closeButton}><Icon name="ios-close" style={styles.closeIcon} /></Button>
          </Modal>
        </View>
      </Container>
    );
  }


NativeBaseのコンポーネントは下記の構成になっています。
react-native-nativebase-03

Provider(Redux)とNavigationが基本構成として備わっていることが分かります。各画面は上図右側のコンポーネント(Container)を作成し、統一的なテーマを持たせる様式になります。(必ずしもこれにあわせる必要はありませんが)

なお、ContainerがContentの位置に直接レンダリングするのは今のところ下記に限られています。
・ViewNB
・Content
・Image
・View
・ScrollView
・Fab
Container直下にこれら以外のコンポーネントを入れても無視されてしまいます。安易に入れてしまったコンポーネントがレンダリングされなくてはまったのですが、Containerのソースを見て納得しました。上の例では、FABやModalを入れるためにViewを一枚挟むという若干トリッキーな実装になっています。
※NativeBaseにおいて、FABはまだ正式に提供されていません(ロードマップの一つという扱い)。

また、AndroidではHeaderが最前面に出てしまい、全画面モーダルになりません。こういったところもちょっと厄介なところです。

i18n


我々のプロダクトは大抵、海外展開を意識しているので、本題とは別ですが国際化についても調べてみました。NativeBaseのスターターキットには国際化のフレームワークは入っていませんので、Igniteの実装を参考にします。

モジュールはこちらを使用します。
react-native-i18n
https://github.com/AlexanderZaytsev/react-native-i18n

以下のように翻訳データ、書式データを作成します。
react-native-nativebase-05

ja.json(翻訳データ)
{
  memoList: "メモ一覧",
  memo: "メモ",
  camera: "カメラ",
}

format/ja.json(書式データ)
{
  date: {
    formats: {
      basic: "%Y/%-m/%-d"
    }
  },
  time: {
    formats: {
      basic: "%Y/%-m/%-d %-H:%M"
    }
  },
}

index.js
import I18n from 'react-native-i18n'

I18n.fallbacks = true

I18n.translations = {
  en: require('./en.json')
}

let languageCode = I18n.locale

switch (true) {
  case /^en\-AU$/.test(languageCode):
  case /^en\-GB$/.test(languageCode):
  case /^en\-IE$/.test(languageCode):
  case /^en\-NZ$/.test(languageCode):
    I18n.translations.en = Object.assign(I18n.translations.en, require('./format/en-GB.json'))
    break
  case /^ja\-/.test(languageCode):
    I18n.translations.ja = require('./ja.json')
    I18n.translations.ja = Object.assign(I18n.translations.ja, require('./format/ja.json'))
    break
  case /^vi\-/.test(languageCode):
    I18n.translations.vi = require('./vi.json')
    I18n.translations.vi = Object.assign(I18n.translations.vi, require('./format/vi.json'))
    break
  default:
    I18n.translations.en = Object.assign(I18n.translations.en, require('./format/en-US.json'))
    break
}

利用イメージ
// ルートコンテナでi18n/index.jsをインポートしている前提
// import './i18n'

import I18n from 'react-native-i18n'

console.log(I18n.t('memoList'))   // メモ一覧
console.log(I18n.l("time.formats.basic", 1480501895000)) // 2016/11/30 19:31

デバイスで設定した言語、タイムゾーンが反映されます。
react-native-nativebase-06


日付書式については、momentのlocaleを使う方法もあります。
momentは国に応じた書式がすでに定義されており、「1時間前」のような表記も可能です。
なお、タイムゾーンに対応する場合は、moment-timezoneを使用する必要があります。
react-native-device-info モジュールと組み合わせるとこんな感じです。
import DeviceInfo from 'react-native-device-info'
import moment from 'moment-timezone'
moment().locale(DeviceInfo.getDeviceLocale()) // ja-JP
moment().tz(DeviceInfo.getTimezone()) // Asia/Tokyo
// ロケールに応じたファイルを読み込み
require('moment/locale/ja')
console.log(moment(1480501895000).format('lll')) // 2016年11月30日 午後7時31分


長くなってきたので、今回はここまでにします。
以降、実装調査を進めているものたちです。ToDoメモみたいになってますが。。

カメラ, OCR
react-native-camera
react-native-tesseract-ocr
OCRは今の所iOSが未実装のようです。

GraphSQL, Relay
F8 Appが参考になります。今後Elixir/Phoenixのバックエンドに接続する可能性があるので、サーバ側の実装はGraphQL Elixirを試してみたいと思います。

Code Push
キーは登録したけど、まだ動作を試せてないです。。

ストレージ
package.jsonにredux-persitが入ってますが、スターターキットでは使用していません。
F8 Appを参考にStoreの永続化が可能ですが、AsyncStorageにしか対応していないのでどうするかは要検討です。
Realmがうまく使えたりしないかなーと。

GPS
react-native-gps

通知
F8 Appではreact-native-push-notificationを使ってますが、我々はこれまでGCM, FCMを使っていたので、react-native-fcmを試してみたいと思います。

アナリティクス(Firebase)
react-native-firebase-analytics


今回のまとめ


NativeBaseのコンポーネント構成を理解し、UIサンプルを作成してみました。
レンダリングが制御されていたりと最初は戸惑うことがありましたが、ある程度理解すれば一般的なUIを持ったアプリが容易に作成できそうです。
NativeBaseで提供されていないコンポーネントもnodeモジュールを探すとたいていは見つかるので、些細なところにこだわりすぎなければ手早くアプリの開発が進められるのではないでしょうか。
次のアプリ開発の機会に導入してみたいと思います。


それでは、また。


次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。