これまでの連載でFirebaseのリアルタイムデータベースの基本的な使い方と、
今回は連載の締めくくりとして、
「わいわいチャット」アプリの説明
まずこれから説明するアプリについておさらいしておきます。
わいわいチャットはシンプルなリアルタイムチャットアプリケーションです。起動するとTwitterアカウントによる認証画面が表示され、



プロジェクトの準備
ソースコードはGitHubに公開しました。
- FirebaseRealTimeChat : Sample real-time chat application using Firebase
- URL:https://
github. com/ srym/ FirebaseRealTimeChat
Apache 2.
プロジェクトのclone
さっそくアプリをビルドしていきましょう。プロジェクトを任意のディレクトリにcloneしてください。
$ git clone [email protected]:srym/FirebaseRealTimeChat.git
本記事連載時点のソースコードはstable
というブランチ名で残しておきますので、stable
ブランチに切り替えて作業します。
$ git checkout stable
Firebaseプロジェクトにアプリを追加
以前の連載記事を参考に、

このとき、us.
とする必要があります。
完了するとgoogle-services.
がダウンロードされるので、./
直下にコピーしてください。
Twitter認証の有効化
わいわいチャットではTwitter認証を利用します。FirebaseのWebコンソールから

APIキーとAPIシークレットはこの後のステップで取得するのでいったん空欄のままにしておき、
Twitterアプリケーションの作成
Twitter認証で利用するAPIキーとAPIシークレットを取得するためにTwitterアプリケーションを新規作成します。https://
「Name」

入力が完了したら下にスクロールし、

作成が完了したら

APIキーとシークレットを、
保存が完了したらWebコンソールでTwitter認証が次のように有効になっていることを確認してください。

ビルドして動作確認
FirebaseのWebコンソールから

次に先ほどcloneしたレポジトリを開き、settings.
というファイルを任意のテキストエディタで開き、
firebase_project_id=your-project-id
twitter_key=your-twitter-api-key
twitter_secret=your-twitter-api-secret
以上で準備が整いました。Android Studioでcloneしたプロジェクトを開いてビルドし、

何かエラーが起きた場合は、
google-services.
を正しくjson ./
以下にコピーできているかapp settings.
の中身を正しく記述できているかproperties - https://
apps. のCallback URLの設定はFirebaseのWebコンソールと合っているかtwitter. com/ - FirebaseのWebコンソールでTwitter認証が正しく有効になっているか
ソースコードの解説
それではソースコードの中から重要な部分を抜粋して解説したいと思います。いま一度stable
ブランチに切り替えられていることを確認してください。
$ git checkout stable
ログイン処理
本アプリケーションのエントリポイントはLoginActivity
です。実際のログイン処理はLoginHelper
のonCreate()
メソッド内に書かれています。次のコードをご覧ください。
// LoginHelper.java
twitterLoginButton.setCallback(new Callback<TwitterSession>() {
@Override
public void success(Result<TwitterSession> result) {
TwitterSession session = result.data;
TwitterAuthToken token = session.getAuthToken();
AuthCredential credential = TwitterAuthProvider.getCredential(token.token, token.secret);
firebaseAuth.signInWithCredential(credential)
.addOnSuccessListener(authResult -> {
FirebaseUser firebaseUser = authResult.getUser();
String uid = firebaseUser.getUid();
UserInfo twitterUser = firebaseUser.getProviderData().get(1);
String name = twitterUser.getDisplayName();
String thumbnail = twitterUser.getPhotoUrl() != null ? twitterUser.getPhotoUrl().toString() : null;
// 中略
User user = new User(name, thumbnail);
databaseReference
.child(User.PATH)
.child(uid)
.setValue(user)
.addOnSuccessListener(userCreation -> {})
.addOnFailureListener(error -> {});
})
.addOnFailureListener(e -> {});
}
@Override
public void failure(TwitterException exception) { }
});
Twitterの認証処理はTwitter社が公式に提供するFabric SDKのTwitterLoginButton
を利用しています。
TwitterLoginButton
を使ったログインが成功するとsuccess(Result<TwitterSession> result)
に処理が渡り、TwitterAuthProvider.
でFirebaseAuthで利用するAuthCredential
を取り出しています。
次にfirebaseAuth.
で、AuthCredential
を使ってFirebaseAuthでログインしています。
無事ログインできると、authResult
経由でFirebase内で一意なユーザ識別子であるuid
を取り出したり、
最終的にUser
エンティティに必要な情報を詰めてリアルタイムデータベースに格納しています。格納した情報はチャットのユーザ情報として利用します。
FirebaseAuthに関しては第5回の連載でパスワード認証について解説しており、
メッセージ受信時のテクニック
ログインが完了するとアプリはチャット画面に遷移します。チャット画面はChatActivity
が担当します。処理自体はMessageHelper
に大部分が移譲されています。
第3回等で解説したように、ValueEventListener
とChildEventListener
の2種類のリスナが利用できるのですが、
ValueEventListenerの使いどころ
ValueEventListener
はあるパス以下のデータを一気に取得するためのリスナです。本アプリでは、ValueEventListener
を利用しています。
- 初回アクセス時
- Pull To Refresh
(引っ張り更新) 時
MessageHelper
の次のコードをご覧ください。
private ValueEventListener singleShotListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
// 一気に取得したメッセージをリストに詰める
}
/* 中略 */
// 更新ダイアログを非表示
swipeRefreshLayout.setRefreshing(false);
// この部分は後述
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).startAt(lastTimestamp + 1).addChildEventListener(childAddListener);
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).addChildEventListener(childRemoveListener);
}
@Override
public void onCancelled(DatabaseError databaseError) { }
};
前述のように、ChildEventListener
で要素の個数分毎回コールバックが発生するよりも一気に取得したほうが効率的です。また、swipeRefreshLayout.
のように更新ステータスの表示/
本アプリではこのリスナをaddListenerForSingleValueEvent()
に渡すことで、addChildEventListener(childAddListener)
に処理をバトンタッチしています。詳しくは次節で解説します。
ChildEventListenerの使いどころ
ChildEventListener
はあるパスにデータが追加・ValueEventListener
が初回データを受信して以降は、ChildEventListener
で受信しています。MessageHelper
の次のコードをご覧ください。
private ChildEventListener childAddListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Message message = getMessageWithId(dataSnapshot);
messages.add(message);
chatAdapter.notifyDataSetChanged();
recyclerView.scrollToPosition(chatAdapter.getItemCount() - 1);
}
/* 以下略 */
};
これで、
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).startAt(lastTimestamp + 1).addChildEventListener(childAddListener);
startAt(lastTimestamp + 1)
としている部分に注目してください。ここはValueEventListener
で一気にメッセージを取得した際に、
こうしないと、addChildEventListener(childAddListener)
したタイミングで、
2つのChildEventListener
もう一点ポイントがあります。MessageHelper
では次のようにChildEventListener
を2つ利用しています。これはなぜなのでしょうか。
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).startAt(lastTimestamp + 1).addChildEventListener(childAddListener);
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).addChildEventListener(childRemoveListener);
これはメッセージの削除処理と関係しています。
前節で解説したように、ValueEventListener
でメッセージを一括取得し、ChildEventListener
で追加分のメッセージのみ受信していますが、
もし受信時のようにstartAt(lastTimestamp + 1)
と指定してリスナを登録してしまうと、startAt()
を指定せずに登録する必要があるのです。
過去のメッセージの取得テクニック
本アプリでは、
追加取得のロジックはMessageHelper#onCreate
内で実装しています。次のコードをご覧ください。
onScrollListener = new ScrollEdgeListener((LinearLayoutManager) recyclerView.getLayoutManager()) {
@Override
public void onTop() {
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).endAt(firstTimestamp - 1).limitToLast(LIMIT).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// 過去のメッセージを現在のメッセージリストにマージ
}
@Override
public void onCancelled(DatabaseError databaseError) { }
});
}
};
recyclerView.addOnScrollListener(onScrollListener);
リストの最上部に到達したことはScrollEdgeListener#onTop()
が通知してくれるので、
databaseReference.child(Message.PATH).orderByChild(Message.KEY_TIMESTAMP).endAt(firstTimestamp - 1).limitToLast(LIMIT).addListenerForSingleValueEvent()
過去のメッセージを遡って取得するので、
メッセージ送信時のテクニック
本アプリは単純なテキストメッセージの送信の他、

これらの機能を実装する際に利用したテクニックをご紹介したいと思います。
タイムスタンプの付与
メッセージの送信はMessageHelper#send()
で行っています。送信と言っても事実上Firebaseのリアルタイムデータベースにメッセージを保存しているだけです。
保存メッセージにタイムスタンプを付与するのはよくある処理だと思いますが、
まずはメッセージを表現するMessage
クラスを見てみましょう。
public class Message {
/* 中略 */
private String body;
@Exclude
private long timestamp;
/* 中略 */
public Message() {
}
public long getTimestamp() {
return timestamp;
}
}
これまでの連載で、setValue(object)
すればフィールドに対応する値が保存されることを見てきました。今回はタイムスタンプをFirebaseのサーバ上の値にするので、Message
クラスのtimestamp
フィールドを@Exclude
アノテーションで修飾します。
次に保存するコードをご覧ください。
Message message = new Message(MessageType.NORMAL.ordinal(), loginInfo.getUid(), body);
DatabaseReference newMessage = databaseReference.child(Message.PATH).push();
newMessage
.setValue(message)
.addOnSuccessListener(result -> {
newMessage
.updateChildren(new HashMap<String, Object>(1) {{
put(Message.KEY_TIMESTAMP, ServerValue.TIMESTAMP);
}})
.addOnSuccessListener(command -> {})
.addOnFailureListener(error -> {});
})
.addOnFailureListener(error -> {});
Message
オブジェクトにはtimestamp
を設定せずそのままsetValue(message)
で保存します。次にその成功コールバックの中でupdateChildren()
メソッドを呼び出してやり、timestamp
、ServerValue.
のHashMap
を渡してタイムスタンプのフィールドを更新しています。
こうすることで、
画像アップロードのテクニック
画像のアップロードには第8回で解説したFirebase Storageを利用しています。
アプリのメッセージ入力欄の左側にあるアイコンをクリックすると端末からファイルを選択する画面が現れ、StorageHelper#uploadImage()
が呼び出されて画像がアップロードされます。
public void uploadImage(String filePath) {
String fileName = UUID.randomUUID().toString();
StorageReference target = storageReference
.child(IMG_DIR)
.child(fileName);
try {
InputStream inputStream = new BufferedInputStream(new FileInputStream(filePath));
target.putStream(inputStream)
.addOnSuccessListener(taskSnapshot -> {
// アップロード完了を通知
})
.addOnFailureListener(e -> Log.e(TAG, e.getMessage(), e));
} catch (FileNotFoundException e) { }
}
端末ローカルからアップロードする画像パスを取得し、
アップロードが完了したらMessageHelper#onImageUploadSuccess()
内で、
メッセージ削除のテクニック
前述のとおり、
ロングタップを検出するコードはChatAdapter#onCreateViewHolder()
内にあります。
@Override
public ChatViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ChatViewHolder viewHolder;
/* 中略 */
viewHolder.itemView.setOnLongClickListener(view -> {
int position = viewHolder.getAdapterPosition();
bus.post(new OnLongClickEvent(position));
return true;
});
return viewHolder;
}
削除を確認するダイアログはMessageDeleteDialogFragment
で表示しており、MessageHelper#onItemeleteYes()
内で削除処理を行っています。
public void onItemDeleteYes(int position) {
Message message = messages.get(position);
databaseReference
.child(Message.PATH)
.child(message.getMessageId())
.removeValue()
.addOnSuccessListener(result -> {
if (message.isTypeImage()) {
bus.post(new ImageDeleteRequestEvent(message.getFileName()));
}
})
.addOnFailureListener(error -> handleError(error, R.string.message_remove_error));
当該positionのメッセージをremoveValue()
で削除した後、
画像の削除処理はStorageHelper#deleteImage()
で行っています。処理内容は第8回の連載がそのまま参考にできるので適宜ご参照ください。
その他のテクニック
以上で、
Remote Config
本アプリでは連載第8回で解説したRemote Configを使い、
具体的には、LoginActivity
を表示している時にRemoteConfigHelper#fetch()
メソッドがバックグラウンドでサーバ設定の取得を試みます。
第8回の記事を参考にしながら、chat_
、#FFE4E1
のような16進数のカラーコードを指定してみてください。

アプリを再起動して新しい背景色でチャット画面が表示されれば成功です。
取得できた値はChatActivity#onCreate()
内のremoteConfigHelper.
で背景色として設定されます。サーバで値を未設定の場合やネットワーク接続がない場合は、remote_
から初期値が使われます。
セキュリティルールとインデックス
最後の仕上げとして、
今回のアプリでは、
また、
セキュリティルールとインデックスはどちらもFirebaseのデータベースのWebコンソールの
{
"rules": {
".read": false,
".write": false,
"users": {
".read": "auth != null",
"$user_id": {
".write": "auth != null && auth.uid === $user_id"
}
},
"messages": {
".read": "auth != null",
".indexOn": ["timestamp"],
"$message_id": {
".write": "(auth != null && auth.uid === newData.child('senderUid').val()) || (auth != null && auth.uid === data.child('senderUid').val())"
}
}
},
}
まずはユーザ一覧に関する設定は次の部分です。
"users": {
".read": "auth != null",
"$user_id": {
".write": "auth != null && auth.uid === $user_id"
}
},
メッセージ一覧で各ユーザのサムネイルを表示する必要があるため、".write": "auth != null && auth.
として本人以外の操作を制限しています。
次にメッセージ一覧に関する設定は次の部分です。
"messages": {
".read": "auth != null",
".indexOn": ["timestamp"],
"$message_id": {
".write": "(auth != null && auth.uid === newData.child('senderUid').val()) || (auth != null && auth.uid === data.child('senderUid').val())"
}
}
こちらも同様で、
書き込みと削除に関しては少しだけ複雑です。まず新規書き込みに関する設定は(auth != null && auth.
の部分です。これから書き込まれるデータはnewData
変数で参照できるので、
削除に関する設定は(auth != null && auth.
の部分です。すでに存在するデータはdata
変数で参照できるので、
最後にインデックスの設定です。メッセージ一覧はどれもtimestamp
でソートを行うため、".indexOn": ["timestamp"]
でインデックスを作成して読み出しを高速化しています。
セキュリティルールとインデックスに関しては、
これでプロダクションリリースとして公開できる設定が完了しました!
まとめ
以上で約半年間に渡ったFirebaseの連載はひとまず完了です。いかがだったでしょうか?
当初はリアルタイムデータベースと認証機能、
顧客のニーズが目まぐるしく変わり、