21 February 2018

[Android Code] รู้จักกับ google-services.json เมื่อต้องใช้งาน Google API และ Firebase



        เดี๋ยวนี้เวลานักพัฒนาคนไหนจะใช้ Google API สำหรับแอนดรอยด์หรือ Firebase ก็คงคุ้นเคยกับไฟล์ที่ชื่อว่า google-services.json กันแน่นอน เพราะว่าผู้ที่หลงเข้ามาอ่านจะต้องเอาไฟล์นี้ไปใส่ไว้ในโปรเจคทุกครั้งที่จะเรียกใช้งาน Google API ซักตัวที่รองรับบนแอนดรอยด์โดยตรง หรือจะเรียกใช้งานบางอย่างใน Firebase

เริ่มเรื่องราวด้วย Google Services Gradle Plugin

        ในยุคสมัยที่ยังเขียนแอปฯบน Eclipse กันอยู่ มันยังไม่มีหรอกไฟล์ google-services.json เนี่ย ในตอนนั้นต้องเอา API Key ต่างๆไปใส่ไว้ใน Android Manifest ถึงจะใช้งาน Google API สำหรับแอนดรอยด์ได้ (ตอนนั้นยังไม่มี Firebase)

        แต่เมื่อยุคสมัยเปลี่ยน Eclipse จากไป เปลี่ยนใหม่เป็น Android Studio ที่มี Gradle คู่ใจที่ใครๆหลายคนชื่นชอบ (กับความช้าตอน Build ของมัน) ก็กลายเป็นเครื่องมือสำคัญที่ทำให้วิธีการเรียกใช้งาน Library หลายๆตัวที่เดิมทีต้องประกาศอะไรให้เยอะแยะ กลับกลายเป็นเรื่องง่ายขึ้นสำหรับผู้เรียกใช้งาน เพียงแค่ประกาศคำสั่งตัวหนึ่งไว้ใน build.gralde แบบนี้

// build.gradle (Project)

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.gms:google-services:3.2.0'
    }
}
...

         บ่อยครั้งที่เจ้าของบล็อกเรียกใช้งาน Plugin หรือ Library บางตัวไว้ในโปรเจค มักจะต้องเพิ่มคำสั่งอะไรทำนองนี้ไว้ใน build.gradle ทุกครั้ง โดยไม่ได้สนใจว่ามันคืออะไร ใส่ไปทำไม รู้แค่ว่าใส่แล้วใช้งานได้ก็พอ

         คำสั่งที่ใส่ไว้ในตัวอย่างข้างบนนี้คือการเพิ่ม Google Services Gradle Plugin เข้าไปในโปรเจค ว่าแต่มันทำอะไรบ้างล่ะ?

Google Services Gradle Plugin ทำงานอยู่ 2 ส่วนด้วยกัน

อย่างแรก : ช่วยจัดการ Dependencies ของ Google Play Services และ Firebase

        อย่างแรกเลยคือตัวมันจะทำหน้าที่ใส่ Dependencies พื้นฐานของ Google API Services ไว้ใน Module ที่ประกาศ Plugin ของ Google API Services ไว้ดังนี้

// build.gradle (App Module)

...

android {
    ...
    defaultConfig {
        ...
    }
    buildTypes {
        ...
    }
}

dependencies {
    ...
}

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

        ให้ประกาศไว้ข้างล่างสุดนะ ถ้าประกาศไว้ข้างบนจะไม่มีผลอะไร 

         Plugin ตัวนี้ก็จะเพิ่ม Library ของ Firebase Core ไว้ในโปรเจคให้โดยอัตโนมัติ สามารถเช็คได้โดยใช้คำสั่ง Gradle ผ่าน Terminal แบบนี้

// Linux / Mac OS
./gradlew app:dependencies

// Windows
gradlew app:dependencies

       ก็จะเห็นว่า Firebase Core ถูกเพิ่มเข้ามาในโปรเจค

com.google.firebase:firebase-core:11.4.2
    \--- com.google.firebase:firebase-analytics:11.4.2
         +--- com.google.android.gms:play-services-basement:11.4.2
         |    +--- com.android.support:support-v4:25.2.0
         |    \--- com.google.android.gms:play-services-basement-license:11.4.2
         +--- com.google.firebase:firebase-common:11.4.2
         |    +--- com.google.android.gms:play-services-basement:11.4.2
         |    +--- com.google.android.gms:play-services-tasks:11.4.2
         |    |    +--- com.google.android.gms:play-services-basement:11.4.2
         |    |    \--- com.google.android.gms:play-services-tasks-license:11.4.2
         |    \--- com.google.firebase:firebase-common-license:11.4.2
         +--- com.google.firebase:firebase-analytics-impl:11.4.2
         |    +--- com.google.android.gms:play-services-basement:11.4.2
         |    +--- com.google.firebase:firebase-iid:11.4.2
         |    |    +--- com.google.android.gms:play-services-basement:11.4.2
         |    |    +--- com.google.firebase:firebase-common:11.4.2
         |    |    \--- com.google.firebase:firebase-iid-license:11.4.2
         |    +--- com.google.firebase:firebase-common:11.4.2
         |    +--- com.google.android.gms:play-services-tasks:11.4.2
         |    \--- com.google.firebase:firebase-analytics-impl-license:11.4.2
         \--- com.google.firebase:firebase-analytics-license:11.4.2

        แต่ในกรณีที่นักพัฒนาใส่ Dependencies ของ Firebase เข้าไปโดยตรง ตัว Plugin ก็จะอิงเวอร์ชันตามเวอร์ชันที่นักพัฒนากำหนดลงไปแทน

...
dependencies {
    ...
    implementation 'com.google.firebase:firebase-analytics:11.6.2'
}

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

         Dependencies ในโปรเจคก็จะมี Firebase Analytics เป็นเวอร์ชัน 11.6.2 แทนของเก่าที่เป็น 11.4.2

        ในขณะเดียวกันถ้าเกิดเรียกใช้ Dependencies ของ Firebase แต่ละตัวเข้าไปในโปรเจคแต่ว่ากำหนดเวอร์ชันต่างกัน ตอนที่ Build Gradle ก็จะมีการแจ้งเตือนจาก Plugin ตัวนี้ว่ามีการกำหนดเวอร์ชันต่างกัน

// build.gradle (App Module)

...
dependencies {
    ...
    implementation 'com.google.firebase:firebase-auth:11.4.2'
    implementation 'com.google.firebase:firebase-analytics:11.6.2'
}

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

        ผลที่ได้

// Message Gradle Build

Error:Execution failed for task ':app:processDebugGoogleServices'.
> Please fix the version conflict either by updating the version of the google-services plugin (information about the latest version is available at 
  https://bintray.com/android/android-tools/com.google.gms.google-services/) or updating the version of com.google.android.gms to 11.6.2.

         เพราะถ้าไม่ใส่ Google Services Gradle Plugin ไว้ มันก็จะไม่มีแจ้งเตือนอะไร ในตอน Build Gradle แต่จะไปปวดหัวตอนจะ Build APK แล้วเจอเออเรอร์แบบนี้แทน

// Message Gradle Build

Error:Execution failed for task ':app:transformDexArchiveWithExternalLibsDexMergerForDebug'.
> java.lang.RuntimeException: java.lang.RuntimeException: com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex

        ซึ่งข้อความนี้ค่อนข้างกว้างเกินไป กว่าจะรู้ได้ว่าเป็นที่อะไรก็ต้องมานั่งไล่เช็ค Dependencies ทีละตัว ซึ่งลำบากกว่ามากเมื่อเทียบกับข้อความที่แจ้งเตือนมาจาก Google Services Gradle Plugin

อย่างที่สอง : อ่านข้อมูลในไฟล์ google-services.json และเตรียม Resource ที่จำเป็น

        ถ้าลองเปิดไฟล์ google-services.json ดู ก็จะพบว่าในนั้นเก็บค่าต่างๆที่ใช้ในการติดต่อกับ Google API for Android หรือ Firebase ที่มีลักษณะแบบนี้

{
  "project_info": {
    "project_number": "728195012345",
    "firebase_url": "https://sleeping-for-less.firebaseio.com",
    "project_id": "sleeping-for-less-12345",
    "storage_bucket": "sleeping-for-less.appspot.com"
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "1:728195012345:android:123438043d123456",
        "android_client_info": {
          "package_name": "com.akexorcist.sleepingforless"
        }
      },
      "oauth_client": [
        {
          "client_id": "728195012345-awesome.apps.googleusercontent.com",
          "client_type": 3
        }
      ],
      "api_key": [
        {
          "current_key": "1234SyANly_L0Jo12345ltQkVuw1Yj1234567"
        }
      ],
      "services": {
        "analytics_service": {
          "status": 1
        },
        "appinvite_service": {
          "status": 1,
          "other_platform_oauth_client": []
        },
        "ads_service": {
          "status": 2
        }
      }
    }
  ],
  "configuration_version": "1"
}

        โดย Google Services Gradle Plugin จะอ่านไฟล์นี้แปลงเป็นโค้ดแล้วใส่ลงไปในโปรเจคให้โดยอัตโนมัติ

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

    ...

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

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

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

    <uses-permission
        android:name="com.google.android.c2dm.permission.RECEIVE" />

    <permission
        android:name="com.akexorcist.sleepingforless.permission.C2D_MESSAGE"
        android:protectionLevel="0x2" />

    <uses-permission
        android:name="com.akexorcist.sleepingforless.permission.C2D_MESSAGE" />

    <application
        ...>

        ...

        <receiver
            android:name="com.google.android.gms.measurement.AppMeasurementReceiver"
            android:enabled="true"
            android:exported="false" />

        <receiver
            android:name="com.google.android.gms.measurement.AppMeasurementInstallReferrerReceiver"
            android:permission="android.permission.INSTALL_PACKAGES"
            android:enabled="true"
            android:exported="true">

            <intent-filter>

                <action
                    android:name="com.android.vending.INSTALL_REFERRER" />
            </intent-filter>
        </receiver>

        <service
            android:name="com.google.android.gms.measurement.AppMeasurementService"
            android:enabled="true"
            android:exported="false" />

        <service
            android:name="com.google.android.gms.measurement.AppMeasurementJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:enabled="true"
            android:exported="false" />

        <activity
            android:theme="@ref/0x01030010"
            android:name="com.google.android.gms.common.api.GoogleApiActivity"
            android:exported="false" />

        <receiver
            android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
            android:permission="com.google.android.c2dm.permission.SEND"
            android:exported="true">

            <intent-filter>

                <action
                    android:name="com.google.android.c2dm.intent.RECEIVE" />

                <category
                    android:name="com.akexorcist.sleepingforless" />
            </intent-filter>
        </receiver>

        <service
            android:name="com.google.firebase.iid.FirebaseInstanceIdService"
            android:exported="true">

            <intent-filter
                android:priority="-500">

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

        <provider
            android:name="com.google.firebase.provider.FirebaseInitProvider"
            android:exported="false"
            android:authorities="com.akexorcist.sleepingforless.firebaseinitprovider"
            android:initOrder="100" />

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@ref/0x7f080004" />
    </application>
</manifest>

        เมื่อเทียบกับสมัยตอนยังใช้ Eclipse อยู่ ตอนนั้นนักพัฒนาจะต้องมานั่งใส่ค่าตรงนี้เอง ในขณะที่ Google Services Gradle Plugin ช่วยทำให้เสร็จสรรพ โดยที่เอาไฟล์ google-services.json มาวางใน Directory ที่กำหนดเท่านั้นพอ

        ซึ่ง Key ต่างๆที่อยู่ใน google-services.json จะถูกนำมาแปะไว้ใน Android Manifest ให้โดยอัตโนมัติ และมีการเตรียม Resource สำคัญอย่าง Layout XML สำหรับแสดงเมื่อเครื่องนั้นไม่ได้ติดตั้ง Google Play Services ไว้ในเครื่องรวมไปถึงข้อความที่ใช้ในการแสดงผล

มีแค่บาง​ Service ใน Google API for Android ที่ต้องใช้ google-services.json

        ในกรณีที่เป็นการเรียกใช้งาน Firebase การกำหนดค่าต่างๆจะอยู่ใน google-services.json ทั้งหมด แต่ถ้าเป็น Google API for Android จะมีเฉพาะบาง Service เท่านั้นที่จำเป็นต้องใช้

        • Google Sign-in
        • Google Analytics
        • Google Cloud Messaging

        นอกเหนือจากนี้ก็ต้องไปกำหนดเองจ้า

ไฟล์ google-services.json กับ Build Variant 

        โดยปกติแล้วไฟล์ google-services.json จะใส่ไว้ใน App Module ของโปรเจคนั้นๆ

SleepingForLess
   +--- app
   |    +--- build
   |    +--- libs
   |    +--- build.gradle
   |    +--- google-services.json
   |    \--- proguard-rules.pro
   +--- build
   +--- gradle
   +--- build.gradle
   +--- gradle.properties
   \--- ...

         แต่ถ้าเกิดว่าโปรเจคทำ Build Variant ไว้หลายๆแบบล่ะ? จะต้องทำอะไรกับไฟล์ google-services.json บ้างหรือป่าว?

หลาย Build Variant แต่อยู่บนโปรเจคตัวเดียวกันใน Firebase Console / Google API Console

        ในกรณีนี้ไม่ต้องทำอะไรเพิ่มเติม นอกจากว่าแต่ละ Build Variant มีการกำหนด Suffix ของ Package Name แตกต่างกัน ก็ให้ไปเพิ่ม Package Name ให้ครบทุกแบบใน Firebase Console / Google API Console ซะ


        ซึ่งการแยก Package Name เป็นหลายๆตัวในโปรเจคเดียวกัน จะยังคงใช้ Key ตัวเดียวกันทั้งหมดอยู่ดี ที่ต้องทำต่อก็แค่ดาวน์โหลดไฟล์ google-services.json จากหน้า Console แล้วเอาไปวางทับแทนที่ของเก่าเท่านั้นเอง

หลาย Build Variant และแยกโปรเจคบน Firebase Console / Google API Console ออกจากกัน

         ในบางโปรเจคอาจจะต้องการวางไฟล์ google-services.json แยกตามแต่ละ Product Flavor หรือ Build Type ซึ่งตัว Google Services Gradle Plugin ก็ทำมาให้รองรับกรณีนี้อยู่แล้ว (ตั้งแต่เวอร์ชัน 2.2.0 ขึ้นไป) โดยตัว Plugin จะค้นหาไฟล์ google-services.json ที่แยกตามแต่ละ Product Flavor หรือ Build Type ให้ด้วย

SleepingForLess
   +--- app
   |    +--- src
   |    |    +--- debug
   |    |    |    \--- google-services.json
   |    |    +--- release
   |    |         +--- dev
   |    |         |    \--- google-services.json
   |    |         \--- prod
   |    |              \--- google-services.json
   |    +--- google-services.json
   |    \--- ...
   \--- ...

        จากตัวอย่างข้างบนคือมี

        • app/src/debug/google-services.json สำหรับ debug (Build Type)
        • app/src/release/dev/google-services.json สำหรับ release (Build Type) และ dev (Product Flavor)
        • app/src/release/prod/google-services.json สำหรับ release (Build Type) และ prod (Product Flavor)
        • app/google-services.json สำหรับ Build Type และ Product Flavor ที่ไม่มีไฟล์ในนั้น จะใช้ตัวนี้เป็น Default แทน

        ดังนั้นถ้าจำเป็นต้องใช้ google-services.json แยกกัน ก็ให้วางในแต่ละ Directory ที่แยกไว้ได้เลยจ้า ไม่ต้องไปเพิ่มอะไรใน build.gradle อีกด้วย

สรุป

        ไฟล์ google-services.json ที่ผู้ที่หลงเข้ามาอ่านใช้งานกันได้ง่ายๆในทุกวันนี้ต้องผ่านอะไรมาก่อนบ้าง ส่วนหนึ่งก็ต้องขอบคุณ Gradle Plugin ของ Google Services ที่ช่วยจัดการอะไรหลายๆอย่างให้ แต่ถึงกระนั้นก็ยังมีบางอย่างที่นักพัฒนาต้องกำหนดเอง อย่างเช่นการทำ Build Variant ที่ใช้โปรเจคของ Google API และ Firebase แยกกัน เป็นต้น

        สุดท้ายแล้วบทความนี้อาจจะดูไม่สำคัญซักเท่าไร แต่การรู้ไว้ก่อนก็น่าจะดีกว่าเนอะ

แหล่งข้อมูลอ้างอิง

        • The Google Services Gradle Plugin [Android Developers]
        • ทำความเข้าใจ build.gradle แบบถึงขั้ว [Medium]




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium