Edited at
XamarinDay 5

Xamarin.FormsでのFirebase利用が捗る5つのライブラリを作ってみた

この記事は、Xamarin Advent Calendar 2018の5日目です。

Firebaseすごく便利です。データベース、ストレージ、認証などなど、バックエンドで必要な機能が簡単に使えてしまいます。Xamarinでもライブラリが用意されており、利用することが可能です。ただ、そのライブラリというのが、iOS用とAndroid用に分かれており、Xamarin.Formsで使うには、DependencyServiceを使うなどしなければならず、ちょっと面倒でした。そこで、Xamarin.Formsでそのまま使えるようなライブラリを作成しました。


作成したライブラリ

Firebaseの5つの機能のライブラリを作成しました。NuGetで公開済みなのですぐに利用できます。

使い方は、それぞれのGitHubをみてください。(詳しい機能の説明はしていないので、公式のドキュメントも参照してください)

NuGet

GitHub


Plugin.CloudFirestore

Cloud Firestoreを利用するためのライブラリです。

データベースから取得した時のデータの型は、当然iOSとAndroidでは異なるので、共通の型にマッピングする機能を備えています。また、リアルタイムでのデータの更新は、Rxが利用できるようにしています。


Plugin.FirebaseAuth

Authenticationを利用するためのライブラリです。

Plugin.CloudFirestoreやPlugin.FirebaseStorageとは、セットで使うことになります。

また、各プロバイダーへの認証の機能はないので、Xamarin.Auhtなどを使ってください。


Plugin.FirebaseStorage

Storageを利用するためのライブラリです。

進捗のモニタリングやキャンセル、一時停止にも対応しています。


Plugin.FirebaseAnalytics

アナリティクスを利用するためのライブラリです。


Plugin.FirebaseCrashlytics

Crashlyticsを利用するためのライブラリです。

昨日の記事にも書かれてあるように、通常色々やらないといけないのをこれを入れるだけで大丈夫なようにもしています。


サンプルアプリ

これらのライブラリを利用したサンプルアプリも作成しました。画像を投稿して、リストに表示されて、いいねができる、ちょっとしたSNSアプリです。

メールアドレスとGoogleでのログインができるようにしています。

リストは、リアルタイム監視をしているので、誰かが投稿やいいねをすると自動で表示が更新されます。

f-miyu/XamarinFirebaseSample


サンプルアプリを動かすには

サンプルアプリのGitHubには、GoogleService-Info.plistgoogle-services.jsonを含めていないので、動かすには各自でFirebaseの設定を行なってください。

以下で設定の仕方を説明します。


Firebaseプロジェクトの作成

Firebaseのコンソールで、[プロジェクトの追加]をクリックします。

プロジェクトの追加画面で、プロジェクト名を入力し、アナリティクスの地域を日本に設定、同意に

チェックをして、[プロジェクトを作成]をクリックします。

トップの画面の、[iOS]ボタンをクリックします。

iOSのバンドルIDを入力して、[アプリの登録]をクリックします。

[GoogleService-Info.plistをダウンロード]をクリックします。

GoogleService-Info.plistがダウンロードできたら、XamarinFirebaseSample.iOSプロジェクトのルートに追加します。また、ビルドアクションがBundleResourceになっていることを確認してください。

次にAndroidの登録を行います。[アプリを追加]をクリックして、Androidを選択します。

Androidのパッケージ名を入力します。また、サンプルアプリでは、Googleログインを行なっているので、SHA-1の入力も必要です。

Xamarin.Androidで使用する、debug.keystoreのSHA-1フィンガープリントを取得するには、ここを参考にしてください。

入力したら、[次へ]をクリックします。

[google-services.jsonをダウンロード]をクリックします。

google-services.jsonがダウンロードできたら、XamarinFirebaseSample.Androidプロジェクトのルートに追加します。また、ビルドアクションがGoogleServicesJsonになっていることを確認してください。


Cloud Firestoreの設定

[Database]セクションで、[データベースの作成]をクリックします。

[有効にする]をクリックします。


セキュリティルールの変更

認証しなければ、読み込みも書き込みもできないようにします。

[ルール]タブで、以下のように記述して、[公開]をクリックします。

service cloud.firestore {

match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null;
}
}
}


複合インデックスの作成

サンプルアプリでは、自分の投稿を新しい順に取得しているため、複合インデックスの作成が必要です。

[インデックス]タブで、[手動でインデックスを作成]をクリックします。

複合インデックスの作成画面で以下のように設定して、[インデックスの作成]をクリックします。

対象となるコレクション

items

フィールド

順番
フィールドのパス
モード

1
OwnerId
Ascending

2
Timestamp
Descending

今回は、手動で設定しましたが、まだ設定していない時に、複合インデックスが必要なクエリを行なったら、以下のような例外が発生します。そこのメッセージにあるアドレスを叩くことでも設定ができます。


Authenticationの設定

[Authentication]セクションで、[ログイン方法]タブを選択します。

[メール/パスワード]を有効にして、[保存]をクリックします。

[Google]を有効にし、メールアドレスを選択して、[保存]をクリックします。


Storageの設定

[Storage]セクションで、[スタートガイド]をクリックします。

セキュリティルールが表示されるので[OK]をクリックします。


Crashlyticsの設定

[Crashlytics]セクションで、[Crashlytics を設定]をクリックします。

[このアプリではCrashlyticsを初めて使用します]を選択して、[次へ]をクリックします。

この設定は、iOS、Androidのアプリごとに行う必要があります。


プログラム側の設定


AppConstans

XamarinFirebaseSampleプロジェクトのHelpersフォルダのAppConstants.csを修正します。

ここには、Googleログインで使用するクライアントIDを設定します。


AppConstants.cs

public static class AppConstans

{
public const string IosGoogleClientId = "";
public const string IosReversedGoogleClientId = "";

public const string AndroidGoogleClientId = "";
public const string AndroidReversedGoogleClientId = "";
}



iOSの設定

iOSは、先ほどダウンロードしたGoogleService-Info.plistで確認できます。

IosGoogleClientIdは、CLIENT_IDキーの値を、IosReversedGoogleClientIdには、REVERSED_CLIENT_IDの値を設定します。


Androidの設定

Androidは、APIコンソールの認証情報ページで確認できます。OAuth 2.0 クライアント IDにあるタイプがAndroidであるクライアントIDをコピーして、AndroidGoogleClientIdに設定します。AndroidReversedGoogleClientIdには、その逆ドメインを設定します。


iOSのURLスキームの設定

iOSプロジェクトのinfo.pistを開き、[詳細設定]画面にします。

[URLの種類を追加]をクリックして、[URL Schenes:]に先ほども利用したGoogleService-Info.plistREVERSED_CLIENT_IDの値を設定します。

これで全ての設定が完了です。ビルドして動かしてみてください。


解説

サンプルアプリについて少し解説を行います。


Services

データの取得やログイン処理などのロジックは、Servicesフォルダ内のクラスで行なっています。ライブラリの使い方は、これらのクラスを見てもらえればいいです。

以下がその一覧です。

クラス
機能
利用Firebase
備考

AccountService
アカウントの管理
CloudFirestore, Auth
ユーザーデータの監視も行なっています

AuthService
認証
なし
Xamarin.Authを利用した認証処理を行なっています

ContributionService
アイテムの投稿
CloudFirestore
StorageServiceを利用して画像アップロードをしています

EmailLoginService
メールアドレスでのログイン
なし
AccountServiceを利用してログイン処理を行なっています

GoogleLoginService
Googleログイン
なし
AccountService、AuthServiceを利用してログイン処理を行なっています

ItemListService
アイテムリストの取得
CloudFirestore
アイテムデータの監視を行なっています

ItemService
アイテムデータの取得、更新
CloudFirestore, Analytics
データの取得後、Analyticsでview_itemイベントを送信しています

SignupService
サインアップ
なし
AccountServiceを利用してサインアップ処理を行なっています

StorageService
画像のアップロード
Storage
画像をアップロードして、ダウンロードURLの取得を行なっています

UserItemListService
投稿アイテムリストの取得
CloudFirestore
複合インデックスが必要なクエリを実行しています


アイテムリスト

ホーム画面のアイテムリストは、アイテムの追加や削除を行うと自動で更新されるようになっています。また、最初に投稿アイテムを一気に取得するのではなく、下にスクロールして最後まで表示されると20件ずつ読み込むようになっています。

それらの処理は、ItemListServiceLoadAsyncで行なっています。最後のアイテムが表示された時なら、LoadAsyncを呼び出し、続きの20件を取得するようにしています。

実際にリアルタイム監視を行なっているのは以下の部分です。


ItemListService.cs

var query = _firestore.GetCollection(Item.CollectionPath)

.OrderBy(nameof(Item.Timestamp), true)
.EndAt(new long[] { _lastTimestamp });

query.ObserveAdded()
.Where(d => _items.FirstOrDefault(i => i.Id == d.Document.Id) == null)
.Subscribe(d => _items.Insert(d.NewIndex, d.Document.ToObject<Item>()))
.AddTo(_disposables);

query.ObserveModified()
.Select(d => d.Document.ToObject<Item>())
.Subscribe(item =>
{
var targetItem = _items.FirstOrDefault(i => i.Id == item.Id);
if (targetItem != null)
{
item.CopyTo(targetItem);
}
})
.AddTo(_disposables);

query.ObserveRemoved()
.Select(d => _items.FirstOrDefault(i => i.Id == d.Document.Id))
.Where(item => item != null)
.Subscribe(item => _items.Remove(item))
.AddTo(_disposables);


ObserveAddedで追加、ObserveModifiedで変更、ObserveRemovedで削除の監視を行なっています。

アイテムデータには、追加された時間であるTimestampを持っています。_lastTimestampは、その表示されている最後のアイテムのTimestampなので、EndAtで最後より新しいアイテムデータを監視していることになります。

ちなみに、Timestamplong型で、DateTime.Now.Ticksを与えています。時間ならDateTime型でもいいのではないかと思いますが、それだとうまく動かないことがあります。なぜなら、NSDateJava.Util.Dateから、DateTimeに変換するときに微妙に誤差が出てしまい、データベース上に持っている値と異なることがあるからです。なので、誤差の出ることがないlong型を使うようにしています。


データ監視の解除

Prismでは、ページが破棄された時にはDestroyが呼ばれるので、監視の解除はこのタイミングで行えばいいです。

ただ、Androidでは、バックボタンでアプリを終了した場合は、Destroyが呼ばれないので、監視をバックグラウンドで続けたままになってしまいます。これはよろしくないので、MainActivityOnDestroyで解除処理を走らせています。(AppクラスのDestroyが解除処理です。)


MainActivity.cs

protected override void OnDestroy()

{
base.OnDestroy();

App.Destroy();
}



トランザクション

いいねした時のいいね数の更新は、取得している値をインクリメントしていますが、そのまま普通に更新してしまうと、同時にいいねされた時などにデータの不整合が起きる可能性があります。このような場合に対応するために、Firestoreにはトランザクションの機能があります。

ItemServiceLikeAsyncで以下のように利用しています。


ItemService.cs

var success = await _firestore.RunTransactionAsync(transaction =>

{
var document = _firestore.GetCollection(Models.Item.CollectionPath)
.GetDocument(Item.Value.Id);

var item = transaction.GetDocument(document).ToObject<Item>();

if (item == null)
return false;

item.LikeCount++;
transaction.UpdateData(document, item);

return true;
}).ConfigureAwait(false);



Analyticsのscreen_viewイベント

Analyticsは、画面遷移が発生すると自動的にUIViewControllerActivityのクラス名でscreen_viewイベントを送るのですが、Xamarin.Formsではうまく機能しません。iOSでは、遷移は同じクラスのViewControllerが作られるので、画面の判別がつきません。Androidは、1つのActivityでViewを切り替えているだけなので、イベント自体送られません。仕方ないので、サンプルアプリでは、OnAppearing時に手動でイベントを送るようにしています。(OnNavigatedToでもいいのですが、最初に表示される画面がうまく設定されなかったためOnAppearingにしました)


ViewModelBase.cs

public virtual void OnAppearing()

{
var typeName = GetType().Name;
CrossFirebaseAnalytics.Current.SetCurrentScreen(typeName, typeName);
}


まとめ

Xamarin + Firebaseはいいぞ!

明日は、@ktz_alias さんです。よろしくお願いします!