Help us understand the problem. What is going on with this article?

React Nativeハンズオン〜initからtodoアプリ実装まで〜

More than 3 years have passed since last update.

React Nativeのハンズオン用に react-native init <プロジェクト名> してからTodoアプリを作るまでの流れを記事にします。store管理にMobXを使う予定ですが、他に使いたいものがある場合は適宜読み替えてください。また、限られた時間で、説明しづらいところはぼかしています。詳しく知りたい方はドキュメントなどを参考にしてください。

ハンズオンでできる予定のコードは下記に公開しています。
https://github.com/OshiroSeiya/AwesomeProject

対象者

  • ワタシハジャバスクリプトチョットデキル。って方
  • React Nativeのざっくりとした使い方知りたいって方
  • Mac使ってる人(ios版で書くのでandroidがやりたい人は読み替えてください)

必要となる周辺知識

最低限カッコ書きの部分のイメージがついてるとわかりやすいと思います

  • Babel(いい感じに変換してくれる)
  • React(いい感じに描画してくれる)
  • MobX(state管理をReactから奪う)

環境構築

node, yarn, watchman, react-native-cliのインストール
(yarnは好みなので入れてますがnpmでも問題ないです)

$ brew install node
$ brew install yarn
$ brew install watchman
$ npm install -g react-native-cli

上記のツールがインストールできているか確認しましょう

$ node -v
v8.2.1
$ yarn --version
0.27.5
$ watchman -v
4.7.0
$ react-native -v
react-native-cli: 2.0.1
react-native: n/a - not inside a React Native project directory

Xcodeインストールページ

Xcodeはインストール後に一度起動して利用規約的なものを許可する必要があります。
※僕の手元でXcodeのインストールに30分ぐらいかかりました。

プロジェクト作成

今回は公式のサンプルに合わせて、AwesomeProjectって名前で作ることにします。

$ react-native init AwesomeProject

ディレクトリ移動して実行できるか確認します。
うまく行けば、iOSシミュレータが起動します。

$ cd AwesomeProject
$ react-native run-ios

初回ビルドなのでそこそこ時間かかります。

Welcome to React Native! がシミュレータに表示されていればokです

うまくいかない場合は

  • 環境構築時にすべてインストールできているかもう一度確認しましょう
  • Xcodeの利用規約的なものを許可されてるか起動して確認しましょう(たまにアップデート来てて昔okしたのにまた聞かれてることがあります)

ハンズオン開始

ディレクトリ構造の変更

初期のディレクトリ構造は下記です。

$ tree -L 1
.
├── __tests__        // テストコード入れる場所(今回は使わない)
├── android          // Androidのプロジェクト(基本的ネイティブの機能を使うとき以外触らない)
├── app.json         // プロジェクト名など
├── index.android.js // Androidのエントリーファイル
├── index.ios.js     // Iosのエントリーファイル
├── ios              // Iosのプロジェクト(基本ネイティブの機能を使うとき以外触らない)
├── node_modules     // パッケージたち(インストールしたら入る)
├── package.json     // パッケージ情報記載
└── yarn.lock        // パッケージのバージョン情報記載(自動生成)

androidとiosで共通のコードをまとめたいので src ディレクトリを生成して main.js に共通処理をまとめて、エントリーファイルから呼び出せるように変更します。

$ tree -L 1
.
├── __tests__
├── android
├── app.json
├── index.android.js
├── index.ios.js
├── ios
├── node_modules
├── package.json
├── src               // これを生成した!
└── yarn.lock

main.jsindex.ios.jsをコピーして配置してください。

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/b5ad7a686c1944b9f13da8cf2918bc0d89e21ba5

Welcome to React Native! という文字列を変更してシミュレーターに反映されればokです。

パッケージの追加

MobXが使いたいやら、かんたんに見た目よくしたいやら、オブジェクトのスプレッド演算子が使いたいやら、デコレーターが使いたいやらあるので下記のパッケージを追加します。

$ yarn add babel-preset-react-native-stage-0 babel-plugin-transform-decorators-legacy -D
$ yarn add mobx mobx-react react-native-vector-icons react-native-elements

デコレーター使いたい、スプレッド演算子使いたい、その他諸々の理由で下記に変更します。

.babelrc
{
-  "presets": ["react-native"]
+  "presets": [
+    "react-native",
+    "react-native-stage-0"
+  ],
+  "plugins": [
+    "transform-decorators-legacy"
+  ]
}

https://github.com/OshiroSeiya/AwesomeProject/commit/b97b3e2fd36d657362db930c2792b41e314a0a54

ネイティブコードが含まれているパッケージはlinkを貼る必要があります。
例えば、 react-native-vector-icons が該当します。

$ react-native link

自動生成なので見る必要ないけど一応サンプルは下記で見れます。

https://github.com/OshiroSeiya/AwesomeProject/commit/c1930aece1d71533ac7b028776d3e208038a3cc4

mock作成する

下記のようなmockを作成します。

mock_1.png

データ構造

todoの構造を下記とします。

src/components/index.js
const todoList = [
  { id: 1, message: 'todo!', isComplete: false, },
  { id: 2, message: 'complete todo!', isComplete: true, },
  { id: 3, message: 'complete todo!', isComplete: true, },
];

React Native Elementsのパッケージを使ってみる

react-native-elementsCheckBox Componentを使って実装します。参考

配列をループさせてDOMを生成する場合は key を入れる必要があります。

title はチェックボックスに表示する文字列が入ります。
checked はチェックをつけるかどうかを判断します。
on~ はイベント処理を定義します。

src/components/index.js
const TodoCheckBox = (row) => (
  <CheckBox
    key={row.id}
    title={row.message}
    onPress={() => console.log('todoトグル')}
    onLongPress={() => {
      Alert.alert(
        '確認',
        `[${row.message}]を削除しますか?`,
        [
          { text: '削除しない', style: 'cancel' },
          { text: '削除', onPress: () => console.log('todo削除') },
        ],
      );
    }}
    checked={row.isComplete}
  />
);

Mock全体

View はwebで言う div 的なものだと思ってください。
View ではスクロールできないので ScrollView も使用します。

cssで画面のレイアウト設定を行います。参考
flex は超絶難しいので、詳しく知りたい人は調べてください。

react-native-elementsFormInput を利用しています。詳しく知りたい人は参照

src/components/index.js
export default class Todo extends Component {
  render() {
    const TodoCheckBox = (row) => (
      <CheckBox
        key={row.id}
        title={row.message}
        onPress={() => console.log('todoトグル')}
        onLongPress={() => {
          Alert.alert(
            '確認',
            `[${row.message}]を削除しますか?`,
            [
              { text: '削除しない', style: 'cancel' },
              { text: '削除', onPress: () => console.log('todo削除') },
            ],
          );
        }}
        checked={row.isComplete}
      />
    );

    return (
      <View style={{ flex: 1 }}>
        <View style={{ marginTop: 20, flexDirection: 'row'}}>
          <View style={{ flex: 5 }}>
            <FormInput
              autoCapitalize="none"
            />
          </View>
          <View style={{ flex: 1 }}>
            <Button
              onPress={() => console.log('add')}
              title="ADD"
            />
          </View>
        </View>
        <ScrollView style={{ flex: 1 }}>
          {todoList.map(TodoCheckBox)}
        </ScrollView>
      </View>
    );
  }
}

下記を実行して見た目を確認してください。

$ react-native run-ios

サンプルは下記でみれます。

https://github.com/OshiroSeiya/AwesomeProject/commit/6e8dd54e5eb70827ce4c6204ef5fa72c32cd6359

ストアー作成する

ストアーとはデータを保持するクラスです。
計算処理やデータ取得処理、イベント処理などを書いたりもします。

@observable list = [] を利用して画面描画した際、変数変更時に自動的に画面に反映されます。

こちらを参照

src/stores/todo.js
import { observable } from 'mobx';

export default class TodoStore {
  @observable list = [
    { id: 1, message: 'todo!', isComplete: false, },
    { id: 2, message: 'complete todo!', isComplete: true, },
    { id: 3, message: 'complete todo!', isComplete: true, },
  ];
}

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/4f4e7d4d79bf174d2c2b0d61ce8a23bac88e8871

Provider でComponentを囲むと中でストアーを使うことができます。
詳細はこちら

src/main.js
import React from 'react';
+import { Provider } from 'mobx-react/native';
import Todo from './components';
+import TodoStore from './stores/todo';
+
+const stores = {
+  todo: new TodoStore(),
+};

export default () => (
-  <Todo />
+  <Provider {...stores}>
+    <Todo />
+  </Provider>
);

Componentに先程作った todo storeを渡していきます。

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/13f9577564c056e40340360d97debab0d4bfe7a4

まず、モック用のデータ消去します。

src/components/index.js
- const todoList = [
-   { id: 1, message: 'todo!', isComplete: false, },
-   { id: 2, message: 'complete todo!', isComplete: true, },
-   { id: 3, message: 'complete todo!', isComplete: true, },
- ];

ストアーを使うためには, injectobserver を使用します。
おまじないとして覚えてください。
こちらを参照

src/components/index.js
+ import { inject, observer } from 'mobx-react/native';

+ @inject('todo')
+ @observer
export default class Todo extends Component {
  render() {
+   const { todo } = this.props;
    return (
         <ScrollView style={{ flex: 1 }}>
-          {todoList.map(TodoCheckBox)}
+          {todo.list.map(TodoCheckBox)}
         </ScrollView>

下記を実行して見た目が変わらないことを確認してください。

$ react-native run-ios

storeに入れているmessageの値を変えてreloadし直して画面に表示されるtodoが変わることを確認してください。

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/859b5d2da2ea07fb508b4b21b0c0d1f78427e09d

アクションを作成する

テキストフォーム内の値を保持するアクションを作成します。

MobXの @action を使うことで実装できます。

src/stores/todo.js
-  import { observable } from 'mobx';
+  import { observable, action } from 'mobx';

+  @observable inputText = '';
+
+  @action onChangeText(message = '') {
+    this.inputText = message;
+  }

Componentに上記のアクションと保持した値を利用できるできるようにします。

src/components/index.js
+            <FormInput
+              autoCapitalize="none"
+              value={todo.inputText}
+              onChangeText={(message) => todo.onChangeText(message)}
+            />

ここまでの変更でテキストフォームに値を入力できれば成功です。
ほんとにできているか気になる方は下記のようにコンソールデバッグメッセージを仕込んで呼び出されていることを確認してください。

src/stores/todo.js
  @action.bound onChangeText(message = '') {
+   console.log('message:', message);
    this.inputText = message;
  }

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/6de06ed423f20eab7aa108f9f8a3bbb78f93a062

ストアをでテキストフォームの値を管理することができるようになったので管理している値でtodoを作成できるようにします。
ホントはサーバーサイド登録するべき、サンプルだから許される(魔法の言葉)

src/stores/todo.js
+  @action add() {
+    // 空文字の場合は登録しない
+    if (this.inputText === '') return;
+
+    this.list.push({
+      id: Math.random().toString(36).slice(-8),    // 雑にランダム文字列を生成
+      message: this.inputText,
+      isComplete: false,
+    });
+    this.inputText = '';
+  }

Componentに上記のアクションを利用できるできるようにします。

src/components/index.js
          <View style={{ flex: 5 }}>
            <FormInput
              autoCapitalize="none"
              value={todo.inputText}
              onChangeText={todo.onChangeText}
+             onSubmitEditing={() => todo.add()}
            />
          </View>
          <View style={{ flex: 1 }}>
            <Button
-             onPress={() => console.log('add')}
+             onPress={() => todo.add()}
              title="ADD"
            />
          </View>

ここまでの変更でテキストフォームにメッセージを入れてボタン押したタイミングでtodoが作成されれば成功です。
必要の亡くなったダミーデータを削除しましょう。

src/stores/todo.js
-  // TODO: とりあえずデータを入れておく
-  @observable list = [
-    { id: 1, message: 'todo!', isComplete: false, },
-    { id: 2, message: 'complete todo!', isComplete: true, },
-    { id: 3, message: 'complete todo!', isComplete: true, },
-  ];
+  @observable list = [];

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/2d901301496eec483065244b10278d0043dd7772

次にチェックボックスのトグルを実装します。リスト内部のオブジェクトのisCompleteを反転させることで実現できます。(もっと良い実装があったら教えてください)

src/stores/todo.js
+  // 指定したidのコンプリートフラグを反転する
+  @action toggleComplete(id) {
+    this.list = this.list.map(o => o.id === id ? { ...o, isComplete: !o.isComplete } : o);
+  }

Componentに上記のアクションを利用できるできるようにします。

src/components/index.js
-        onPress={() => console.log('todoトグル')}
+        onPress={() => todo.toggleComplete(row.id)}

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/65aed63e7608b736c8f101dc5a2f679e3a0fb2d6

最後にtodoを削除できるようにします。 Array.filter をつかって一致する id を消します。

src/stores/todo.js
+  @action del(id) {
+    this.list = this.list.filter(o => o.id !== id);
+  }

Componentに上記のアクションを利用できるできるようにします。

src/components/index.js
-              { text: '削除', onPress: () => console.log('todo削除') },
+              { text: '削除', onPress: () => todo.del(row.id) },

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/85d266205da7b68ea82bf6e3c3c7ebd83ea76a57

すべての機能が動くことを確認しましょう。

$ react-native run-ios

発展(時間が余った人用)

現状のアプリは閉じたらtodoが全部吹き飛びます。これを AsyncStorage 等に入れられるようにしてみましょう。生のAPIを利用するのは苦痛なのでいい感じにラップされている react-native-store を利用して見るといいかもしれません。

後ろが真っ白で見づらいので、backgroundColorを設定してみましょう。

todoが下に入っていくのに違和感を感じる人は最新の登録が上に来るようにしてみましょう。

最後に

ハンズオンはこれで終わりです。
Nativeの話よりReactとMobXの話が中心になってしまった感じはしますが、ネイティブが関係しないコードであればほぼweb開発の知識で開発を始めることができると感じたと思います。
まだメジャーバージョンが出ておらず辛いところもあります。そこは頑張って行きましょう!

togana
個人的なメモとして使うつもり
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away