🔧

Xamarin に Firebase を導入する

2021/12/23に公開

Xamarin プロジェクトに Firebase を導入する方法を解説します。

なお、Firebase 自体は公式ドキュメントを参考に iOS/Android 用のセットアップをすればOKです。Xamarin 固有の特別な設定をする必要は特にありません。

共通セットアップ

Firebase コンソール > プロジェクトの設定 > 全般 > マイアプリ から構成ファイルを取得します。

  • iOS: GoogleService-Info.plist
  • Android: google-services.json

取得した構成ファイルを iOS/Android 各プロジェクトルートに配置します。構成ファイルのビルドアクションは下記のように設定します。(Visual Studio for Mac の場合、ファイル名の右クリックメニューからビルドアクションを設定できます。)

  • iOS: BundleResource
  • Android: GoogleServicesJson

Firebase Analytics

NuGet パッケージの追加

NuGet から下記パッケージを追加します。

  • iOS: Xamarin.Firebase.iOS.Analytics
  • Android: Xamarin.Firebase.Analytics

初期化

iOS

AppDelegate.csFinishedLaunching で初期化します。

AppDelegate.cs
[Register(nameof(AppDelegate))]
public partial class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        //...

        // Firebase共通の初期化処理
        Firebase.Core.App.Configure();

        //...
        return base.FinishedLaunching(app, options);
    }
}

Android

MainActivity.csOnCreate で初期化します。

MainActivity.cs
public class MainActivity : FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        //...

        // GetInstanceを呼ぶことで初期化させておく(もしかしたら不要かも)
        _ = Firebase.Analytics.FirebaseAnalytics.GetInstance(this);

        //...
    }
}

トラッキング

各種トラッキング方法について解説します。

ここでは Dependency Injection できるように IAnalyticsService というインタフェースを用意して実装する例を示します。不要であれば各メソッドの実装のみ参考にしてください。

IAnalyticsService.cs
public interface IAnalyticsService
{
    void SetUserId(string userId);
    void SetUserProperty(string name, string value);
    void TrackScreenView(string screenName, string screenClass);
    void TrackEvent(string name, IReadOnlyDictionary<string, string> properties = null);
}

iOS

AnalyticsService.ios.cs
using Firebase.Analytics;

sealed class AnalyticsService : IAnalyticsService
{
    public void SetUserId(string userId)
    {
        Analytics.SetUserId(userId ?? string.Empty);
        // Crashlytics を使用する場合は併せて設定
        Firebase.Crashlytics.Crashlytics.SharedInstance.SetUserId(userId ?? string.Empty);
    }

    public void SetUserProperty(string name, string value)
    {
        Analytics.SetUserProperty(value, name);
    }

    public void TrackScreenView(string screenName, string screenClass)
    {
        if (!string.IsNullOrEmpty(screenName) && !string.IsNullOrEmpty(screenClass))
        {
            Analytics.SetScreenNameAndClass(screenName, screenClass);
        }
    }

    public void TrackEvent(string name, IReadOnlyDictionary<string, string> properties = null)
    {
        if (string.IsNullOrEmpty(name))
        {
            return;
        }

        var keys = new List<NSString>();
        var values = new List<NSString>();

        if (properties is object)
        {
            foreach (var item in properties)
            {
                if (!string.IsNullOrEmpty(item.Value))
                {
                    keys.Add(new NSString(item.Key));
                    values.Add(new NSString(item.Value));
                }
            }
        }

        NSDictionary<NSString, NSObject> parametersDictionary;
        if (keys.Count > 0 && values.Count > 0)
        {
            parametersDictionary = NSDictionary<NSString, NSObject>.FromObjectsAndKeys(values.ToArray(), keys.ToArray(), keys.Count);
        }
        else
        {
            parametersDictionary = new NSDictionary<NSString, NSObject>();
        }

        Analytics.LogEvent(name, parametersDictionary);
    }
}

Android

AnalyticsService.android.cs
using Firebase.Analytics;

sealed class AnalyticsService : IAnalyticsService
{
    private readonly FirebaseAnalytics _analytics = FirebaseAnalytics.GetInstance(Application.Context);

    public void SetUserId(string userId)
    {
        _analytics.SetUserId(userId);
        // Crashlytics を使用する場合は併せて設定
        Firebase.Crashlytics.FirebaseCrashlytics.Instance.SetUserId(userId);
    }

    public void SetUserProperty(string name, string value)
    {
        _analytics.SetUserProperty(name, value);
    }

    public void TrackScreenView(string screenName, string screenClass)
    {
        if (!string.IsNullOrEmpty(screenName) && !string.IsNullOrEmpty(screenClass))
        {
            var bundle = new Bundle();
            bundle.PutString(FirebaseAnalytics.Param.ScreenName, screenName);
            bundle.PutString(FirebaseAnalytics.Param.ScreenClass, viewModel.Name);
            _analytics.LogEvent(FirebaseAnalytics.Event.ScreenView, bundle);
        }
    }

    public void TrackEvent(string name, IReadOnlyDictionary<string, string> properties)
    {
        if (string.IsNullOrEmpty(name))
        {
            return;
        }

        var p = new Bundle();
        if (properties is object)
        {
            foreach (var item in properties)
            {
                p.PutString(item.Key, item.Value);
            }
        }

        _analytics.LogEvent(name, p);
    }
}

Firebase Crashlytics

NuGet パッケージの追加

NuGet から下記パッケージを追加します。

  • iOS: Xamarin.Firebase.iOS.Crashlytics
  • Android: Xamarin.Firebase.Crashlytics

構成設定

iOS

dSYM自動アップロード設定

iOS の .csproj ファイルを直接開き、下記設定を追記します。これによりビルド時にdSYMが自動でFirebaseにアップロードされるようになります。(ただしデバッグビルド時には FirebaseCrashlyticsUploadSymbolsEnabledFalse にしておくと良いかと思います。)

Sample.iOS.csproj
<PropertyGroup>
    <FirebaseCrashlyticsUploadSymbolsEnabled>True</FirebaseCrashlyticsUploadSymbolsEnabled>
    <FirebaseCrashlyticsUploadSymbolsContinueOnError>False</FirebaseCrashlyticsUploadSymbolsContinueOnError>
</PropertyGroup>

Android

com.google.firebase.crashlytics.mapping_file_id の追加

Resources/values/strings.xml に必要な string 定義を1つ追加します。(これに関しては詳細はイマイチよくわかっていない)

Resources/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="com.google.firebase.crashlytics.mapping_file_id">none</string>
</resources>

初期化

iOS

AppDelegate.csFinishedLaunching で初期化します。

AppDelegate.cs
[Register(nameof(AppDelegate))]
public partial class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        //...

        // Firebase共通の初期化処理
        Firebase.Core.App.Configure();

        //...
        return base.FinishedLaunching(app, options);
    }
}

Android

MainActivity.csOnCreate で初期化します。

MainActivity.cs
public class MainActivity : FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        //...

        Firebase.Crashlytics.FirebaseCrashlytics.Instance.SetCrashlyticsCollectionEnabled(true);

        //...
    }
}

以上でクラッシュログが Firebase に自動送信されるようになります。

非致命的なエラーの情報収集

ハンドリング可能なランタイムエラーや独自のエラーログを、クラッシュとは区別した「非致命的」なイベントとして Firebase Crashlytics に送信することができます。

ここでは Dependency Injection できるように ILogService というインタフェースを用意して実装する例を示します。不要であれば各メソッドの実装のみ参考にしてください。

ILogService.cs
public interface ILogService
{
    void Error(Exception e, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1);
    void Error(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1);
}

iOS

LogService.ios.cs
sealed class LogService : ILogService
{
    public void Error(Exception e, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1)
        => WriteAsError(e, "", memberName, filePath, lineNumber);

    public void Error(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1)
        => WriteAsError(null, message, memberName, filePath, lineNumber);

    internal static void WriteAsError(Exception e, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1)
    {
        var exception = e ?? new Exception(message);
        var fileName = System.IO.Path.GetFileName(filePath);

        var exInfo = new Dictionary<object, object>
        {
            [NSError.LocalizedDescriptionKey] = exception.Message,
            [nameof(exception.StackTrace)] = exception.StackTrace,
            ["Exception"] = exception.ToString(),
            ["Location"] = $"{memberName}({fileName}:{lineNumber})"
        };

        WriteAsError(new NSError(
            new NSString($"{fileName} line {lineNumber}"),
            -1,
            NSDictionary.FromObjectsAndKeys(exInfo.Values.ToArray(), exInfo.Keys.ToArray(), exInfo.Count)
        ));
    }

    internal static void WriteAsError(NSError e)
    {
        Firebase.Crashlytics.Crashlytics.SharedInstance.RecordError(e);
    }
}

Android

LogService.android.cs
using Firebase.Crashlytics;

sealed class LogService : ILogService
{
    public void Error(Exception e, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1)
        => WriteAsError(e, "", memberName, filePath, lineNumber);

    public void Error(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1)
        => WriteAsError(null, message, memberName, filePath, lineNumber);

    internal static void WriteAsError(Exception e, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = -1)
    {
        var location = $"{memberName}({System.IO.Path.GetFileName(filePath)}:{lineNumber})";
        var throwable = e is null ? new Java.Lang.Exception(message) : Java.Lang.Throwable.FromException(e);

        FirebaseCrashlytics.Instance.Log($"Logged at {location}");
        FirebaseCrashlytics.Instance.Log(infoJson);
        FirebaseCrashlytics.Instance.RecordException(throwable);
    }
}

おわりに

今回は Xamarin への Firebase Analytics と Firebase Crashlytics の導入方法を解説しました。

Firebase Cloud Messaging, Firebase Remote Config, Firebase Performance Monitoring, Firebase Dynamic Links の導入経験もあるため、やる気が出たらそのうち書きます。(書くとは言っていない)

この記事は エムティーアイ Advent Calendar 2021 の23日目の記事でもあります。業務を通じて得た知見をアウトプットしました。弊社の他メンバーの記事もよろしくお願いします!

Discussion