React Native は、Reactを用いたモバイルアプリ実装のためのフレームワークです。
特徴としては
- 基本的には1ソースでiOS、Android両方のアプリを作ることができる(異なるところは分けて書くことも可能)
- プログラミング言語はECMAScript2015。
- ビュー部分のライブラリはReact.jsであり、JSXというHTMLっぽいタグで記述する。
- 状態が変化すると表示にインタラクティブに反映される。
などがあります。
ここでの手順は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
ボタンの配置
ここでボタンを配置して、ボタンを押したらアラートが表示されるというシンプルな機能を実装します。
変更するのは App.js
のみです。
まず、新たに Button
Alert
という2つのコンポーネントを使うので、これらをimportし、 <View>
タグ内にボタンを配置します。
また、 onPress
でイベント発生時に実行するメソッドを指定しておき、そこにアラートを出す処理を記述します。
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を修正します。 テキスト入力エリアに文字を入力すると、その文字をリアルタイムにボタンのラベルに反映し、さらにボタンがタップされた時にその文字をアラートとして表示します。
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
を編集。
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
へ移します。
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では、 state
と action
を受け取る reducer(state, action)
という関数を定義し、他のソースからも参照できるように export
しています。
ここでは action.type
の値を元に処理を分岐しています。 action.type
には、このあと作る action.js
で生成するアクションのオブジェクトで、必ず type
プロパティだけは定義することになっていて、そこで type
プロパティに設定した値によって reducer.js
で処理が分かれるってことになります。
なので、処理内容を増やすにはここでswitch文に新たなcaseを増やすことになります。
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として渡されます。
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
に渡す処理を追加します。
ポイントになるのは
- 冒頭で
createStore
とProvider
をimportしている点。 -
const store = createStore(reducer);
でstoreを生成している点。 -
render()
関数内で、<Provier store={ store }>...</Provider>
として大きくProvider
タグで囲ってstore
を渡している点。
の3つです。
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を使うように変更します。
修正後のソースが以下です。
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でアプリを実装している場合の画面遷移を導入してみます。
先にどんな機能を実装するのか確認します。
トップ画面は下の通り。 Hello
と Check
という2つのボタンが表示されるのみです。
このトップ画面はタイトルが ホーム
と表示されています。
トップ画面で Hello
をタップすると、iOSアプリの場合は画面が右に遷移して以下のような画面が表示されます。
これは、これまでのところで実装して来た Hello
コンポーネントに他なりません。
テキストエリアで文字列を編集すると、リアルタイムにボタンのラベルが同じ文字列に変わります。
そして左上に ホーム
というボタンが表示され、これをタップすると先ほどのトップ画面に戻ります。
今度はトップ画面で Check
をタップすると、また画面が右に遷移して、 チェック画面
というタイトルの画面が表示されます。
ここで画面中央にテキストで表示されるのは、先ほどの Hello
コンポーネントの画面でテキストエリアに入力した文字列です。
App.jsの編集
ひとつづつ見ていきます。まずは App.js
ですが、これは以下のようなソースになります。
ポイントとしては、
-
stackNavigator
によってナビゲーターを作っているところ - そのナビゲーターを
App
コンポーネントのrender(){...}
でタグとして使用しているところ - そしてコンポーネントのタグ全体を
<Provider stor={ store }>...</Provider>
で包んで、Reduxのstoreで管理されている状態へのアクセスをアプリ全体で可能にしている点
の3点です。
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
です。
import { createStore } from 'redux';
import reducer from './reducer.js';
export const store = createStore(reducer);
styles.jsの新規作成
store.js同様に、stylesも App.js
から外に出してimportするようにリファクタリングします。
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: "ホーム"
としているので、実際の画面のタイトルが ホーム
となっているのです。
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
というプロパティが含まれており、その値は文字列でなければならないという制約ができます。
型がちがっていたなどの場合は、実行画面で画面下に警告が表示されます。
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
に保存された文字列をテキストで表示するだけです。
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
というファイル名のコンポーネントを実装します。
ポイントは
- 取得ボタンを設置して、タップされたら
_handleGetMovieList
関数を実行。 -
_handleGetMovieList
内ではfetch
を使って先ほどのHTTP APIを叩きJSONを取得。 - 取得したJSONをパースして
this.props.updateMovieList(responseJson)
に流し込む(updateMovieListはこの後実装します)。 -
PropTypes
で渡されるオブジェクトをチェックしている
点です。
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.js
に updateMovieList
を実装します。実装後のコードはこんな感じです。
ポイントは {type: 'MOVIE_LIST', movieList: movieList}
というアクションオブジェクトを作ってdispatch関数に渡しているところです。
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
を編集します。ポイントは
- 最初に
const initialState
として状態の初期化を行うところで空のmovieList
オブジェクトを定義しています。 -
action.type
が'MOVIE_LIST'
の場合に、state.movieList
を受け取ったオブジェクトで更新します。
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
へのナビゲーターを追加します。
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
へジャンプするためのボタンを追加します。
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ブランチにあります。