Edited at

React Nativeによるモバイルアプリ開発はじめの一歩 〜プロジェクト作成・基本・Redux導入・画面遷移・サーバーとの通信〜

More than 1 year has passed since last update.

React Native は、Reactを用いたモバイルアプリ実装のためのフレームワークです。

特徴としては


  1. 基本的には1ソースでiOS、Android両方のアプリを作ることができる(異なるところは分けて書くことも可能)

  2. プログラミング言語はECMAScript2015。

  3. ビュー部分のライブラリはReact.jsであり、JSXというHTMLっぽいタグで記述する。

  4. 状態が変化すると表示にインタラクティブに反映される。

などがあります。

ここでの手順はMacOSXでHomebrewがインストール済みであると仮定しています。

Homebrewが入ってない方は、Homebrewインストール手順を参考に入れておいてください。

WindowsやLinuxは適当に... Linuxならnode入れたらあとは同じだろうけど、Windowsはわかりません。。


nodeとcreate-react-nativeのインストール

$ brew install node

$ brew install watchman

なお、これを書いている2017年9月時点で、npm5ではcreate-react-nativeは動作しません。すでにnpm5をいれちゃってる場合は以下のようにして4系を入れる必要があります。

$ sudo npm install -g npm@4.6.1

$ sudo npm install -g create-react-native-app


プロジェクトの新規作成

プロジェクト名を MobileTimeline として新しくプロジェクトを作ります。

$ create-react-native-app MobileTimeline

$ cd MobileTimeline

結構かかります。コーヒーでも飲みながら待ちましょう☕️


依存ライブラリのインストール

あとで使うのでReduxを入れておきます。

$ npm install --save redux react-redux

あ、それと型チェックライブラリの prop-types も。

$ npm install --save prop-types

画面遷移で使うナビゲーションライブラリ

$ npm install --save react-navigation

そしてHTTP APIを叩くためのライブラリ

$ npm install --save react-fetch


起動

とりあえず起動します。

$ npm start

が、エラーがでました

(ただし、このエラーが出た時は watchman 入れてなかったので、入れてたらエラーでないかも)

> MobileTimeline@0.1.0 start /Users/shin/src/react-learn/native/MobileTimeline

> react-native-scripts start

0:10:30: Unable to start server
See https://git.io/v5vcn for more information, either install watchman or run the following snippet:
sudo sysctl -w kern.maxfiles=5242880
sudo sysctl -w kern.maxfilesperproc=524288

とりあえず言われた通りに

$ sudo sysctl -w kern.maxfiles=5242880

もう一回起動

$ npm start

お、こんなんでました。

なにやら Expo というアプリをインストールしろと言ってくるので、iPhoneで http://expo.ioにアクセスしてインストールします。

(Androidはあとで調査)

Expoをインストールしたら起動して、 Scan QR code をタップ。表示されたQRコードを読み込むと自動的にアプリが起動します。この時、iPhoneとパソコンがネットワークを介して接続可能であることが必要です。

なお、アプリ画面から再度Expoの画面に戻るにはiPhoneを振ってシェイクします。


プロジェクト生成直後のApp.js

src/App.js を開いてみるとこんな感じでした。


ボタンの配置

ここでボタンを配置して、ボタンを押したらアラートが表示されるというシンプルな機能を実装します。

変更するのは App.js のみです。

まず、新たに Button Alert という2つのコンポーネントを使うので、これらをimportし、 <View> タグ内にボタンを配置します。

また、 onPress でイベント発生時に実行するメソッドを指定しておき、そこにアラートを出す処理を記述します。


App.js

import React from 'react';

import { StyleSheet, Text, View, Button, Alert } from 'react-native';

export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Changes you make will automatically reload.</Text>
<Text>Shake your phone to open the developer menu.</Text>

<Button title="こんにちわ世界" onPress={this._handlePress} />
</View>
);
}

_handlePress() {
Alert.alert("こんにちは!!");
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});


これで次のようなアプリが動きます。


stateによる値の保持

続いて、 state を使って値を保持するサンプルです。

以下のようにApp.jsを修正します。 テキスト入力エリアに文字を入力すると、その文字をリアルタイムにボタンのラベルに反映し、さらにボタンがタップされた時にその文字をアラートとして表示します。


App.js

import React from 'react';

import { StyleSheet, Text, View, Button, Alert , TextInput} from 'react-native';

export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {hello: "こんにちは"}
}

render() {
return (
<View style={styles.container}>

<TextInput
style={{height: 40, width:300, borderColor: 'gray', borderWidth: 1}}
onChangeText={(text) => this.setState({hello: text})}
value={ this.state.hello } />

<Button title={ this.state.hello }
onPress={ this._handlePress.bind(this) } />

</View>
);
}

/* ボタンをクリックされたら現在の state.helloの内容をアラートする。 */
_handlePress(e) {
Alert.alert(this.state.hello);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});



コンポーネントの分割

先ほどのアプリは、ビューの内容を全て App.js 内に書いています。しかし大きなアプリになると一定の塊ごとに分割することがあるでしょう。

ここでは App.js からテキスト入力エリア、ボタンを、別の Hello.js へ分割します。

まずは App.js を編集。


App.js

import React from 'react';

import { StyleSheet, Text, View } from 'react-native';
import Hello from './Hello.js';

class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Hello />
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

export default App;


App.jsではテキストエリアやボタンの配置はせず、単に Helloコンポーネント を呼んでるだけです。

そして、もともと App.js にあった内容を Hello.js へ移します。


Hello.js

import React from 'react';

import { View, Button, Alert , TextInput} from 'react-native';

class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {hello: "こんにちは"}
}

render() {
return (
<View>
<TextInput
style={{height: 40, width:300, borderColor: 'gray', borderWidth: 1}}
onChangeText={(text) => this.setState({hello: text})}
value={ this.state.hello } />

<Button title={ this.state.hello }
onPress={ this._handlePress.bind(this) } />
</View>
);
}

/* ボタンをクリックされたら現在の state.helloの内容をアラートする。 */
_handlePress(e) {
Alert.alert(this.state.hello);
}
}

export default Hello;



Reduxの導入

これまでは、それぞれのコンポーネントに state をもたせて、 this.setState() として保持している値を更新したりしてきましたが、ここでFluxの考え方を導入します。

Fluxの考え方については私がごちゃごちゃいうより解りやすい記事があるのでそちらを参照してください。

Fluxとはなんなのか

Flux自体は考え方なのですが、このFluxの考え方をもとに作られたReduxというフレームワーク(?)があるので、これを使います。

結局FluxやらReduxやらって何なのか個人的なまとめ


reducer.jsの新規作成

まずはReducerを作ります。ファイル名は reducer.js です。

Reducerでは、 stateaction を受け取る reducer(state, action) という関数を定義し、他のソースからも参照できるように export しています。

ここでは action.type の値を元に処理を分岐しています。 action.type には、このあと作る action.js で生成するアクションのオブジェクトで、必ず type プロパティだけは定義することになっていて、そこで type プロパティに設定した値によって reducer.js で処理が分かれるってことになります。

なので、処理内容を増やすにはここでswitch文に新たなcaseを増やすことになります。


reducer.js

const initialState = {

hello: "Hello World"
}

export default function reducer(state = initialState, action) {
switch(action.type) {

/* hello文字列を変更する */
case 'HELLO_WORLD':
return {
...state,
hello: action.hello
};

default:
return state
}
}



action.jsの新規作成

続いてAction Createrを実装します。ファイル名は action.js です。

ここでは updateHello という関数を定義し、 dispatch(action) 関数にオブジェクトを渡します。このオブジェクトは上で書いたように、typeプロパティだけは必須、残りの項目は任意で定義します。

ここでdispatchに渡したオブジェクトが先ほどのreducer関数でactionとして渡されます。


action.js

export function mapStateToProps(state) {

return state;
}

export function mapDispatchToProps(dispatch) {
return {
/* hello文字列を変更する */
updateHello: (text) => {
dispatch( {type: 'HELLO_WORLD', hello: text} );
}
}
}



storeの生成

App.js において、 状態を管理する store を生成し、 Provider に渡す処理を追加します。

ポイントになるのは


  1. 冒頭で createStoreProvider をimportしている点。


  2. const store = createStore(reducer); でstoreを生成している点。


  3. render() 関数内で、 <Provier store={ store }>...</Provider> として大きく Provider タグで囲って store を渡している点。

の3つです。


App.js

import React from 'react';

import { StyleSheet, View } from 'react-native';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import Hello from './Hello.js';
import reducer from './reducer.js';

const store = createStore(reducer);

class App extends React.Component {
render() {
return (
<Provider store={ store }>
<View style={styles.container}>
<Hello />
</View>
</Provider>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

export default App;



connectによるReactNativeとReduxの接続

最後に、Reduxによって管理されたstateの状態を参照したり変更したりするコンポーネントにおいて、ReactNativeとReduxを接続します。

また、これまでコンポーネント内で定義した state を使っていた部分を新たにReduxによって保持されているstateを使うように変更します。

修正後のソースが以下です。


Hello.js

import React from 'react';

import { View, Button, Alert , TextInput} from 'react-native';
import { connect } from 'react-redux';
import {mapStateToProps, mapDispatchToProps} from './action.js';

class Hello extends React.Component {
render() {
return (
<View>
<TextInput
style={{height: 40, width:300, borderColor: 'gray', borderWidth: 1}}
onChangeText={ (text) => this.props.updateHello(text) }
value={ this.props.hello } />

<Button title={ this.props.hello }
onPress={ this._handlePress.bind(this) } />
</View>
);
}

/* ボタンをクリックされたら現在の state.helloの内容をアラートする。 */
_handlePress(e) {
Alert.alert(this.props.hello);
}
}

export default connect(mapStateToProps, mapDispatchToProps)(Hello);


最後の行で、これまでは export default Hello; だったところが、 export default connect(mapStateToProps, mapDispatchToProps)(Hello); となって、 action.js で定義した関数と関連づけられている点に注意してください。


動作確認

起動してテキストを編集するとリアルタイムにボタンのラベルに反映されています。

また、ボタンをタップすると、現在の state.hello の内容でアラートが表示されます。


画面遷移

今やほとんどのアプリで画面遷移は必須です。なので、ReactNativeとReduxでアプリを実装している場合の画面遷移を導入してみます。

先にどんな機能を実装するのか確認します。

トップ画面は下の通り。 HelloCheck という2つのボタンが表示されるのみです。

このトップ画面はタイトルが ホーム と表示されています。

トップ画面で Hello をタップすると、iOSアプリの場合は画面が右に遷移して以下のような画面が表示されます。

これは、これまでのところで実装して来た Hello コンポーネントに他なりません。

テキストエリアで文字列を編集すると、リアルタイムにボタンのラベルが同じ文字列に変わります。

そして左上に ホーム というボタンが表示され、これをタップすると先ほどのトップ画面に戻ります。

今度はトップ画面で Check をタップすると、また画面が右に遷移して、 チェック画面 というタイトルの画面が表示されます。

ここで画面中央にテキストで表示されるのは、先ほどの Hello コンポーネントの画面でテキストエリアに入力した文字列です。


App.jsの編集

ひとつづつ見ていきます。まずは App.js ですが、これは以下のようなソースになります。

ポイントとしては、



  1. stackNavigator によってナビゲーターを作っているところ

  2. そのナビゲーターを App コンポーネントの render(){...} でタグとして使用しているところ

  3. そしてコンポーネントのタグ全体を <Provider stor={ store }>...</Provider> で包んで、Reduxのstoreで管理されている状態へのアクセスをアプリ全体で可能にしている点

の3点です。


App.js

import React from 'react';

import { View } from 'react-native';
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
import { Provider } from 'react-redux';

import { store } from './store.js';
import { styles } from './styles.js';
import Hello from './Hello.js';
import CheckScreen from './CheckScreen.js';
import HomeScreen from './HomeScreen.js';

const AppNavigator = StackNavigator({
Home: {screen: HomeScreen},
Hello: {screen: Hello},
Check: {screen: CheckScreen}
});

class App extends React.Component {
render() {
return (
<Provider store={ store }>
<AppNavigator />
</Provider>
)
}
}

export default App;



sotre.jsの新規作成

先ほどまでは App.js 内でstoreを作っていましたが、ちょっと分かりにくくなって来たのでこれを外に出します。

ファイル名は store.js です。


store.js

import { createStore } from 'redux';

import reducer from './reducer.js';

export const store = createStore(reducer);



styles.jsの新規作成

store.js同様に、stylesも App.js から外に出してimportするようにリファクタリングします。


styles.js

import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});



HomeScreen.jsの新規作成

先ほどの修正によって、 App.js はナビゲーションを呼び出すだけとなりましたので、これまで App.js 内にあったビューの部分を新たに HomeScreen.js というファイルに移します。

これまでのコンポーネントと異なる点は、 static navigationOptions = {...} としてナビゲーションで使用するオプション項目を設定している点です。ここで title: "ホーム" としているので、実際の画面のタイトルが ホーム となっているのです。


HomeScreen.js

import React from 'react';

import { View, Button } from 'react-native';
import { styles } from './styles.js';

class HomeScreen extends React.Component {
static navigationOptions = {
title: "ホーム"
}

render() {
const { navigate } = this.props.navigation;

return (
<View style={styles.container}>
<Button title="Hello" onPress={ () => navigate('Hello') } />
<Button title="Check" onPress={ () => navigate('Check') } />
</View>
);
}
}

export default HomeScreen;



Hello.jsの編集

Hello.jsは基本的にはそのままでいいのですが、一部修正します。

ひとつめは、先ほどの HomeScreen.js と同様に、ナビゲーションのオプションとしてタイトルを設定しているところ。

もう一つは、これまではやっていませんでしたが、 PropTypes による型チェックを導入しているところです。

これによって、 Hello コンポーネントに渡される props には、 hello というプロパティが含まれており、その値は文字列でなければならないという制約ができます。

型がちがっていたなどの場合は、実行画面で画面下に警告が表示されます。


Hello.js

import React from 'react';

import { View, Button, Alert , TextInput} from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { mapStateToProps, mapDispatchToProps } from './action.js';
import { styles } from './styles.js';

class Hello extends React.Component {
static navigationOptions = {
title: "ハローワールド"
}

render() {
return (
<View style={styles.container}>
<TextInput
style={{height: 40, width:300, borderColor: 'gray', borderWidth: 1}}
onChangeText={ (text) => this.props.updateHello(text) }
value={ this.props.hello } />

<Button title={ this.props.hello }
onPress={ this._handlePress.bind(this) } />
</View>
);
}

/* ボタンをクリックされたら現在の state.helloの内容をアラートする。 */
_handlePress(e) {
Alert.alert(this.props.hello);
}
}

/* 型チェック */
Hello.propTypes = {
hello: PropTypes.string.isRequired
}

export default connect(mapStateToProps, mapDispatchToProps)(Hello);



CheckScreen.jsの新規作成

最後に CheckScreen コンポーネントを新規で作ります。このコンポーネントの役割は、 this.props.hello に保存された文字列をテキストで表示するだけです。


CheckScreen.js

import React from 'react';

import { View, Text} from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { mapStateToProps, mapDispatchToProps } from './action.js';
import { styles } from './styles.js';

class CheckScreen extends React.Component {
static navigationOptions = {
title: "チェック画面"
}

render() {
return (
<View style={styles.container}>
<Text>{ this.props.hello }</Text>
</View>
);
}
}

CheckScreen.propTypes = {
hello: PropTypes.string.isRequired
}

export default connect(mapStateToProps, mapDispatchToProps)(CheckScreen);


これで最初に確認した動作が実現しました。

この時点でのソースコードはhiroeorz/MobileExsampleのnaviブランチにあります。


サーバーとのHTTP通信

ここではアプリにほぼ必須の、サーバーとの通信のやり方を記します。

やることは、 fetch を使ってデータを取得し、取得したデータを先ほどのアクションクリエーター( action.js )で定義した関数に流し込み、最終的に reducer.js で定義した処理を行うことで状態を更新し、その結果がViewに反映されます。

なお、情報を取得するのは

http://facebook.github.io/react-native/movies.json

から行います。


動作

先に、これから作る機能でどのようなことが実現するのか見ておきます。

トップ画面。ここで HTTP API と表示されたボタンをタップします。

続いて表示されるのは、ムービー情報取得ビュー。現時点では内容は空。

取得 ボタンをタップすると、サーバーから情報を取得して表示。


HttpScreen.jsの新規実装

まずは新たに、 HttpScreen.js というファイル名のコンポーネントを実装します。

ポイントは


  1. 取得ボタンを設置して、タップされたら _handleGetMovieList 関数を実行。


  2. _handleGetMovieList 内では fetch を使って先ほどのHTTP APIを叩きJSONを取得。

  3. 取得したJSONをパースして this.props.updateMovieList(responseJson) に流し込む(updateMovieListはこの後実装します)。


  4. PropTypes で渡されるオブジェクトをチェックしている

点です。


HttpScreen.js

import React from 'react';

import { View, Text, Button, Alert } from 'react-native';
import { connect } from 'react-redux';
import { mapStateToProps, mapDispatchToProps } from './action.js';
import PropTypes from 'prop-types';

import { styles } from './styles.js';

class HttpScreen extends React.Component {
static navigationOptions = {
title: "HTTP通信"
}

render() {
var movies = [];

for (let i in this.props.movieList.movies) {
var movie = this.props.movieList.movies[i];
movies.push(<Text key={ i }> * { movie.title }</Text>);
}

return (
<View style={styles.container}>
<Button title="取得" onPress={ this._getMoviewList.bind(this) } />
<Text>タイトル: { this.props.movieList.title }</Text>
<Text>説明: { this.props.movieList.description }</Text>
<Text>ムービー一覧</Text>
{ movies.map((m) => {return m;}) }
</View>
);
}

_getMoviewList(e) {
return fetch('http://facebook.github.io/react-native/movies.json')
.then((response) => response.json() )
.then((responseJson) => {
this.props.updateMoviewList(responseJson);
})
.catch((error) => {
Alert.alert("通信エラーが発生しました");
console.error(error);
});
}
}

HttpScreen.propTypes = {
moviewList: PropTypes.shape({
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
movieList: PropTypes.arrayOf({
title: PropTypes.string,
releaseYear: PropTypes.string
})
})
}

export default connect(mapStateToProps, mapDispatchToProps)(HttpScreen);



action.jsにアクションを追加

次に、 action.jsupdateMovieList を実装します。実装後のコードはこんな感じです。

ポイントは {type: 'MOVIE_LIST', movieList: movieList} というアクションオブジェクトを作ってdispatch関数に渡しているところです。


action.js

import store from './store.js';

export function mapStateToProps(state) {
return state;
}

export function mapDispatchToProps(dispatch) {
return {
/* テキストエリアの内容を state.hello に保存する */
updateHello: (text) => {
dispatch( {type: 'HELLO_WORLD', hello: text} );
},

/* movieListオブジェクトを state.movieListに保存する */
updateMoviewList: (movieList) => {
dispatch( {type: 'MOVIE_LIST', movieList: movieList} )
}
}
}



reducer.jsにReducerを追加

続いて reducer.js を編集します。ポイントは


  1. 最初に const initialState として状態の初期化を行うところで空の movieList オブジェクトを定義しています。


  2. action.type'MOVIE_LIST' の場合に、 state.movieList を受け取ったオブジェクトで更新します。


reducer.js

const initialState = {

hello: "Hello World",
movieList: { title: "", description: "", movies: [] }
}

export default function reducer(state = initialState, action) {
switch(action.type) {

/* テキストエリアの内容を state.hello に保存する */
case 'HELLO_WORLD':
return {
...state,
hello: action.hello
};

/* ムービー情報をJSONで取得したものを state.movieList に保存する */
case 'MOVIE_LIST':
return {
...state,
movieList: action.movieList
};

default:
return state
}
}



App.jsにてナビゲーターの追加

HttpScreen.js をインポートして HttpScreen へのナビゲーターを追加します。


App.js

import React from 'react';

import { View } from 'react-native';
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
import { Provider } from 'react-redux';

import { store } from './store.js';
import { styles } from './styles.js';
import Hello from './Hello.js';
import CheckScreen from './CheckScreen.js';
import HomeScreen from './HomeScreen.js';
import HttpScreen from './HttpScreen.js'; //追加!

const AppNavigator = StackNavigator({
Home: {screen: HomeScreen},
Hello: {screen: Hello},
Check: {screen: CheckScreen},
Http: {screen: HttpScreen} //追加!
});

class App extends React.Component {
render() {
return (
<Provider store={ store }>
<AppNavigator />
</Provider>
)
}
}

export default App;



HomeScreen.jsにボタンを追加

最後に、 HomeScreen.js 内で、先ほど作った HttpScreen へジャンプするためのボタンを追加します。


HomeScreen.js

import React from 'react';

import { View, Button } from 'react-native';

import { styles } from './styles.js';

class HomeScreen extends React.Component {
static navigationOptions = {
title: "ホーム"
}

render() {
const { navigate } = this.props.navigation;

return (
<View style={styles.container}>
<Button title="Hello" onPress={ () => navigate('Hello') } />
<Button title="Check" onPress={ () => navigate('Check') } />
<Button title="HTTP API" onPress={ () => navigate('Http') } />
</View>
);
}
}

export default HomeScreen;


これで完了です。実行すればHTTP APIからデータを取得して表示します。

この時点でのソースコードは hiroeorz/MobileExampleのhttpapiブランチにあります。