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:

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 を追加する

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

image.png

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

「Add FCM to your App」したあとの app/build.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

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

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

AndroidManifest.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

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

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

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

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

image.png

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

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

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

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

  1. で追加された 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

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

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

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

com.google.gms.google-services を削除した app/build.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:

// 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:

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

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

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

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

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 xxxxxxxx

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

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

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

環境変数を埋め込んだ app/build.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:

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:

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'
}

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

最後に

プロジェクト全体は

に。