Firebase を最新の環境で使用する【2018年6月版】

Firebase Cloud Messaging を最新の開発環境で使おうとしたらいろいろハマったので手順をまとめてみました。

だらだら長くなったので短くまとめると、

* ``com.google.firebase:firebase-messaging:17.0.0`` と ``apply plugin: 'com.google.gms.google-services'`` が仲が悪い
* ので ``apply plugin: 'com.google.gms.google-services'`` を使わないようにした
* それに伴い ``google-services.json`` も使わないようにした
* Firebase のプロジェクト設定は環境変数から埋め込むようにした

です。


## 1. とりあえず Android Studio で新規プロジェクトを作る

* Android Studio のバージョンは 3.1.2

* :heavy_check_mark: Include Kotlin support
* Targeting API 23 and later
* :heavy_check_mark: Backward Compatibility(AppCompat)

**プロジェクト削除直後の ``app/build.gradle``:**

```gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "net.yourdomain.fcmsample"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
```

## 2. プロジェクトに Firebase を追加する

* [Android プロジェクトに Firebase を追加する  |  Firebase](https://firebase.google.com/docs/android/setup?hl=ja)

の手順をトレース。Firebase Cloud Messaging(FCM) が使いたかったので "Cloud Messaging" から開始。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8227/0a783607-0793-764b-9bed-0c350b788a9c.png)

「Connect to Firebase」と「Add FCM to your App」をやります。

**「Add FCM to your App」したあとの ``app/build.gradle`` :**

```gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "net.yourdomain.fcmsample"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    implementation 'com.google.firebase:firebase-messaging:11.8.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

apply plugin: 'com.google.gms.google-services'
```

``implementation 'com.google.firebase:firebase-messaging:11.8.0'`` と ``apply plugin: 'com.google.gms.google-services'`` が追加された。**後者は後で消します**。

その他、ルートの ``build.gradle`` に ``classpath 'com.google.gms:google-services:3.1.1'`` が、``app`` ディレクトリに ``google-services.json`` が追加される、 **どちらも後で消します**。 特に ``google-services.json`` は、プロジェクトIDやAPI Keyなどの秘匿情報が含まれるので git にコミットしない方がよいです。

gradle sync を行うと、なんか取り消し線が付いたり、赤線が付いたり、Warning が出たりするけど、synced successfully が出て成功はしました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8227/ba6d8bae-48eb-1eea-62bc-6fb28cedf374.png)

## 3. FCM をとりあえず使うところまで実装する

* [Android に Firebase Cloud Messaging クライアント アプリを設定する  |  Firebase](https://firebase.google.com/docs/cloud-messaging/android/client?hl=ja)

に従い、必要なクラスを追加、 ``AndroidManifest.xml`` への設定を行います。

**AndroidManifest.xml**

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="net.yourdomain.fcmsample">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <service
            android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>

        <service
            android:name=".MyFirebaseInstanceIDService">
            <intent-filter>
                <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
            </intent-filter>
        </service>
    </application>
</manifest>
```

**MyFirebaseMessagingService.kt**

```kotlin
package net.yourdomain.fcmsample

import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage?) {
        // FCMメッセージを受信したときに呼び出される

        // 通知メッセージの受信
        if (remoteMessage?.notification != null) {
            val notification = remoteMessage.notification
            val title = notification?.title ?: ""
            val body = notification?.body ?: ""

            android.util.Log.d("FCM-TEST", "メッセージタイプ: 通知\nタイトル: $title\n本文: $body")
        }
    }
}
```

**MyFirebaseInstanceIDService.kt**

```kotlin
package net.yourdomain.fcmsample

import android.util.Log
import com.google.firebase.iid.FirebaseInstanceId

import com.google.firebase.iid.FirebaseInstanceIdService

class MyFirebaseInstanceIDService : FirebaseInstanceIdService() {
    override fun onTokenRefresh() {
        // トークンが更新されたときに呼び出される
        val refreshedToken = FirebaseInstanceId.getInstance().token ?: ""
        Log.d("FCM-TEST", "Refreshed token: $refreshedToken")
    }
}
```

**MainActivity.kt**

```kotlin
package net.yourdomain.fcmsample

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.google.firebase.iid.FirebaseInstanceId

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val refreshedToken = FirebaseInstanceId.getInstance().token ?: ""
        Log.d("FCM-TEST", "Refreshed token: $refreshedToken")
    }
}
```

ビルドして実行します。
ここから先は実機が必要でしょうか。
Play services 入りのエミュレータでは、``E/FirebaseInstanceId: Token retrieval failed: SERVICE_NOT_AVAILABLE`` というエラーが出てしまいました(Google Maps API は使えるんですけど…)。

実機で動かすと、初回は ``MyFirebaseInstanceIDService`` 、次回以降は ``MainActivity`` で FCM のトークンが Logcat に出力されます。

```
D/FCM-TEST: token = dRkrWdZqARg:APA91bFXU0-Z7OdL4UfyLmbRHhBq2w4OTMNncbFeIgJuqWt6EJVsRgNKUrdGGG3LZRjT9PLTGokKqb7_w4iRm1gl0Vydy77kHkkdKy0lZvNr1uNDXkaMKaiWX5n1YaxlvFtZDNYtO2Eq
```

な感じのやたら長い文字列です(ちなみに上の文字はフィクションです)。
Logcat に出力されたトークンをコピーしておきます。

## 4. メッセージを送って、受信してみる

Android アプリは起動しっぱなしにしておいてください、とりあえず。

https://console.firebase.google.com/ からプロジェクト「FcmSample」を開き、「Notification」の「使ってみる」をクリックします（なんか別途 「Cloud Messaging」もありますがトラップ？）。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8227/4cdb45b4-8ef7-b950-bac2-4115040335bd.png)

さらに「最初のメッセージを送信する」をクリックして、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8227/8a3ca86b-a07d-e028-6629-022c390445fa.png)

のように設定します。
ターゲットは「単一の端末」にして、トークンの欄にコピーしておいた文字列を貼り付けます。

で、「メッセージ送信」を押すと、ただちにメッセージが送信され、起動させておいた Androidアプリの ``MyFirebaseMessagingService`` が受信して、次のようなログを出力します。

```
D/FCM-TEST: メッセージタイプ: 通知
    タイトル: 
    本文: はろー
```

## 5. Firebase のライブラリを最新にする

2. で追加された Firebase のライブラリのバージョンは 11.8.0 でしたが、2018年6月現在の最新は **17.0.0** です。

ので ``app/build.gradle`` に記述されているバージョンを **17.0.0** に変更して「Sync now」します。

すると見事に **Failed to resolve: com.google.firebase:firebase-core:17.0.0** というエラーになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8227/262c7f9c-fcfa-3f3e-446f-febffca87395.png)

小一時間ほど消費していろいろ調べた結果、最終行の ``apply plugin: 'com.google.gms.google-services'`` が無ければエラーにならない、ことが判りました（ついでに Warning されてた "Configulation 'compile' is obsolete and ..." も出なくなります）。

``apply plugin: 'com.google.gms.google-services'`` は、

* [Google Developers Japan: Android での Firebase の初期化を使いこなす](https://developers-jp.googleblog.com/2017/04/take-control-of-your-firebase-init-on.html)

によると、 ``google-services.json`` からプロジェクト情報を読み込んで、自動的にアプリに設定してくれるプラグイン、であると理解できます。
オーケー、ここで全部削除して設定は手動で行いましょう。

**com.google.gms.google-services を削除した ``app/build.gradle``:**

```gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "net.yourdomain.fcmsample"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    implementation 'com.google.firebase:firebase-messaging:17.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
```

**com.google.gms.google-services を削除したルートの ``build.gradle``:**

```gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.2.41'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

``google-services.json`` から自動で読み込まれていた設定を手動で行うには、``FirebaseApp.initializeApp(Context, FirebaseOptions)`` を起動時に呼び出します。``android.app.Application`` クラスを拡張した ``MyApplication`` クラスを作って、 ``onCreate`` で行うのが一般的でしょう。**``MyApplication`` を ``AndroidManifest.xml`` へ追加するのを忘れずに**

ちなみに ``FirebaseApp.initializeApp`` の呼び出しがされないと、``FirebaseInstanceId.getInstance()`` を使用したときに、

```
Default FirebaseApp is not initialized in this process net.yourdomain.fcmsample. Make sure to call FirebaseApp.initializeApp(Context) first.
```

という例外がでます。

FCM を使うだけなら、次のように「ApplicationID」と「APIKey」を設定すればよいようです。

**Firebaseの初期化を行う ``MyApplication.kt``:**

```kotlin
package net.yourdomain.fcmsample

import android.app.Application
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        FirebaseApp.initializeApp(this, FirebaseOptions.Builder()
                .setApplicationId("<application_id>")
                .setApiKey("<api_key>")
                .build())

    }
}
```

「ApplicationID」と「APIKey」は ``google-services.json`` にも記述されていますが、それに頼らず Firebase のサイトからも確認できます。

https://console.firebase.google.com/ からプロジェクト「FcmSample」を開き、Project Overview → プロジェクトの設定 で表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8227/51dcfd8e-7ba7-c589-875a-1dccb45b6436.png)

サイトから得た「ApplicationID」と「APIKey」をソースコードにベタ貼りすると、それもよくないので、外出ししましょう。

いくつか方法がありますが、

* [Gradle tips and recipes｜Android Developers](https://developer.android.com/studio/build/gradle-tips#share-custom-fields-and-resource-values-with-your-app-code)
* [Environment Variables - Visual Studio App Center | Microsoft Docs](https://docs.microsoft.com/en-us/appcenter/build/custom/variables/#buildgradle-for-android)
* [AndroidStudioに環境変数を渡す - Qiita](https://qiita.com/tmiyamon/items/ed660dff7846f5ec95d3)

に従ってみます。
この仕組みは、

A. システム環境変数に APP_ID と API_KEY を作成する
B. gradle を使って、 **ビルド時** に、1. の値をソースコード(の ``BuildConfig`` クラス)に埋め込む
C. 実装は ``BuildConfig.APP_ID``, ``BuildConfig.API_KEY`` を使う

です。

### A. システム環境変数に APP_ID と API_KEY を作成する

mac の Terminal だと ``launchctl setenv`` を使います。
環境変数名はプロジェクト名を接頭辞にして ``FCMSAMPLE_APP_ID``、``FCMSAMPLE_API_KEY`` としました。

```
launchctl setenv FCMSAMPLE_APP_ID 1:7618378357:android:70bef98060071fe6
launchctl setenv FCMSAMPLE_API_KEY AIzaSyDIUooI-LGeKAAaT6lf8sQEAemJcGx7R4s
```

こんな感じで。
**実行後、Android Studio を再起動します**。しないと環境設定値が反映されませんでした。

### B. gradle を使って、``BuildConfig`` クラスに埋め込む

``app/build.gradle`` を次のように編集し、``FCMSAMPLE_APP_ID``、``FCMSAMPLE_API_KEY`` を埋め込みます。

**環境変数を埋め込んだ ``app/build.gradle``:**

```gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "net.yourdomain.fcmsample"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            buildConfigField("String", "APP_ID", "\"${System.env.FCMSAMPLE_APP_ID}\"")
            buildConfigField("String", "API_KEY", "\"${System.env.FCMSAMPLE_API_KEY}\"")
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField("String", "APP_ID", "\"${System.env.FCMSAMPLE_APP_ID}\"")
            buildConfigField("String", "API_KEY", "\"${System.env.FCMSAMPLE_API_KEY}\"")
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    implementation 'com.google.firebase:firebase-messaging:17.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
```

``buildTypes`` に ``buildConfigField("String", "APP_ID", "\"${System.env.FCMSAMPLE_APP_ID}\"")`` などを追記しています。注意点は、``String`` は大文字(Javaのクラス名なので)にすること、第二引数が文字列なら ``\"`` で囲むこと、です。特に後者を忘れると、ダブルコートなしの文字列リテラルが埋め込まれてエラーになります。

### C. 実装は ``BuildConfig.APP_ID``, ``BuildConfig.API_KEY`` を使う

最後に、この buildConfig を使用するように ``MyApplication.kt`` を変更します。

**BuildConfigを使用するように変更した MyApplication.kt:**

```kotlin
package net.yourdomain.fcmsample

import android.app.Application
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        FirebaseApp.initializeApp(this, FirebaseOptions.Builder()
                .setApplicationId(BuildConfig.APP_ID)
                .setApiKey(BuildConfig.API_KEY)
                .build())
    }
}
```

手順が成功していれば、``BuildConfig.APP_ID`` や ``BuildConfig.API_KEY`` には、A. で設定した環境変数の値が入っているはずです。失敗していれば ``null`` になります。

この仕組みは大抵の CIサービスでも対応しているので、リリース時には CIサービス の設定で環境変数を設定してあげれば埋め込まれるはずです。

## DataBinding、kapt, coroutine, AAC, Support Libs を追加する

ここまで来たらやってしまえ、ということで

* DataBinding
* kapt
* Kotlin coroutine - 0.22.5
* Android Architecture Components(AAC) - 1.1.0 〜 1.1.1
* Android Support Library - 27.1.1

も追加してみた。

**諸々追加した ``app/build.gradle``:**

```gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "net.yourdomain.fcmsample"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            buildConfigField("String", "APP_ID", "\"${System.env.FCMSAMPLE_APP_ID}\"")
            buildConfigField("String", "API_KEY", "\"${System.env.FCMSAMPLE_API_KEY}\"")
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField("String", "APP_ID", "\"${System.env.FCMSAMPLE_APP_ID}\"")
            buildConfigField("String", "API_KEY", "\"${System.env.FCMSAMPLE_API_KEY}\"")
        }
    }

    dataBinding {
        enabled = true
    }
}

dependencies {
    kapt 'com.android.databinding:compiler:3.1.2'

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"

    def coroutines_version = '0.22.5'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

    // Android Support Libraries
    implementation 'com.android.support:design:27.1.1'
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:cardview-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'

    // AAC
    implementation 'android.arch.lifecycle:extensions:1.1.1'
    implementation 'android.arch.persistence.room:runtime:1.1.0'
    annotationProcessor "android.arch.lifecycle:compiler:1.1.1"
    annotationProcessor "android.arch.persistence.room:compiler:1.1.0"

    // FCM
    implementation 'com.google.firebase:firebase-messaging:17.0.0'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
```

とりあえずエラーにはならなかったので、依存関係のトラブルは大丈夫みたいです。

## 最後に

プロジェクト全体は

* https://github.com/amay077/FcmSample201806

 に。