Firebase
reactnative
Firestore
CloudFirestore
FirebaseDay 20

React NativeとCloud Firestoreで(5分では無理だけど)チャットアプリを作る

はじめに

この記事について

Firebase Advent Calendar 2017 20日目の記事です。

昨日は @daikiojm さんの Firebase Realtime DatabaseとAngularでフォロー/フォロワー機能の構築をしてみた でした。
Realtime DatabaseやCloud Firestoreのスキーマ設計は、RDBとは違った考え方をしなければ行けないにも関わらず日本語の情報が十分でないように感じますので、少しでも知見が溜まっていのは有り難いですね!

本記事は @imaimiami さんの new FirebaseとReact NativeでiOS, Android向けチャットを5分で作る でRealtime Databaseを使っていたものを、Cloud Firestoreでやってみようという位置づけになります。
(以下、省略してFirestoreと記載します。)

5分では無理な理由

「5分で」の記事では firebase モジュールを使っていたのですが、本記事では react-native-firebase を導入するためです。

環境

package.json
{
  ...
  "dependencies": {
    "react": "16.0.0",
    "react-native": "0.51.0",
    "react-native-firebase": "^3.2.0",
    "react-native-gifted-chat": "^0.3.0"
  },
}

で確認済みです。

firebase vs react-native-firebase

Advent Calendar 2日目の @k2wanko さんの記事 React Native Firebaseについて #firebase #fjug でも触れられていますが、React NativeからFirebaseを使う際は、

のどちらかのnpmモジュールを導入することになります。

ざっくりと、 firebase はJavaScript上のみで動くのに対し、 react-native-firebase はiOSおよびAndroidネイティブのSDKをReact Native用にラップしたものです。

READMEに対応表がありますが、 firebase からのFirestoreの使用はpartial supportとなっています。(2017年12月時点)
Firestoreがベータ版で登場した10月ころは、 react-native-firebase のみが対応となっていました。このようにバージョンアップ対応が早いのと、パフォーマンス面でも優れているらしいので、React NativeからFirebaseを使うときは、基本的に react-native-firebase を導入するといいでしょう。

Firebaseプロジェクトの設定

Firebaseプロジェクト作成

Firebaseコンソールからプロジェクトを追加します。
プロジェクト名などは適当に入力します。

iOSおよびAndroidアプリの登録

ダッシュボードから「iOSアプリにFirebaseを追加」もしくは「AndroidアプリにFirebaseを追加」をクリックし、アプリを登録します。
このとき、iOSの場合はバンドルID、Androidの場合はパッケージ名を入力します。
ステップ2以降はReact Nativeプロジェクト作成後に行うので、今はアプリの登録だけで大丈夫です。

Firestoreの有効化

「Database」メニューから、「Firestoreベータ版を試してみる」をクリックします。
今回は簡単に試したいので、セキュリティルールは「テストモードで開始」を選択します。
firestore-chat_–_Database_–_Firebase_console.png

React Nativeの初期設定

プロジェクトのinit

react-native-firebase はネイティブのSDKを使うため、Expo(create-react-native-app)は使えません。

$ react-native init [APP_NAME]
$ cd [APP_NAME]

react-native-firebaseの依存追加

以下のドキュメントに沿ってreact-native-firebaseモジュールを追加し、iOSおよびAndroidプロジェクトにFirebase SDKを追加していきます。
手順が多いので割愛します:bow:

このとき、Firebaseへの依存関係として、iOSのPodfileには

pod 'Firebase/Core'
pod 'Firebase/Firestore'

を、Androidのbuild.gradleには

compile "com.google.firebase:firebase-core:11.4.2"
compile "com.google.firebase:firebase-firestore:11.4.2"

を追加しておきます。
また、プロジェクト作成時のiOSのバンドルIDやAndroidのパッケージ名は適当なものになっているので、Firebaseプロジェクトで指定したものと一致するように変更が必要です。

react-native-gifted-chatの依存追加

react-native-gifted-chatを使うことでチャットUIがすぐに利用できます!

$ yarn add react-native-gifted-chat

React Nativeコードの実装

基本のチャット画面作成

いよいよJSXを触っていきます。
react-native-gifted-chatのExampleを参考に、App.jsに renderonSend を実装していきます。1

App.js
import React, { Component } from 'react';
import { GiftedChat } from 'react-native-gifted-chat';

export default class App extends Component {
  state = {
    messages: [],
  };

  /**
   * Sendボタンがタップされたときのイベント
   */
  onSend = (messages = []) => {
    // massagesをstateに渡す
    this.setState((previousState) => ({
      messages: GiftedChat.append(previousState.messages, messages),
    }));
  }

  render() {
    return (
      <GiftedChat
        messages={this.state.messages}
        onSend={this.onSend}
        user={{
          _id: 1,
          name: 'John Doe'
        }}
      />
    );
  }
}

これだけのコードでチャット画面が表示されます。

Firestoreとの連携を追加

今の状態ではアプリを閉じるとチャットが消えてしまいますので、messagesをFirestoreに保存するように変更していきます。

App.js
import React, { Component } from 'react';
import { GiftedChat } from 'react-native-gifted-chat';
import firebase from 'react-native-firebase';

export default class App extends Component {
  state = {
    messages: [],
  };

  componentDidMount() {
    // Firestoreの「messages」コレクションを参照
    this.ref = firebase.firestore().collection('messages');

    // refの更新時イベントにonCollectionUpdate登録
    this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate);
  }

  componentWillunmount() {
    // onCollectionUpdateの登録解除
    this.unsubscribe();
  }

  /**
   * Sendボタンがタップされたときのイベント
   */
  onSend = (messages = []) => {
    // Firestoreのコレクションに追加
    messages.forEach((message) => {
      this.ref.add(message);
    });

    // onCollectionUpdateが呼ばれるので、ここではstateには渡さない
    //this.setState((previousState) => ({
    //  messages: GiftedChat.append(previousState.messages, messages),
    //}));
  }

  /**
   * Firestoreのコレクションが更新されたときのイベント
   */
  onCollectionUpdate = (querySnapshot) => {
    // docsのdataをmessagesとして取得
    const messages = querySnapshot.docs.map((doc) => {
      return doc.data();
    });

    // messagesをstateに渡す
    this.setState({ messages });
  }

  render() {
    return (
      <GiftedChat
        messages={this.state.messages}
        onSend={this.onSend}
        user={{
          _id: 1,
          name: 'John Doe'
        }}
      />
    );
  }
}

Firebaseコンソールで確認する

ここまでうまくできていれば、アプリ側で入力したメッセージがFirestoreに保存され、他の端末からも見れるようになっているはずです。

「すべてのドキュメントを削除」したときのおおおお!という感じを味わっていただきたいです。2
firestore-15-32-18-3.gif

この先の実装方針

このままではドキュメントのIDがランダムで設定されるため、メッセージが正しい順番で表示されません。(上のスクリーンショットでも発生しています)
IDの頭にタイムスタンプをつけて時系列順になるようにするとコンソール上でも順番が一致するので、そのようにするのがよさそうです。

また、実際のチャットアプリでは、ユーザーの情報があったり複数のチャットルームが存在したりするので、それに応じてスキーマやセキュリティルールも変わってくると思います。

さいごに

先日、 Firebase Japan User Group の第1回meetupが開催されました!
Firebaseを使っていたり興味があるけど話せる相手が少ないという方は、ぜひSlackやmeetupにご参加ください。

logo.png


  1. Handling Events - React を参考に、public class field syntaxを使っています 

  2. この画面では conversations -> test_chat -> messages というスキーマになっていて、掲載しているApp.jsとは若干表示が異なります