【Android/Kotlin】Firestoreでリアルタイムなメッセージの送受信を実装してみた

# はじめに
執筆者は趣味レベルのAndroidアプリ開発者である為，説明間違いが含まれている可能性があります。ですので，お気づきの点がありましたら，是非コメントお願いいたします。また，アップデートにより，機能やコードが記事執筆時点と異なる場合があります。

また，本記事ではAndroidアプリ開発における基本的な部分に関する説明は省略していますので，ご了承下さい

#実装内容
Androidアプリにて，Firebaseを用いてリアルタイムなメッセージの送受信を実装します
（Kotlinで実装しています。簡単に実装してますので，細かい部分は適当です）
イメージとしては，簡易なメールアプリと言ったような感じになります

#実装の流れ
1. UIを作る
2. ログイン機能を実装 (端末ID識別のため，Firebase Authenticationを使用)
3. メッセージ送信を実装（送信メッセージをFirestoreに格納）
4. メッセージ受信を実装（受信メッセージが更新されたタイミングでFirestoreから取得）

#UIを作る

まずUIを作っていきます
特にデザインには拘らず，ログイン画面と，メッセージ送受信画面の2つを作成します
ログインが完了したら，自動的に送受信画面に遷移するといった流れにします。

ログイン画面には以下の要素を入れます

- ログインID入力ボックス
- パスワード入力ボックス
- ログインボタン


メッセージ送受信画面には以下の要素を入れます

- 自分のアドレス
- 送信先のアドレス入力ボックス
- 送信メッセージ入力ボックス
- 送信ボタン
- 受信メッセージ一覧リスト（RecyclerView）




ログイン画面作成の手間が省けるので，今回はテンプレートの「Login Activity」を使用します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/cdb9e085-ef31-4f57-f07e-48bb9804a79e.png)


実際に作った画面

ログイン画面：LoginActivity（テンプレートのまま）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/aae20a91-0808-a2fa-90d0-dec6875cb002.png)


メッセージ送受信画面：MessageActivity
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/da45646a-0bfb-b7cf-e839-9a647dd661d3.png)


```xml:activity_message.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MessageActivity">

    <TextView
        android:id="@+id/myId"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="未ログイン"
        android:layout_gravity="center_horizontal"
        android:gravity="center_horizontal"
        android:layout_marginTop="100dp" />

    <EditText
        android:id="@+id/destEmailAddrEdit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textEmailAddress"
        android:layout_marginHorizontal="50dp"
        android:layout_marginVertical="10dp"
        android:hint="送信先ID" />

    <EditText
        android:id="@+id/messageEdit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:gravity="start|top"
        android:inputType="textMultiLine"
        android:layout_gravity="center_horizontal"
        android:layout_marginHorizontal="50dp"
        android:layout_marginVertical="10dp"
        android:hint="送信メッセージ" />

    <Button
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="送信" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/messageInbox"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"
        android:layout_margin="30dp" />

</LinearLayout>

```



#ログイン機能を実装

続いてログイン機能を作っていきます
Firebase Authenticationを使います
既存テンプレートは色々と丁寧な実装がしてあり，Firebase Authenticationに適用させようとすると変更部分がかなり増えそうなので，とりあえずデモ実装ということで，最低限の変更で実装していきます

###Firebase Authentication有効化
Firebaseの設定や使い方に関する詳しい解説は今回は省略します
設定に関しては，メニューのToolsのFirebaseから設定すれば，数クリックで簡単に設定できます
準備として，Firebaseコンソールからプロジェクトを作成し，アプリと連携させ，コンソール内のFirebase Authenticationの設定で「メール/パスワード」の認証を有効化しましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/623f3f51-25e9-cab1-a8ba-aed0beae58ce.png)



###ログイン成功時の画面遷移
以下のように記述を変更し，成功時にメッセージ画面に遷移するように設定します

```kotlin:LoginActivity.kt

        loginViewModel.loginResult.observe(this@LoginActivity, Observer {
            val loginResult = it ?: return@Observer

            loading.visibility = View.GONE
            if (loginResult.error != null) {
                showLoginFailed(loginResult.error)
            }
            if (loginResult.success != null) {
                updateUiWithUser(loginResult.success)

                //画面繊維の追加
                val intent = Intent(this, MessageActivity::class.java)
                startActivity(intent)
                finish()
            }
        })
```

###Firebase Authenticationでサインアップの実装
かなり雑ですがViewModelにサインアップ・サインイン処理を書いてしまいます


```kotlin:LoginActivity.kt

    private lateinit var auth: FirebaseAuth

    fun login(username: String, password: String) {
        auth = FirebaseAuth.getInstance()

        //サインアップ
        auth.createUserWithEmailAndPassword(username, password)
            .addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    _loginResult.value =
                        LoginResult(success = LoggedInUserView(displayName = username))
                } else if (task.exception is FirebaseAuthUserCollisionException) {
                    //既にアカウントが登録済みの時にはサインイン
                    auth.signInWithEmailAndPassword(username, password)
                        .addOnCompleteListener { task ->
                            if (task.isSuccessful) {
                                _loginResult.value =
                                    LoginResult(success = LoggedInUserView(displayName = username))
                            } else {
                                _loginResult.value = LoginResult(error = R.string.login_failed)
                            }
                        }
                } else {
                    _loginResult.value = LoginResult(error = R.string.login_failed)
                    Log.w("Auth", "signInWithEmail:failure", task.exception)
                }
            }
    }
```

これで，とりあえず適当なメールアドレスとパスワード入れればメッセージ画面に移動します
（メールアドレスはちゃんとメールアドレスの形式でないとFirebaseで弾かれてエラーになります）


#メッセージ送信を実装
次はメッセージ送信を実装します
とはいっても，メッセージ送信は単にメッセージをFirestoreに格納するだけですが

###Firestore有効化
こちらも詳しい説明は省きます
こちらもToolbarのFirestoreから簡単に連携できます
FirebaseコンソールでFirestoreをテストモードで開始させておけば大丈夫です

###メッセージを送信
以下のように実装しています
送ったメッセージを，日付と送信者の名前付きで，相手のメッセージ受信ボックスに送っています
本当は送信者IDが正しいフォーマットであるか検証したり，送信者IDが存在しない場合をチェックする必要がありますが，今回は省略しています
ログアウトボタンも本来は欲しいところですね・・・
これでメッセージを送ってみると，Firestoreのデータベースにデータが追加されると思います
また，自分のメールアドレス（送信者ID）も送信画面に表示しています

```kotlin:MessageActivity.kt

class MessageActivity : AppCompatActivity() {

    private lateinit var myEmailAddr: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_message)

        myEmailAddr = FirebaseAuth.getInstance().currentUser?.email.toString()

        //自分のユーザー名を表示
        myId.setText(myEmailAddr)

        //送信ボタン押下時の設定
        send.setOnClickListener {
            sendMessage(destEmailAddrEdit.text.toString(), messageEdit.text.toString())
        }
    }


    //メッセージをDBに格納
    fun sendMessage(destEmailAddr: String, message: String) {
        val db = FirebaseFirestore.getInstance()

        // 現在時刻の取得
        val date = Date()
        val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")

        val mail = hashMapOf(
            "datetime" to format.format(date),
            "sender" to myEmailAddr,
            "message" to message
        )

        db.collection("messages")
            .document(destEmailAddr)
            .collection("inbox")
            .add(mail)
            .addOnSuccessListener {
                Toast.makeText(applicationContext, "送信完了！", Toast.LENGTH_LONG).show()
                messageEdit.text.clear()
            }
            .addOnFailureListener { e ->
                Log.w("Firestore", "Error writing document", e)
            }
    }
}

```

#メッセージ受信を実装

次は，メッセージ受信機能です。
自分の受信ボックスにメッセージが追加されたタイミングで，
送信ボタンの下に設置してあるRecyclerViewにメッセージを追加していくことが目標です

###受信ボックスのメッセージをRecyclerViewに適用

まずはRecyclerViewのレイアウトを作ります

```xml:mail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        tools:ignore="UselessParent">

        <TextView
            android:id="@+id/sender"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="from:"
            android:textColor="@android:color/holo_red_light"
            android:textAppearance="@style/TextAppearance.AppCompat.Small" />

        <TextView
            android:id="@+id/message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="message"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium" />

    </LinearLayout>
</LinearLayout>
```

こんな感じのシンプルなレイアウトです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/12955b2b-3e54-38a2-0316-9cb5c602773d.png)

RecyclerViewのAdapterも作っていきましょう

```kotlin:MyAdapter.kt

class MyAdapter(private val myDataset: ArrayList<List<String?>>) :

    RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val sender: TextView = view.sender
        val message: TextView = view.message
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val inflatedView = LayoutInflater.from(parent.context)
            .inflate(R.layout.mail, parent, false)
        return MyViewHolder(inflatedView)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.message.text = myDataset[position][0]
        holder.sender.text = "from: " + myDataset[position][1]
    }

    override fun getItemCount() = myDataset.size
}

```

続いて，メッセージ一覧をRecyclerViewに適用します

```kotlin:MessageActivity.kt

    private lateinit var myEmailAddr: String
    private lateinit var recyclerView: RecyclerView
    private lateinit var viewAdapter: RecyclerView.Adapter<*>
    private lateinit var viewManager: RecyclerView.LayoutManager
    private lateinit var db: FirebaseFirestore

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_message)

        //自分のユーザー名を表示
        myEmailAddr = FirebaseAuth.getInstance().currentUser?.email.toString()
        myId.setText(myEmailAddr)

        //　受信ボックスのメッセージを取得してrecyclerViewに適用
        db = FirebaseFirestore.getInstance()
        val allMessages = ArrayList<List<String?>>()
        db.collection("messages")
            .document(myEmailAddr)
            .collection("inbox")
            .orderBy("datetime", Query.Direction.DESCENDING)
            .get()
            .addOnSuccessListener { result ->
                for (document in result) {
                    val message = document.getString("message")
                    val sender = document.getString("sender")
                    allMessages.add(listOf(message, sender))
                }

                viewManager = LinearLayoutManager(this)
                viewAdapter = MyAdapter(allMessages)
                recyclerView = messageInbox.apply {
                    setHasFixedSize(true)
                    layoutManager = viewManager
                    adapter = viewAdapter
                }
            }

        //送信ボタン押下時の設定
        send.setOnClickListener {
            sendMessage(destEmailAddrEdit.text.toString(), messageEdit.text.toString())
        }
    }
```

これで，自分宛のメッセージ一覧が送信ボタンの下に表示されると思います
現在，特に自分宛てのメッセージ送信は制限していないため，自分のアドレスにメッセージを送ってみてください
メッセージ一覧が表示されるかと思います
（リアルタイムな反映はまだ実装していないので，表示するにはアプリを開き直す必要があります）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260207/43bb483b-21a4-4fa7-db4b-ba73868b52ba.png)


###受信ボックスのメッセージをRecyclerViewに適用

さて，次はメッセージを受け取ったらメッセージ一覧を更新していきます
更新通知時の処理については[こちら](https://firebase.google.com/docs/firestore/query-data/listen?hl=ja#events-metadata-changes)に記載されている内容を参考にしています

最終的にMessageActivity.ktは以下のようになっています
今回変更したのはonCreate中です
firestoreの更新通知に関しては`.addSnapshotListener`を使用しています
この中に書いた処理がデータベース更新時に自動で処理されます
`.addSnapshotListener`を設定した時点で一度中の処理が呼び出されるため，既に記述していたメッセージ取得＆表示処理は`.addSnapshotListener`に置き換えました
また，RecyclerViewに紐付いているallMessagesの更新を表示に反映するためには`notifyDataSetChanged()`を使用します

```kotlin:MessageActivity.kt
class MessageActivity : AppCompatActivity() {

    private lateinit var myEmailAddr: String
    private lateinit var recyclerView: RecyclerView
    private lateinit var viewAdapter: RecyclerView.Adapter<*>
    private lateinit var viewManager: RecyclerView.LayoutManager
    private lateinit var db: FirebaseFirestore

    private var allMessages = ArrayList<List<String?>>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_message)

        db = FirebaseFirestore.getInstance()

        //自分のユーザー名を表示
        myEmailAddr = FirebaseAuth.getInstance().currentUser?.email.toString()
        myId.setText(myEmailAddr)

        viewManager = LinearLayoutManager(this)
        viewAdapter = MyAdapter(allMessages)
        recyclerView = messageInbox.apply {
            setHasFixedSize(true)
            layoutManager = viewManager
            adapter = viewAdapter
        }

        //　Firestore更新時の操作の登録
        db.collection("messages")
            .document(myEmailAddr)
            .collection("inbox")
            .orderBy("datetime", Query.Direction.DESCENDING)
            // Firestoreの更新時の操作を登録
            .addSnapshotListener { value, e ->
                if (e != null) {
                    Log.w("Firestore", "Listen failed.", e)
                    return@addSnapshotListener
                }

                allMessages.clear()
                for (doc in value!!) {
                    val message = doc.getString("message")
                    val sender = doc.getString("sender")
                    allMessages.add(listOf(message, sender))
                }
                
                // RecyclerViewの更新
                viewAdapter.notifyDataSetChanged()
            }


        //送信ボタン押下時の設定
        send.setOnClickListener {
            sendMessage(destEmailAddrEdit.text.toString(), messageEdit.text.toString())
        }
    }


    //メッセージをDBに格納
    fun sendMessage(destEmailAddr: String, message: String) {
        // 現在時刻の取得
        val date = Date()
        val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")

        val mail = hashMapOf(
            "datetime" to format.format(date),
            "sender" to myEmailAddr,
            "message" to message
        )

        db.collection("messages")
            .document(destEmailAddr)
            .collection("inbox")
            .add(mail)
            .addOnSuccessListener {
                Toast.makeText(applicationContext, "送信完了！", Toast.LENGTH_LONG).show()
                messageEdit.text.clear()
            }
            .addOnFailureListener { e ->
                Log.w("Firestore", "Error writing document", e)
            }
    }
}
```

これで，リアルタイムなメッセージの反映が完了しました。
自分宛てにメッセージを送ってみましょう。
すぐにメッセージ一覧に反映されるかと思います
もちろん，複数端末間でもメッセージの送受信は可能です


#最後に

一通り書いてみましたが，ミスがある場合は是非コメントお願いします
また，アプリ開発初心者ですので，実装方法が雑であったり間違っている場合は多々あるかと思います
ご指摘ありましたらぜひコメントお願いいたします

本来であれば，追加で

- 入力値の検証
- 受信メッセージのキャッシュ
- 更新時に全メールを取得するのではなく，端末にキャッシュした最新のメッセージより新しいものだけを取得
- バックグランドでのメッセージ受信
- メッセージ受信時の通知

といったことをやるべきかと思いますが，長くなりそうでしたので今回は割愛しました。