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

 この記事は、[Xamarin Advent Calendar 2018](https://qiita.com/advent-calendar/2018/xamarin)の５日目です。

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

# 作成したライブラリ
Firebaseの５つの機能のライブラリを作成しました。NuGetで公開済みなのですぐに利用できます。
使い方は、それぞれのGitHubをみてください。（詳しい機能の説明はしていないので、[公式のドキュメント](https://firebase.google.com/docs/)も参照してください）

NuGet

* [Plugin.CloudFirestore](https://www.nuget.org/packages/Plugin.CloudFirestore/)
* [Plugin.FirebaseAuth](https://www.nuget.org/packages/Plugin.FirebaseAuth/)
* [Plugin.FirebaseStorage](https://www.nuget.org/packages/Plugin.FirebaseStorage/)
* [Plugin.FirebaseAnalytics](https://www.nuget.org/packages/Plugin.FirebaseAnalytics/)
* [Plugin.FirebaseCrashlytics](https://www.nuget.org/packages/Plugin.FirebaseCrashlytics/)

GitHub

* [f-miyu/Plugin.CloudFirestore](https://github.com/f-miyu/Plugin.CloudFirestore)
* [f-miyu/Plugin.FirebaseAuth](https://github.com/f-miyu/Plugin.FirebaseAuth)
* [f-miyu/Plugin.FirebaseStorage](https://github.com/f-miyu/Plugin.FirebaseStorage)
* [f-miyu/Plugin.FirebaseAnalytics](https://github.com/f-miyu/Plugin.FirebaseAnalytics)
* [f-miyu/Plugin.FirebaseCrashlytics](https://github.com/f-miyu/Plugin.FirebaseCrashlytics)

## Plugin.CloudFirestore
[Cloud Firestore](https://firebase.google.com/docs/firestore/)を利用するためのライブラリです。
データベースから取得した時のデータの型は、当然iOSとAndroidでは異なるので、共通の型にマッピングする機能を備えています。また、リアルタイムでのデータの更新は、Rxが利用できるようにしています。

## Plugin.FirebaseAuth
[Authentication](https://firebase.google.com/docs/auth/)を利用するためのライブラリです。
Plugin.CloudFirestoreやPlugin.FirebaseStorageとは、セットで使うことになります。
また、各プロバイダーへの認証の機能はないので、[Xamarin.Auht](https://www.nuget.org/packages/Xamarin.Auth)などを使ってください。

## Plugin.FirebaseStorage
[Storage](https://firebase.google.com/docs/storage/)を利用するためのライブラリです。
進捗のモニタリングやキャンセル、一時停止にも対応しています。

## Plugin.FirebaseAnalytics
[アナリティクス](https://firebase.google.com/docs/analytics/)を利用するためのライブラリです。

## Plugin.FirebaseCrashlytics
[Crashlytics](https://firebase.google.com/docs/crashlytics)を利用するためのライブラリです。
[昨日の記事](https://qiita.com/hypermild/items/206345ddb6cb0ad53806)にも書かれてあるように、通常色々やらないといけないのをこれを入れるだけで大丈夫なようにもしています。

# サンプルアプリ
これらのライブラリを利用したサンプルアプリも作成しました。画像を投稿して、リストに表示されて、いいねができる、ちょっとしたSNSアプリです。
メールアドレスとGoogleでのログインができるようにしています。
リストは、リアルタイム監視をしているので、誰かが投稿やいいねをすると自動で表示が更新されます。

[f-miyu/XamarinFirebaseSample](https://github.com/f-miyu/XamarinFirebaseSample)

![スクリーンショット 2018-12-04 21.30.15.png](https://qiita-image-store.s3.amazonaws.com/0/236182/30bf7762-b026-82ac-a44a-df69a65cf6d1.png) ![スクリーンショット 2018-12-04 21.32.23.png](https://qiita-image-store.s3.amazonaws.com/0/236182/c950c5ba-e835-c6d1-38c6-e3a0a4133235.png)

# サンプルアプリを動かすには
サンプルアプリのGitHubには、`GoogleService-Info.plist`や`google-services.json`を含めていないので、動かすには各自でFirebaseの設定を行なってください。
以下で設定の仕方を説明します。

## Firebaseプロジェクトの作成

[Firebaseのコンソール](https://console.firebase.google.com)で、[プロジェクトの追加]をクリックします。
プロジェクトの追加画面で、プロジェクト名を入力し、アナリティクスの地域を日本に設定、同意に
チェックをして、[プロジェクトを作成]をクリックします。

![スクリーンショット 2018-12-04 1.11.11.png](https://qiita-image-store.s3.amazonaws.com/0/236182/361ff01f-3e4b-4b20-7050-a1e180156ddb.png)

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

![スクリーンショット 2018-11-26 2.17.28.png](https://qiita-image-store.s3.amazonaws.com/0/236182/f0ca5220-6d3e-0821-72e0-8c28642893df.png)

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

![スクリーンショット 2018-11-26 2.17.55.png](https://qiita-image-store.s3.amazonaws.com/0/236182/755be9a7-e1b7-a63a-aace-fa6a3d998a3d.png)

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

![スクリーンショット 2018-11-26 2.18.05.png](https://qiita-image-store.s3.amazonaws.com/0/236182/92bdbc3f-23d7-0abe-bd83-56517d1a3022.png)

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

![スクリーンショット 2018-12-04 2.26.21.png](https://qiita-image-store.s3.amazonaws.com/0/236182/2d8a75b6-6d35-12e2-885d-903286b98718.png)

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

![スクリーンショット 2018-11-26 2.19.27.png](https://qiita-image-store.s3.amazonaws.com/0/236182/4677043c-473e-4a8e-423a-d34e4d37fb9a.png)

Androidのパッケージ名を入力します。また、サンプルアプリでは、Googleログインを行なっているので、SHA-1の入力も必要です。
Xamarin.Androidで使用する、debug.keystoreのSHA-1フィンガープリントを取得するには、[ここ](https://docs.microsoft.com/ja-jp/xamarin/android/deploy-test/signing/keystore-signature)を参考にしてください。
入力したら、[次へ]をクリックします。

![スクリーンショット 2018-11-26 2.20.03.png](https://qiita-image-store.s3.amazonaws.com/0/236182/cdb1cfae-7906-218b-975b-33bfc9efc413.png)

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

![スクリーンショット 2018-11-26 2.20.10.png](https://qiita-image-store.s3.amazonaws.com/0/236182/99177506-82b7-edc7-2644-cab900b83e3b.png)

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

<img width="1306" alt="スクリーンショット 2018-12-04 2.57.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/236182/d799e649-d3c9-d5ae-dfed-e5aaa0658ff4.png">

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

![スクリーンショット 2018-11-26 2.21.44.png](https://qiita-image-store.s3.amazonaws.com/0/236182/8b85cc3e-4c3b-ecd7-836f-a177cc1e259d.png) 

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

![スクリーンショット 2018-11-26 2.21.48.png](https://qiita-image-store.s3.amazonaws.com/0/236182/c03f5cf3-41f6-66e5-347f-3b20c614f523.png)

### セキュリティルールの変更
認証しなければ、読み込みも書き込みもできないようにします。
[ルール]タブで、以下のように記述して、[公開]をクリックします。

```
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}
```
![スクリーンショット 2018-12-04 19.50.42.png](https://qiita-image-store.s3.amazonaws.com/0/236182/577d017a-dad6-a2ba-8243-9dc823ab9c04.png)

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

![スクリーンショット 2018-12-04 19.47.19.png](https://qiita-image-store.s3.amazonaws.com/0/236182/681a6c9e-13f7-eacf-ee8f-a6c1eb6fc36c.png)

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

**対象となるコレクション**
items

**フィールド**

順番|フィールドのパス|モード
---|---|---
1|OwnerId|Ascending
2|Timestamp|Descending

![スクリーンショット 2018-12-02 16.06.09.png](https://qiita-image-store.s3.amazonaws.com/0/236182/b457e9b6-3444-da94-0c7e-ad4516572b9a.png)

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

![スクリーンショット 2018-12-02 16.00.33.png](https://qiita-image-store.s3.amazonaws.com/0/236182/77023f22-1e89-4380-dc48-61a6b5bf508a.png)

## Authenticationの設定
[Authentication]セクションで、[ログイン方法]タブを選択します。
[メール/パスワード]を有効にして、[保存]をクリックします。

![スクリーンショット 2018-12-03 23.26.34.png](https://qiita-image-store.s3.amazonaws.com/0/236182/ca2fae9b-d8d5-0ae4-af9e-a7bff81cbd71.png)

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

![スクリーンショット 2018-12-04 0.36.51.png](https://qiita-image-store.s3.amazonaws.com/0/236182/b8100b73-684e-b872-781a-532059dcf1c9.png)

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

![スクリーンショット 2018-11-30 18.01.14.png](https://qiita-image-store.s3.amazonaws.com/0/236182/5e07c557-174b-815d-fb80-be0a2b3ca88b.png) 

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

![スクリーンショット 2018-11-30 18.01.22.png](https://qiita-image-store.s3.amazonaws.com/0/236182/6fbb1a0e-f442-0c91-ad0a-2a771050f6e5.png)

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

![スクリーンショット 2018-12-04 22.24.55.png](https://qiita-image-store.s3.amazonaws.com/0/236182/becb51cf-28be-f487-e348-5ab1ae324ebe.png)

[このアプリではCrashlyticsを初めて使用します]を選択して、[次へ]をクリックします。
この設定は、iOS、Androidのアプリごとに行う必要があります。

![スクリーンショット 2018-12-02 22.13.20.png](https://qiita-image-store.s3.amazonaws.com/0/236182/65183fb0-9d32-d45a-996b-b131397ad571.png)

## プログラム側の設定

### 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コンソールの[認証情報ページ](https://console.developers.google.com/apis/credentials)で確認できます。OAuth 2.0 クライアント IDにあるタイプがAndroidであるクライアントIDをコピーして、`AndroidGoogleClientId`に設定します。`AndroidReversedGoogleClientId`には、その逆ドメインを設定します。

![スクリーンショット 2018-12-04 16.06.40.png](https://qiita-image-store.s3.amazonaws.com/0/236182/5a4bb50f-77b1-ca51-d252-b7cc1659e0e2.png)

### iOSのURLスキームの設定
iOSプロジェクトの`info.pist`を開き、[詳細設定]画面にします。
[URLの種類を追加]をクリックして、[URL Schenes:]に先ほども利用した`GoogleService-Info.plist`の`REVERSED_CLIENT_ID`の値を設定します。

![スクリーンショット 2018-12-04 19.02.54.png](https://qiita-image-store.s3.amazonaws.com/0/236182/bdd33711-716b-d791-887d-19b3436b74e8.png)

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

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

## 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|複合インデックスが必要なクエリを実行しています

## アイテムリスト
ホーム画面のアイテムリストは、アイテムの追加や削除を行うと自動で更新されるようになっています。また、最初に投稿アイテムを一気に取得するのではなく、下にスクロールして最後まで表示されると２０件ずつ読み込むようになっています。
それらの処理は、`ItemListService`の`LoadAsync`で行なっています。最後のアイテムが表示された時なら、`LoadAsync`を呼び出し、続きの２０件を取得するようにしています。
実際にリアルタイム監視を行なっているのは以下の部分です。

```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`で最後より新しいアイテムデータを監視していることになります。
ちなみに、`Timestamp`は`long`型で、`DateTime.Now.Ticks`を与えています。時間なら`DateTime`型でもいいのではないかと思いますが、それだとうまく動かないことがあります。なぜなら、`NSDate`や`Java.Util.Date`から、`DateTime`に変換するときに微妙に誤差が出てしまい、データベース上に持っている値と異なることがあるからです。なので、誤差の出ることがない`long`型を使うようにしています。

## データ監視の解除
Prismでは、ページが破棄された時には`Destroy`が呼ばれるので、監視の解除はこのタイミングで行えばいいです。
ただ、Androidでは、バックボタンでアプリを終了した場合は、`Destroy`が呼ばれないので、監視をバックグラウンドで続けたままになってしまいます。これはよろしくないので、`MainActivity`の`OnDestroy`で解除処理を走らせています。（`App`クラスの`Destroy`が解除処理です。）

```MainActivity.cs
protected override void OnDestroy()
{
    base.OnDestroy();

    App.Destroy();
}
```

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

```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は、画面遷移が発生すると自動的に`UIViewController`や`Activity`のクラス名で`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 さんです。よろしくお願いします！