Xamarin に Firebase を導入する
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.cs
の FinishedLaunching
で初期化します。
[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.cs
の OnCreate
で初期化します。
public class MainActivity : FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
//...
// GetInstanceを呼ぶことで初期化させておく(もしかしたら不要かも)
_ = Firebase.Analytics.FirebaseAnalytics.GetInstance(this);
//...
}
}
トラッキング
各種トラッキング方法について解説します。
ここでは Dependency Injection できるように IAnalyticsService
というインタフェースを用意して実装する例を示します。不要であれば各メソッドの実装のみ参考にしてください。
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
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
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にアップロードされるようになります。(ただしデバッグビルド時には FirebaseCrashlyticsUploadSymbolsEnabled
は False
にしておくと良いかと思います。)
<PropertyGroup>
<FirebaseCrashlyticsUploadSymbolsEnabled>True</FirebaseCrashlyticsUploadSymbolsEnabled>
<FirebaseCrashlyticsUploadSymbolsContinueOnError>False</FirebaseCrashlyticsUploadSymbolsContinueOnError>
</PropertyGroup>
Android
com.google.firebase.crashlytics.mapping_file_id
の追加
Resources/values/strings.xml
に必要な string 定義を1つ追加します。(これに関しては詳細はイマイチよくわかっていない)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="com.google.firebase.crashlytics.mapping_file_id">none</string>
</resources>
初期化
iOS
AppDelegate.cs
の FinishedLaunching
で初期化します。
[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.cs
の OnCreate
で初期化します。
public class MainActivity : FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
//...
Firebase.Crashlytics.FirebaseCrashlytics.Instance.SetCrashlyticsCollectionEnabled(true);
//...
}
}
以上でクラッシュログが Firebase に自動送信されるようになります。
非致命的なエラーの情報収集
ハンドリング可能なランタイムエラーや独自のエラーログを、クラッシュとは区別した「非致命的」なイベントとして Firebase Crashlytics に送信することができます。
ここでは Dependency Injection できるように ILogService
というインタフェースを用意して実装する例を示します。不要であれば各メソッドの実装のみ参考にしてください。
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
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
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