LoginSignup
4
2

More than 3 years have passed since last update.

Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(11) Firebase導入編(Firebase Analytics)

Last updated at Posted at 2020-05-13

以下の記事の続きです。

Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(10)
AndroidアプリでCIツール-Jenkins/CircleCI編

かなり時間が空いてしまいました。
Flutterにだいぶ浮気していました(笑)

よろしければご覧下さい。
FlutterアプリをPlayストアに登録してみた
FlutterアプリをiOS版ビルドに必要な手順のまとめ(debug/release)とTestFlightに上げるまで

他にもFlutter記事を多数上げています。

さて、Firebaseにデータ保存するのをやっていこうと思います。
FirebaseのFirestoreというのを使います。クラウドへのデータ保存ですね。データを保存するには、その前にアカウントを作ってもらって、認証するのが良いでしょうね。たとえば他の端末でも同じアプリを入れたら同じデータが見られるとか。
それには、Firebase Authenticationを使って認証していこうと思います。

・・・が、その前に、準備編として、Firebase導入、その接続の確認として、Analytics, Crashlyticsを入れていきます。

今回の目標

FirebaseSDKを導入し、Analytics、Crashlyticsの確認が出来る。

環境など

Gradleプラグインのバージョンを上げてあります。もし前回までのままでビルドできないなどあったら、上げてみて下さい。

root/build.gradle
classpath 'com.android.tools.build:gradle:3.6.3'
gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

Kotlinも最新版に上げました。

root/build.gradle
    ext.kotlin_version = '1.3.72'

Firebase超概要

アプリやウェブサービスで有用ないくつものクラウドサービスを提供しているGoogleのmBaaS(mobile Backend as a Service)です。

サーバーレスで色んなことが出来ます。
昔はサードパーティーのサービスでしたが、Googleに買収され、成長を続けています。

特に個人や小規模な人数でアプリを開発するときには、サーバーを立てたり等のバックエンド側の準備、開発がネックになってきたりもします。それらのおおよその部分をほとんど省くことが出来るので、大変有り難いサービスです。

1.Analytics概要

Googleといえば、Google Analyticsですね。サイト上のユーザー行動を解析して、それらを広告に連動させ、広告収入を得るのが、Googleさんの一番の収入源です。
その行動解析用のデータ収集をモバイルアプリで簡単に出来るようにしてくれるのが、Firebase Analyticsです。
※以前はGoogle Analyticsのモバイル向けSDKがGoogleから出ていましたが、廃止されており、現在はFirebase Analyticsに統一されています。

2.Crashlytics概要

こちらはもとはFabric社が行っていた、クラッシュレポートサービスです。
https://get.fabric.io/
↑Good bye言われてる^^;

これもGoogleがFirebaseに吸収しました。

クラッシュレポートサービスの何が嬉しいかって、スタックトレースが見られるので、「JavaコードのXXクラスのMM行目でYYY Exceptionですよ」ってのが分かることですね。で、それらが一定期間中に何件発生しているかとか、OS別の集計とか、そういったことを出してくれます。

そうすると、対応を急がなければならないクラッシュ、様子見して良いクラッシュなど、対応の優先付けが出来て便利なわけです。
特に、リリースされたアプリは通信瞬断や他のアプリの割り込み等の、開発者が単独でそのアプリだけを使っているときには起こりえない/想定してテストしづらい状況が起こって、それらに起因するクラッシュというのが目に見えて分かります。

コードを追ってるだけだとnullは有り得ないけど、非同期実行されていて実はあり得て、ものすごい量のnullチェックが必要だったとか、そんなことも発覚したりもします。

Firebaseの導入

GoogleアカウントまたはGsuitのアカウントがあれば、Firebase Consoleにログイン出来ます。
https://console.firebase.google.com/?hl=JA

1.Firebaseプロジェクトの作成

  • Firebaseコンソールにログインしたら、[+プロジェクトを追加]をクリック

firebase_add_project.png

  • プロジェクト名を入力
    • 英数字の小文字のみ使えます。記号は使えません。ハイフン-、アンダーバー_、不可です。
  • [続行]をクリック
  • [このプロジェクトで Google アナリティクスを有効にする]が有効なのを確認

    • GAのアカウントを用意してない場合は、ここで作るように言われると思います。
  • [続行]をクリック

少し待つと、プロジェクトページに遷移します。

2.Androidアプリの登録

トップページで、ドロイド君をタップします。

firebase_add_app.png

(1)リリース用パッケージ名の登録

アプリのパッケージ名(リリース用)と、表示用の名称(これは日本語可)を入れます。
デバッグ証明書のSHA-1は今は特に不要です。

firebase_app_info.png

debugビルド用にapplicationIdSuffixを使っている場合、以下は今はスキップします。

  • play-services.jsonのダウンロード
  • Firebase SDK の追加
  • 接続の確認

[コンソールに進む]をクリックしていったん終了させて下さい。

使っていない場合は、(3)play-servicesjsonのダウンロードから、そのまま続けて行って下さい。

(2)debugアプリの登録

debugビルド用にapplicationIdSuffixを使っている場合に行います。
上記と同じようにAndroidアプリのパッケージ名と表示名を入力し、[アプリを登録]をクリックするところまで進めます。

(3)play-services.jsonのダウンロード

  • play-services.jsonをダウンロード
  • プロジェクトルート/app下に置く

(4)アプリにFirebaseを設定する

プロジェクトルート下にあるbuild.gradleに追記します。

root/build.gradle
  dependencies {
      ...
      classpath 'com.google.gms:google-services:4.3.3' // 追加
  }

app下にあるbuild.gradleに追記します。

app/build.gradle
apply plugin: 'com.google.gms.google-services' // ファイルの上の方
andorid{
}

※昔はbuild.gradleの一番下に書かなければならなかったけど、いつの間にか変わったようです。

Gradle Syncをします。

(5)通信権限をアプリに追加

追記 2020/05/15
アプリのマニフェストファイルを編集します。
※しなくてもdebubビルドは通信してしまいますが、releaseビルドで権限が無く落ちます。

AndroidManifest.xml

<manifest
    ...
    <uses-permission android:name="android.permission.INTERNET"/>

    <application

(6)接続確認

アプリをビルドして、起動します。
接続確認が取れたら、[コンソールに進む]をクリックして設定を完了します。

もし、接続確認がどうしても終わらない場合は、いったんスキップして、次のDebugViewの設定を行ってみて下さい。

2.DebugView

Firebase Analyticsへの送信は通信量やバッテリー負荷などを軽減するためある程度情報が溜まってからまとめて送られているそうなのですが、それをデバッグ用途に逐次送るようにする、という設定をすることが可能です。

この設定がONだと、FirebaseのDebugViewというページで、発生したイベントをほぼリアルタイムで見ることが出来るようになります。

debugview.png

(1)Android端末に設定する

ターミナルなどで以下のコマンドを実行し、その後アプリを再起動する。
パッケージ名は、debugビルドの場合でsuffixを付けている場合は、ちゃんと付けたものを指定して下さい。

adb shell setprop debug.firebase.analytics.app パッケージ名

不要になったら、以下のように実行する

adb shell setprop debug.firebase.analytics.app .none.

こうしておくと、逐次送る設定が解除されます。

(2)Debug Viewで確認

アプリをある程度操作してからDebugViewを見ると、こんな風にイベントが随時届きます。

android_debugview.png

Firebase Analyticsでイベントを送信する

以下の最低限のイベントを送信しておくことにします。

1.画面名報告

Debug Viewを見ていると気付いたかと思いますが、Activityを遷移するとそのクラス名が送られているのが分かるかと思います。
このままでも良いですが、ここを日本語にしてみようと思います。
また、同一Activity内でFragmentFragmentの遷移がある場合に、同じように使うことが出来ます。

一番良い方法は、ActivityFragmentonResumeで画面名報告イベントを送ることです。onCreateonStartだと、まだアクティブなActivityがないということで送信はエラーになってしまうので、onResumeでやりします。

直接毎回FirebaseのAPIを呼んでも良いのですが、DIすることを考えてラッパークラスを作っておきます。

パッケージはutilsとか作りましょうか。

(1)utilsパッケージを作成

  • AndroidStudioのパッケージルートで右クリックし、[New]-[Package]と選ぶ

qiita11_02.png

  • 任意のパッケージ名を入力する。例:utils
  • [OK]をクリック

続いて、Util.ktというファイルを、そのパッケージに移動しておくことにします。

  • Util.ktutilsパッケージにDrag&Drop

qiita11_03.png

  • [OK]をクリック

ちょっと時間がかかりますがダイアログが消えるまで待ちます。

無事、移動しました。

qiita11_04.png

  • test下のUtil.ktも同様にutilsパッケージを作って移動

(2)Analytics用のラッパークラスを作成

utilsパッケージで右クリックし、[New]-[Kotlin File/Class]でClassを選び、AnalyticsUtilと入力します。

utils/AnalyticsUtil.kt
import android.app.Activity
import android.app.Application
import com.google.firebase.analytics.FirebaseAnalytics

class AnalyticsUtil(app: Application) {

    private val firebaseAnalytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(app)

    fun sendScreenName(activity: Activity, screenName: String, classOverrideName: String?) {
        firebaseAnalytics.setCurrentScreen(activity, screenName, classOverrideName)
    }
}

(3)Koinモジュールに追加

Koinやったの覚えてますか?
モジュールを追加してインスタンスが注入されるようにします。

modules.kt

// FirebaseService
val firebaseModule = module{
    single{ AnalyticsUtil(androidApplication()) }
}

// モジュール群
val appModules = listOf(
    viewModelModule
    , daoModule
    , repositoryModule
    , providerModule
    , firebaseModule // 追加
)

モジュール群のリストに追加するのも忘れずに。
上記のような書き方をしておくと、後でリストに追加していくのがちょっとだけ楽です。

(4)Activityで画面名報告を送る

Activityクラスで、Koinを使ってインジェクトします。
そしてonResumeをオーバーライドし、AnalyticsUtil#sendScreenNameで報告します。

MainActivity.kt
class Activity... {
    companion object {
        ...
        const val SCREEN_NAME = "トップ画面" // 追加
    }

    // AnalyticsTool inject by Koin
    val analytics:AnalyticsUtil by inject() // 追加


    override fun onCreate(savedInstanceState: Bundle?) {
      ...
    }

    // 追加
    override fun onResume() {
        super.onResume()
        analytics.sendScreenName(this, SCREEN_NAME)
    }
}

他のActivityにも追加しましょう。

なお、Activity追加する度に全く同じコードを追加していくことになるので、アナリティクス 送信に限っては、基底クラスを作ってそこから全部派生させるようにするのもアリです。
ただ、基底クラスを作ると全部そこに処理を入れたくなって肥大していく可能性があるので、使う場合は注意が必要です。

base/BaseActivity.kt
// Analytics送信を基底クラスに持たせる場合のサンプル
abstract class BaseActivity : AppCompatActivity() {

    abstract val screenName: String

    // AnalyticsTool inject by Koin
    val analytics: AnalyticsUtil by inject()

    override fun onResume() {
        super.onResume()
        analytics.sendScreenName(this, screenName)
    }
}

Kotlinでは、プロパティも抽象クラスに持たせ、overrideすることが出来ます
これは、「プロパティに見えているけど、実はsetter/getter関数を見えないように使っている」と考えるとなんとなくしっくりくるかと思います。

使う場合はこうなります。

MainActivity.kt
class MainActivity : BaseActivity() {
    companion object {
        ...
        const val SCREEN_NAME = "トップ画面"
    }
    override val screenName: String
        get() = SCREEN_NAME

まあ、onResumeのオーバーライドしなくて良くなくなる程度ですが、送信コードの追加し忘れなんかは防止できますね。

以下のActivityに対して、設定を行いました。

  • MainActivity
  • InstagramShareActivity
  • TwitterShareActivity

(5)Fragmentで画面名報告を送る

Fragmentでもやることは同じです。
たとえば、このアプリはこれまで作ってきた形通りだとすると、InputActivityは、LogEditFragmentLogInputFragmentのどちらかが表示されます。両方とも「入力画面」という画面名で送っても良ければそれでも良いのですが、ここでは変えて送ってみることにします。

FragmentもActivityと同様、基底クラスを作ってそこに集約することにします。

base/BaseFragment.kt
import androidx.fragment.app.Fragment
import jp.les.kasa.sample.mykotlinapp.utils.AnalyticsUtil
import org.koin.android.ext.android.inject

abstract class BaseFragment : Fragment() {

    abstract val screenName: String

    // AnalyticsTool inject by Koin
    val analytics: AnalyticsUtil by inject()

    override fun onResume() {
        super.onResume()
        activity?.let { analytics.sendScreenName(it, screenName) }
    }
}

activityのnullチェックを一応しておきます。
使う方もActivityの場合と同じです。

LogInputFragment.kt
class LogInputFragment : BaseFragment() {

    companion object {
        ...
        const val SCREEN_NAME = "ログ編集画面"
    }

    // 画面報告名
    override val screenName: String
        get() = SCREEN_NAME

以下のFragmentに設定しました。

  • LogInputFragment
  • LogEditFragment

(6)Dialogで画面名報告を送る

DialogもDialogFragmentを使っていれば通常のFragmentと同じです。

ErrorDialog.kt
class ConfirmDialog : DialogFragment(), DialogInterface.OnClickListener {

    private val analytics: AnalyticsUtil by inject()

    override fun onResume() {
        super.onResume()
        activity?.let{ analytics.sendScreenName(it, "エラーダイアログ")}
    }

でもエラーダイアログはエラーメッセージも分かるようなイベントを送った方が良いかも知れませんね。
それに、ダイアログを閉じたときに元の画面に戻った報告が来ないので、ダイアログはダイアログで別な送り方をした方が良さそうです。
ということで、Githubにアップしたるコードでは、現時点で使われていないConfirmDialogには入れているものの、ErrorDialogには入れていません。

色々画面遷移したり、ダイアログを表示させたりしてから、Debug Viewを見てみて下さい。

screen_viewというのを開くと、パラメーターがたくさん出てきます。
そのなかの、firebase_screenを開くと、送った画面名が入っているはずです。
firebase_previous_screenなどがあって、前にどの画面にいたかも分かるようになっていますね。

qiita11_05.png

※本記事を作成中に、Calendarクラスの拡張関数clearTextに不具合があることが分かって、以下のように修正しています。

Util.kt
fun Calendar.clearTime(): Calendar {
    set(Calendar.HOUR_OF_DAY, 0) // HOURから修正
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
    set(Calendar.MILLISECOND, 0)
    return this
}

HOURのクリアだと、AM/PMが切り替わっていませんでした。従って、午後に実行すると、「正午」にセットされることになっており、LogInputFragmentでのlogInputValidationで不具合が起きていました。

2.ボタンイベント

ボタンをタップしたらイベント報告を送信します。
ボタン名をパラメータで送ります。

(1)Analyticsクラスにラッパーメソッドを作成

イベントを送信するには、Firebase Analytics APIのlogEventを使います。

AnalyticsUtil.kt
    /**
     * ボタンクタップイベント送信
     */
    fun sendButtonEvent(buttonName: String) {
        val bundle = Bundle().apply { putString("buttonName", buttonName) }
        firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_ITEM, bundle)
    }

logEventの第一引数は、FirebaseAnalytics.Eventで定義済みのイベント名を使うか、直接文字列で指定してカスタムイベントを送ることも出来ます。
ボタンタップは、定義済みのFirebaseAnalytics.Event.SELECT_ITEMを使うことにします。
パラメータとして、buttonNameを送ります。
パラメーターは一括してBundleに入れて送ります。

(2)ボタンタップイベントを送信する

ボタンタップイベントに実際にAnalyticsを送信するコードを入れていきましょう。
例えば、入力画面の日付選択ボタン、登録ボタンなどです。

LogInputFragment.kt
        contentView.button_update.setOnClickListener {
            validation()?.let {
                ErrorDialog.Builder().message(it).create().show(parentFragmentManager, null)
                return@setOnClickListener
            }
            analytics.sendButtonEvent("登録ボタン")
   ...
        }

        // 日付を選ぶボタンで日付選択ダイアログを表示
        contentView.button_date.setOnClickListener {
            analytics.sendButtonEvent("日付選択ボタン")
            DateSelectDialogFragment().show(parentFragmentManager, DATE_SELECT_TAG)
        }

バリデーションが通ってから送るかどうかは仕様次第ですが、ここはチェックがOKな時のみ送ることにします。

select_itemのイベントに、カスタムパラメーターとして送った"buttonName"がいることがDebug Viewで確認できます。

qiita11_06.png

編集画面には、更新、編集ボタンと、シェアメニューボタンがありますね。
同じように追加しておきましょう。

ボタン以外のチェックボックスやスピナー、スイッチは、イベントリスナーを登録していないので今回は送りません。
送る必要がある場合は、それぞれにイベントリスナーを登録してその中でアナリティクス送信をすれば良いでしょう。

ちなみに、Bundleで送っているパラメータですが、最大25個まで送信できます。

3.シェアイベント

Twitterシェア画面、Instagramシェア画面はまたちょっと別のイベントを送ることにします。
定義済みのFirebaseAnalytics.Event.SHAREが定義済みなので、これを使って、TwitterやInstagramシェア画面イベントを送信することにします。
パラメーターに、TwitterかInstagramか付けることにします。

TwitterShareActivity.kt
    private fun post(message: String) {
        ...
        try {
            startActivity(intent)
            analytics.sendShareEvent("Twitter")
        }catch(e: ActivityNotFoundException){
             ...
        }

Twitterは公式アプリだけを探しているのでActivityNotFoundExceptionが起こる可能性がありますが、InstgramはIntent.createChooserで共有画面をOSで出させているため、try-catchはありません。そのまま、startActivityしたあとにイベントを送れば良いでしょう。

InstagramShareActivity.kt
            startActivity(Intent.createChooser(share, "Share to"))
            analytics.sendShareEvent("Instagram")

4.その他のカスタムイベント

ボタン以外のイベントを送ることも考えてみます。
例えば、カレンダーのセルをタップしたとき。エラーダイアログの内容。などなどです。

Firebase Analytics APIのlogEventの第一引数に、カスタムイベント名をセットすれば良いだけです。

AnalyticsUtil.kt
    /**
     * カレンダーセルタップイベント送信
     */
    fun sendCalendarCellEvent(date: String) {
        val bundle = Bundle().apply { putString("date", date) }
        firebaseAnalytics.logEvent("calendar_cell", bundle)
    }
}

MonthlyPageFragmentonItemClickで呼んでやります。

MonthlyPageFragment.kt
    override fun onItemClick(data: CalendarCellData) {
        analytics.sendCalendarCellEvent(data.calendar.getDateStringYMD())
qiita11_07.png

なお、MonthlyPageFragmentは画面名は送らないことにします。なので、アナリティクス送信をする基底クラスを作っていたとしても、MonthlyPageFragmentはそれを継承しないようにします。

5.UserPropertyとAudience

Analyticsが威力を発揮するのは、UserPropertyを使ってAudience(条件に該当するユーザーのリスト)を作るときです。
実は、Analyticsが送られているとき、同時にUserPropertyも送られています。

自動で収集されている内容は、以下のページから確認が出来ます。
https://support.google.com/firebase/answer/6317486?hl=ja

これ以外に、アプリに固有のプロパティを作っておくと、イベント送信時に同時に送信することが出来ます。

例えば、アプリを開始するときに、「犬を飼っているか」というアンケートを表示して、その結果をUserPropertyに入れたとします。
そして、アプリの運用で「犬を飼っている人」にだけpushを送ったり、なんてことが出来ます。

UserPropertyを使うと何が便利かっていうのは、こちらの記事などが参考になります。
https://qiita.com/rmakiyama/items/5abb0064677dbc65a94e

(1)ユーザープロパティを作る

まず最初にFirebaseコンソール上でユーザープロパティを作る必要があります。

  • プロジェクトの左側のメニューにUser Propertiesというのがあるので、そこをクリック

qiita11_09.png

  • [新しいユーザープロパティ]をクリック
  • 英数小文字とアンダーバー('_')のみで名称を付ける
  • 説明を任意で入力する
qiita11_08.png
  • [作成]をクリック

これで、ユーザープロパティを作成できました。

(2)アプリでユーザープロパティの値を設定する

Firebase Analytics APIのsetUserPropertyを使います。

AnalyticsUtil.kt
    /**
     * ユーザープロパティ設定の例
     */
    fun setPetDogProperty(hasDog: Boolean) {
        firebaseAnalytics.setUserProperty("pet_dog", hasDog.toString())
    }

例えばこれを、アプリの初回起動時にダイアログか何か出して、質問するとします。
その結果を受けて、この関数を呼び出しておきます。

MainActivity.kt
class MainActivity : BaseActivity(), SelectPetDialog.SelectPetEventListener {
    ...
    val settingRepository: SettingRepository by inject()

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val hasPet = settingRepository.readPetDog()
        if (hasPet == null) {
            val dialog = SelectPetDialog()
            dialog.show(supportFragmentManager, null)
        }
    }

    /**
     * 犬を飼っているかの選択肢を送信
     */
    override fun onSelected(hasDog: Boolean) {
        analytics.setPetDogProperty(hasDog)
        settingRepository.savePetDog(hasDog)
    }

SelectPetDialogは簡単なので自分で書いてみてください。
リポジトリにはアップしておきます。

実行して、アンケートに[はい]と答えると、Debug Viewで次のように表示されました。

qiita11_10.png

Debug Viewの右下にある[現在アクティブなユーザープロパティ]にも、pet_dogが追加されました。

qiita11_11.png

(3)Audienceを作ってみる

  • プロジェクトの左側のメニューにAudiencesというのがあるので、そこをクリック

qiita11_12.png

すでにAll UsersというのとPurchasersというのが作成されていますね。これらのリストはデフォルトで必ず作成されます。

なお、購入レポートは、自動的に上がってくるはずで、その時、このPurchasersリストに「購入済みユーザー」として追加されます。
自動で収集されるイベントについては、以下を参考にしてください。
https://support.google.com/firebase/answer/6317485?hl=ja

以下、自分で作成する手順です。

  • [オーディエンス]をクリック
  • [カスタムオーディエンス]をクリック
  • [新しい...]と見えているドロップダウンリストをクリック
    • "新しい条件を追加"らしいです。マウスをホバリングさせておくとツールチップが出ます。
  • [ディメンション]-[登録済み]-[pet_dog]を選ぶ
qiita11_13.png
  • 任意の設定をする
    • 名称は日本語でも大丈夫そう
    • 説明も日本語可
    • 条件を、[完全一致 (=)] 、値=trueとする
    • 有効期間を、[上限に設定する]にする
    • 多分、「期間設定無し」、つまり無期限という意味だと思いますw
qiita11_15.png

FirebaseAnalyticsの仕様上、リストに該当するユーザー数が10名未満だと、詳細が表示されません。人数が少ないと個人が特定されやすくなってしまうからです。
なので、このリストにユーザーが追加されているのを確認するには、複数の端末でアプリを起動してアンケートに答えておく必要があります。エミュレーターなら、エミュレーターを作ってアプリを起動して設定後、いったんエミュレーターのデータをWipe Dataして再起動して、と同じことを繰り返すと、簡単にユーザー数を稼ぐことが出来ます。

qiita11_16.png

まあ、面倒ですけどね。
リストに入ったかちゃんと確認したい人は、是非やってみてください^^

エミュレーターの場合、起動する度にadb shell setprop debug.firebase.analytics.app パッケージ名しておくのを忘れないように。
実機の場合はネットにさえ繋がった状態でアプリをアンインストールしないで放置しておけばそのうち送信されるはずですが、気が短い人は端末をつなぎ替える度に同じようにセットをした方が良いでしょう(笑)

ちなみに、私はAndroid端末が4台あるのでそれらでやったのと、エミュレーターで6回ほど頑張ってみました。古いOSの端末で実行できるように、一時的にminSDKを16まで下げました。かなり重かったですが、なんとか動きました^^; (Firebase AnalyticsがminSDK16なので、それより下には下げられません)

12時間後の結果がこちらです。

qiita11_30.png

12回作業したようです(笑)
1人は「いいえ」にしてしまったようです。

(4)Audienceの利用

こうやって作ったAudienceは、例えばPush通知用のセグメンテーションに使えます。
まだPushを受け取るのは入れてないので、配信しても何も起きませんが、とりあえず作ってみましょう。
いずれpushの回はやるつもりですが、気が逸る方は、調べて入れてみて下さい。基本的には依存関係を1つ入れるだけです。ただ、それだと結構実運用には合わなかったりして細かい設定が必要になるので、別に回を設けてしっかりやるつもりです。

  • プロジェクトの左側のメニューから[Cloud Messaging]を選ぶ

qiita11_40.png

  • [Send your first message]をクリック
qiita11_41.png
  • 通知のタイトル、通知テキスト、通知名に任意のものを入力して、[次へ]をクリック
qiita11_42.png
  • ターゲット
    • [ユーザーセグメント]を選ぶ
    • [アプリ]で、任意のアプリをドロップダウンリストから選ぶ
    • [および]をクリック
    • [ユーザーオーディエンス]-[次を1つ以上含む]で、作成したオーディエンスを選ぶ
qiita11_44.png

とりあえずここまでです。今はpushを送っても何も起こりませんので^^;

「潜在なユーザーの○○パーセントがこのキャンペーンの対象になっています:人数」と表示されていて、どれくらいのユーザーに実際に通知が届くのかが分かりやすくなっていますね。

ただし、エミュレーターにいれてアンインストールしないでWipe Dataしたユーザーとかが母数に残るので、厳密な数字ではありません。あくまでも「目安」ですね。

こんな風にAudienceを活用してアプリの運用をしてユーザーに継続的に使ってもらうことを目指しましょう。

作りっぱなしのアプリは使って貰えませんよ^^;

(5)注意点

このUserPropertyAudience、実は結構使い勝手に工夫が必要なものです。
例えば、頻繁に代わるような属性にするのはあまり良くありません。リストがリフレッシュされるタイミングがあり、上手く拾えないことがあるからです。
また、あるAudienceを作っても、過去に遡って作成してくれません。Audienceを作成後、Analyticsを送ってきたユーザーに対して分類が行われるのです。

※更に昔は、あるAudience(当時はユーザーリストと呼んでいた)に入ったユーザーは、仮にアプリ内で属性を変えたとしても、二度とそのリストから抜けることが無かったんです。ただ、2018年頃の変更により、「動的なユーザーリスト」となったようで、これは解消されています。

また、UserPropertyは1プロジェクトに25までしか作成できないという制限もあります。更に作ったものは削除が出来ません。(アーカイブは出来るようになったようです)
Audienceは、50個まで。

よほどしっかりした設計の上で設定、使用しないと、簡単に破綻するのでご注意を。

Crashlyticsを導入する

1.アプリにCrashlyticsを設定する

(1)ルートbuild.gradleの変更

プロジェクトルート直下にあるbuild.gradleに以下を追加します。

root/build.gradle
    dependencies {
        // ...

        classpath 'com.google.gms:google-services:4.3.3'

        // 追加
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.0'
    }

(2)アプリのbuild.gradleの変更

app/build.graldleにプラグインを設定します。

  • ファイルの上の方に以下の記述を追加
app/build.gradle
apply plugin: 'com.google.firebase.crashlytics'

android{ ...
  • 依存関係に以下を追加
app/build.gradle
dependencies {
     ...
     implementation 'com.google.firebase:firebase-crashlytics:17.0.0'
}

Gradle Syncをしておきます。

2.強制的にクラッシュを送ってみる

(1)アプリで強制的にクラッシュさせる

普通にやってたんではなかなか意図的にクラッシュは起こせないので、ここは強制的にクラッシュさせます。

どこか任意の場所で、以下を呼びます。

    throw RuntimeException("Test Crash")

とりあえず先ほどの犬を飼ってるか聞くダイアログで、「いいえ」をタップしたらクラッシュするようにしてみました(どんな罠w)

MainActivity.kt
    /**
     * 犬を飼っているかの選択肢を送信
     */
    override fun onSelected(hasDog: Boolean) {
        analytics.setPetDogProperty(hasDog)
        if (!hasDog) {
            Crashlytics.getInstance().crash()
        }
        settingRepository.savePetDog(hasDog)
    }

[いいえ」を選択するとクラッシュしてセーブされないので次回起動時にまた聞かれるという(笑)

取り敢えず、実行してみます。
最初は静かに落ちるだけですが、2回以上クラッシュさせると、このようなポップアップが出ます。
これは別にCrashlyticsの機能ではなくて、AndroidOSの機能ですがね。

qiita11_17.png

コンソールログはこんな感じでスタックトレースが出ています。

    --------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: jp.les.kasa.sample.mykotlinapp.debug, PID: 7268
    java.lang.RuntimeException: Test Crash
        at jp.les.kasa.sample.mykotlinapp.activity.main.MainActivity.onSelected(MainActivity.kt:125)
        at jp.les.kasa.sample.mykotlinapp.alert.SelectPetDialog.onClick(SelectPetDialog.kt:52)
        at androidx.appcompat.app.AlertController$ButtonHandler.handleMessage(AlertController.java:167)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

(2)Firebaseコンソールで確認する

プロジェクトの左側のメニューに[Crashlytics]というのがあるのでそれをクリックします。

qiita11_18.png

ページ左上のドロップダウンリストから、デバッグ用のアプリを選びます。

qiita11_19.png

取り敢えず2回ほど起こしただけだと、「クラッシュ無し統計情報」は低下しましたが、「問題」の詳細が出てきません。

qiita11_20.png

少し時間をおいた方が良いのかも知れません。
30分位待ったらやっと出ました。
※アプリを次に再起動したときに送る、という説明があります。

qiita11_21.png

タップすると詳細が見られます。

qiita11_22.png

デバイス情報や、スタックトレースが見られます。

[ログ]タブを見てみて下さい。

qiita11_23.png

Analyticsのイベントが順番に送られていますね。なのでユーザーの行動を詳細にイベント送信していればしているほど、ここで障害発生手順が分かりやすくなるというわけです。

(3)カスタムログを送る

Crashlyticsでは、カスタムログを送ることが出来ます。
たとえば、今のクラッシュだと、「どこで」落ちたかは、スタックトレースから分かりますが、「なぜ」までは分かりません。
そこで、ダイアログの選択をした瞬間に、どっちを選んだかというカスタムログを送ることにします。

SelectPetDialog.kt
   override fun onClick(dialog: DialogInterface?, which: Int) {
        // 例外の原因を追いやすくするためどちらを選んだかユーザー操作をCrashlyticsに記録する
        FirebaseCrashlytics.getInstance().log("select_pet_dog = $which")  // (a)
        try {
            val listener = activity as SelectPetEventListener
            listener.onSelected(which == DialogInterface.BUTTON_POSITIVE)
        } catch (e: ClassCastException) {
            FirebaseCrashlytics.getInstance().recordException(e) // (b)
            Log.e(
                "SelectPetDialog",
                "Activity should implement ConfirmEventListener!!"
            )
        }
    }
  • (a)は、このあとクラッシュする可能性があるのでその原因となり得る選択肢を前もってログに入れています。

  • (b)は、アプリの実装上例外を握りつぶしているけど、Crashlyticsにはその報告を上げている例です。

デバッグ実行中はLog.eで吐いている内容で気付けば良いですが、もし見落としていた場合に、リリースアプリでも、Crashlyticsに上がってくれれば気付きやすいでしょう。

(a)のカスタムログが例外と一緒に送られたサンプルです。

qiita11_24.png

(b)のスルーした例外のログのサンプルです。一時的にMainActivityのSelectPetEventListenerの継承をやめ、コールバック関数はコメントアウトして実行しました。

qiita11_25.png

詳細には、ちゃんとスタックトレースがあります。

qiita11_26.png

なお、カスタムログは、例外発生箇所に近い場所で呼んでいると、Crasylyticsの情報収集に間に合わず無視されることがあるようです。(非同期でどこかに書き込んでいるからでしょう。それよりはスタックトレースを収集する方が優先度が高いから、だそうです)
なので、例外が発生するコードよりも物理的な処理順序が「それなりに前」に、ログを仕込んでおく、ということが重要になります。

それと、recordExceptionの方は、「次にFatal例外が起きてクラッシュ後、アプリを再起動したとき」に一緒に送られるようです。(なので他に例外が起きなければ報告に上がってきません。それってどうだろうという気もしますが・・・)

UserIdを送る

AnalyticsもCrashlyticsも、UserIdを使い、アプリ利用ユーザーを一意に特定することも可能です。例えば、個人情報保護法的な申し立てで、あるユーザーが「私のデータ消して下さい」と言ってきたときに、UserIdを特定して消すことが出来ます(※Googleに依頼する必要がありますが、その際にUserIdが分かっているとやりやすいようです)。

1.UserIdの生成

Hashidsというのを使ってみます。
https://hashids.org/
Kotlin版もあるようなのですがどこかのリポジトリに上がっているわけでないようで・・・
Java版が上がっていたのでそちらを利用することにします。
https://github.com/10cella/hashids-java

app/build.gradle
dependencies{
    implementation 'org.hashids:hashids:1.0.3'
}

(1)UserIdを作成する

Utils.ktに作ってみました。

Utils.kt
/**
 * 9文字のUserIdを作成する
 * SaltにはrandomUUIDを利用
 * ハッシュするソース元の数値は現在時刻を利用(ms)
 */
fun uniqueUserId(): String {
    val hashids = Hashids(UUID.randomUUID().toString(), 9)
    return hashids.encode(System.currentTimeMillis())
}

randomUUIDと、System.currentTimeMillis()を使うことで、予測されづらいUserIdの生成になっていると思います。
なぜ予測されにくくする必要があるかというと、これが推測しやすくなっていると、たとえばあるAPIのクエリーパラーメータに?userid=xxxxxxなどと付けているのが悪意のある攻撃者に分かった場合、このuseridを推測してAPIリクエストを投げつけて、ユーザー情報を盗み取ることが簡単にできてしまうからです。

(2)アプリケーションクラスで作成する

アプリ起動時に一度だけ作成します。
場所はMyAppクラスのonCreateにします。

MyApp.kt
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
            androidContext(this@MyApp)
            modules(appModules)
        }

        val analyticsUtil: AnalyticsUtil by inject()
        val settingRepository: SettingRepository by inject()
        // 一度だけUserIdを作成する
        val userId = settingRepository.readUserId() ?: uniqueUserId()
        analyticsUtil.setUserId(userId)
        FirebaseCrashlytics.getInstance().setUserId(userId)
        settingRepository.saveUserId(userId)
    }
}

Koinのインジェクションを利用してAnalyticsUtilSettingRepositoryを得ているので、必ずstartKoinの後に書きます。
設定ファイルに保存されたUserIdが無ければ新規作成し、Analytics, Crashlyticsそれぞれにセットして設定ファイルに保存しています。
settingRepository.readUserId() ?: uniqueUserId()?:は、左側の値がnullならば、右側の値を使う、というKotlinの文法です。覚えてますか?エルビス演算子といいます。

(3)実行結果

Debug Viewの結果です。
AnalyticsにユーザーIdがあることが分かります。

qiita11_31.png

アンインストールしてからもう一度起動すると、ユーザーIdが変わっているのが分かります。

qiita11_33.png

Crasylyticsの方は、クラッシュログの[データ]タブに出てきます。

qiita11_32.png

ご覧の通り、このUserIdは、アプリをアンインストールすると再生成されます。randomUUIDと現在時刻は当然変わってしまうため、その度に生成されるUserIdも変わってしまいます。
そうすと、アプリ内で新規ユーザーという扱いになるため、リセマラ(※)対策のようなものが必要になる場合があります。
ちょっと高度すぎるのでこのシリーズでは触れませんが、業務で作るアプリならば検討が必要になることもあるので、心に留めておいて下さい。

※リセマラ=リセットマラソン:アプリのアンインストール&再インストールを繰り返して、無料トライアルなどを延々と使い続ける方法。

また、このUserIDを普通に設定ファイルに保存していますが、可能ならば暗号化するなりした方が良いですね。ユーザー自身には開示しても問題ないですが、悪意のあるアプリからその値を読み取れてしまったら、それを元にユーザー情報を盗まれる原因になってしまいます。
基本的にはルート化されてないと設定ファイルは読み取れないはずですので、そこはユーザーの自己責任、と言ってしまうことも出来ますが。

テスト

1.UnitTest

どうやらRobolectricのテストに影響があるようで、Fireabse Analyticsの初期化を少し変更する必要があります。

FirebaseApp.initializeAppが必要とエラーログに出ていたので、対応します。
多分普通にアプリを実行するときは中でよしなに自動的にされるのでしょうが、Robolectricはアプリを起動しませんから、そこでなにか不整合が起きてしまうようです。

AnalyticsUtil.kt
class AnalyticsUtil(app: Application) {

    private val firebaseAnalytics by lazy { FirebaseAnalytics.getInstance(app) }

    init {
        FirebaseApp.initializeApp(app)
    }

init {}はコンストラクタに続けて行われる初期化関数です。

by lazyは、最初に変数にアクセスがあったときに実行して初期化するという初期化方法になります。

これで実際のアプリ起動にも影響は無さそうなので、これで行くことにします。

2.CIツールでのテスト

CIを回すようになっている場合、このままpushすると、恐らくビルドが通らないのでは無いかと思います。

プロジェクトルートにある、.gitignoreを見てみてください。

※Macで隠しファイルが見えない場合は、Shift+Command+.ショートカットキーを押して下さい。

# Google Services (e.g. APIs or Firebase)
google-services.json

そう、無視ファイルにデフォルトで追加されているんですね。
ということは、publicなリポジトリなどには上げない方が無難なファイルということです。
privateなら上げてしまっていてもいいかもしれませんが。

そこで、どうしたらいいでしょうか?

このプロジェクトでメインで運用していこうと思っている、Github Actionsの場合で考えます。恐らく、他のCIでも同じようなアプローチで出来るのでは無いかと思います。

Github ActionsでReleaseビルド用に証明書ファイルを設定したのと同じ方法を採ります。

つまり、google-services.jsonのBase64エンコードした文字列をSecretsに設定しておき、それをビルド前にデコードしてファイルに書き出しておく、ということになります。
早速やってみましょう。

1.google-services.jsonのBase64エンコードした文字列を取得

ターミナルでプロジェクトルートにいるものとします。

$ cd app
$ openssl base64 -in google-services.json -out base64file.txt

2.GithubのSecretsに保存する

Githubのリポジトリの[Setttings]タブから、[Add a new secret]します。

qiita11_34.png

先ほど出力したBase64文字列を貼り付け、任意の名前をつけて[Add secret]します。

qiita11_35.png

出力したbase64file.txtは不要なので削除しておきましょう。(間違ってコミットされないように)

3.ビルドスクリプトの設定

decodeしてファイルとして配置するスクリプトを、.github/workflow/xxxx.yamlに追加します。

    - name: copy google-service
      env:
          GOOGLE_SERVICE: ${{ secrets.GOOGLE_SERVICES_JSON }}
      run: echo $GOOGLE_SERVICE | base64 --decode > ./app/google-services.json

これを./gradlew assembleDebugなどの前に置きます。

コミットして、pushしてワークフローを動かしてみて下さい。

なお、CircleCI用の設定ファイルもリポジトリにはアップしてありますので参考にして下さい。
※CircleCIはエミュレーターテストはありません。(出来ない)

まとめ

Firebaseを設定して、AnalyticsとCrashlyticsを利用できるようになりました。
また、UserIdをそれぞれに設定することで、ユーザーを一意に特定することが出来るようになりました。

ここで特定しているのは、アプリである行動をしているユーザーが同一ユーザーであるかどうかだけで、リアル世界での個人を特定出来るわけではありません。
ただ、同じ情報を利用して、問い合わせや特典の付与などに使っていけますね。
これらを利用して、アプリの継続利用をしてもらうための効果的な施策を考えてアプリを運用していくことが、「使って貰える」アプリの条件となってくることがあります。ただし、やりすぎると「うるさいアプリだなあ」と通知を切られたり、アンインストールされてしまうことにも繋がってしまいます。なので、ご利用は計画的に。

おまけ(Analaytics設計の重要性について)

Analyticsを送っていて、例えば、

「Instagramシェア画面はみんな使ってるけど、Twitterシェア画面はほとんど使われてないな」

なんてことが分かれば、将来、Twitter側の仕様変更などで対応が必要になったとき、機能自体を無くしてしまう、なんていう判断に使えます。

ボタンタップをいちいち送るのも、「どこまではタップされていて、どこからユーザーが遷移をやめてしまうのか」などということが取れて分析できるようになると、UIの改善だったり何かイベントを打ったりするのに役立てることが出来ます。

それから一番強力なのは、UserPropertyと組み合わせてAudienceのリストを作り、該当ユーザーにpushを送ったりする、という使い方が出来ることです。
しかし、このAudienceリストが出来上がるまでには、24Hくらいかかります(2年ほど前の体感)。利用するためには、かなり前もってリストが出来るように、アプリを作っておかなければならないのです。

つまり、「何をどういう形で送っておくか」というのは、実はアプリ開発の初期から綿密な設計をしておかないと、後でとても苦労するということになりかねません。
特にAnalyticsを利用して色々やろうとしている場合には、最初から細かく取っておかないと、「あの値が取れてないと分析できないよ!」となるとアプリを改修してリリースする時間がかかる上に、Firebaseのレポートに上がるまでに時間差があるため、結構なロスになってしまいます。UserPropertyとAudienceを使う場合は特にです。

お仕事の現場では、iOS版も同時に開発していることがあると思いますが、iOS版と送っているパラメータが合ってないなど齟齬が生じてしまうと、これまたのちのち響いてきます。

なので、Analyticsの設計は実は結構大事なのです。
勉強のためだけなら、今回送ったような内容だけで十分ですが、アプリをリリースすることを考えているときには、「運用フェーズでどうしていきたいか」までを計画して設計しておいたほうが絶対良いので、心に留めておいてください。

注意

AnalyticsとCrashlyticsだけなら問題は起こりにくいですが、Firestoreなどストレージを使うタイプのものは、FreeのSparkプランだと上限があり超過するとストップしてしまいますし、Blazeプランだと従量課金されていつの間にかウン百万!なんて話も聞きます。
ご利用は計画的に。

予告

次回は、Firebase Authenticationでユーザー作成と認証を行います。

参考ページなど

Hashids関係
https://qiita.com/peutes/items/d88f6fea7ca440b28d7c
https://www.programcreek.com/java-api-examples/index.php?api=org.hashids.Hashids

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2