ดูวิธีใช้ฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ในแอป Android

1. ก่อนเริ่มต้น

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีติดตั้งใช้งานฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ใน Android โดยใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบ

ข้อกำหนดเบื้องต้น

  • ความเข้าใจพื้นฐานเกี่ยวกับการใช้ Kotlin ในการพัฒนาแอป Android
  • มีความเข้าใจพื้นฐานเกี่ยวกับ Jetpack Compose (ดูข้อมูลเพิ่มเติมได้ที่นี่)

สิ่งที่คุณจะได้เรียนรู้

  • วิธีสร้างโปรเจ็กต์ Google Cloud
  • วิธีสร้างไคลเอ็นต์ OAuth ใน Google Cloud Console
  • วิธีติดตั้งใช้งานฟีเจอร์ลงชื่อเข้าใช้ด้วย Google โดยใช้โฟลว์ของชีตด้านล่าง
  • วิธีติดตั้งใช้งานฟีเจอร์ลงชื่อเข้าใช้ด้วย Google โดยใช้ขั้นตอนปุ่ม

สิ่งที่ต้องมี

2. สร้างโปรเจ็กต์ Android Studio

ระยะเวลา 3:00 - 5:00

หากต้องการเริ่มต้นใช้งาน เราต้องสร้างโปรเจ็กต์ใหม่ใน Android Studio โดยทำดังนี้

  1. เปิด Android Studio
  2. คลิกโปรเจ็กต์ใหม่ยินดีต้อนรับสู่ Android Studio
  3. เลือกโทรศัพท์และแท็บเล็ตและกิจกรรมเปล่าโปรเจ็กต์ Android Studio
  4. คลิกถัดไป
  5. ตอนนี้ถึงเวลาตั้งค่าโปรเจ็กต์บางส่วนแล้ว
    • ชื่อ: นี่คือชื่อโปรเจ็กต์ของคุณ
    • ชื่อแพ็กเกจ: ระบบจะเติมข้อมูลนี้โดยอัตโนมัติตามชื่อโปรเจ็กต์
    • บันทึกตำแหน่ง: ค่าเริ่มต้นควรเป็นโฟลเดอร์ที่ Android Studio บันทึกโปรเจ็กต์ของคุณ คุณเปลี่ยนการตั้งค่านี้เป็นที่ใดก็ได้ตามต้องการ
    • SDK ขั้นต่ำ: นี่คือ Android SDK เวอร์ชันต่ำสุดที่แอปของคุณสร้างขึ้นเพื่อเรียกใช้ ใน Codelab นี้ เราจะใช้ API 36 (Baklava)
    โปรเจ็กต์การตั้งค่า Android Studio
  6. คลิกเสร็จสิ้น
  7. Android Studio จะสร้างโปรเจ็กต์และดาวน์โหลดการอ้างอิงที่จำเป็นสำหรับแอปพลิเคชันพื้นฐาน ซึ่งอาจใช้เวลาหลายนาที หากต้องการดูการดำเนินการนี้ ให้คลิกไอคอนสร้างการสร้างโปรเจ็กต์ Android Studio
  8. เมื่อดำเนินการเสร็จแล้ว Android Studio ควรมีลักษณะดังนี้สร้างโปรเจ็กต์ Android Studio แล้ว

3. ตั้งค่าโปรเจ็กต์ Google Cloud

สร้างโปรเจ็กต์ Google Cloud

  1. ไปที่ Google Cloud Console
  2. เปิดโปรเจ็กต์หรือสร้างโปรเจ็กต์ใหม่GCP สร้างโปรเจ็กต์ใหม่GCP สร้างโปรเจ็กต์ใหม่ 2GCP create new project 3
  3. คลิก API และบริการAPI และบริการของ GCP
  4. ไปที่หน้าจอขอความยินยอม OAuthหน้าจอขอความยินยอม OAuth ของ GCP
  5. คุณจะต้องกรอกข้อมูลในช่องในภาพรวมเพื่อดำเนินการต่อ คลิกเริ่มต้นใช้งานเพื่อเริ่มกรอกข้อมูลต่อไปนี้ปุ่มเริ่มต้นใช้งาน GCP
    • ชื่อแอป: ชื่อของแอปนี้ ซึ่งควรเป็นชื่อเดียวกับที่คุณใช้เมื่อสร้างโปรเจ็กต์ใน Android Studio
    • อีเมลฝ่ายสนับสนุนผู้ใช้: อีเมลนี้จะแสดงบัญชี Google ที่คุณใช้เข้าสู่ระบบและกลุ่ม Google ที่คุณจัดการ
    ข้อมูลแอป GCP
    • ผู้ชม:
      • ภายในสำหรับแอปที่ใช้ภายในองค์กรเท่านั้น หากไม่มีองค์กรที่เชื่อมโยงกับโปรเจ็กต์ Google Cloud คุณจะเลือกตัวเลือกนี้ไม่ได้
      • เราจะใช้ "ภายนอก"
    กลุ่มเป้าหมาย GCP
    • ข้อมูลติดต่อ: อีเมลนี้จะเป็นอีเมลใดก็ได้ที่คุณต้องการให้เป็นอีเมลติดต่อสำหรับแอปพลิเคชัน
    ข้อมูลติดต่อของ GCP
    • อ่านบริการ Google API: นโยบายข้อมูลผู้ใช้
  6. เมื่ออ่านและยอมรับนโยบายข้อมูลผู้ใช้แล้ว ให้คลิกสร้างGCP Create

ตั้งค่าไคลเอ็นต์ OAuth

เมื่อตั้งค่าโปรเจ็กต์ Google Cloud แล้ว เราต้องเพิ่มไคลเอ็นต์ของเว็บและไคลเอ็นต์ Android เพื่อให้เรียก API ไปยังเซิร์ฟเวอร์แบ็กเอนด์ OAuth ด้วยรหัสไคลเอ็นต์ได้

สำหรับเว็บไคลเอ็นต์ Android คุณจะต้องมีสิ่งต่อไปนี้

  • ชื่อแพ็กเกจของแอป (เช่น com.example.example)
  • ลายเซ็น SHA-1 ของแอป
    • ลายเซ็น SHA-1 คืออะไร
      • ลายนิ้วมือ SHA-1 คือแฮชการเข้ารหัสที่สร้างจากคีย์การลงนามของแอป โดยทำหน้าที่เป็นตัวระบุที่ไม่ซ้ำกันสำหรับใบรับรองการลงนามของแอปที่เฉพาะเจาะจง ซึ่งเปรียบเสมือน "ลายเซ็น" ดิจิทัลสำหรับแอป
    • ทำไมเราจึงต้องใช้ลายเซ็น SHA-1
      • ลายนิ้วมือ SHA-1 ช่วยให้มั่นใจได้ว่ามีเพียงแอปของคุณที่ลงนามด้วยคีย์การลงนามที่เฉพาะเจาะจงเท่านั้นที่ขอโทเค็นเพื่อการเข้าถึงได้โดยใช้รหัสไคลเอ็นต์ OAuth 2.0 ซึ่งจะป้องกันไม่ให้แอปอื่นๆ (แม้แต่แอปที่มีชื่อแพ็กเกจเดียวกัน) เข้าถึงทรัพยากรและข้อมูลผู้ใช้ของโปรเจ็กต์
      • ลองคิดแบบนี้
        • คีย์ Signing ของแอปก็เหมือนกุญแจจริงที่ใช้เปิด "ประตู" ของแอป ซึ่งเป็นสิ่งที่อนุญาตให้เข้าถึงการทำงานภายในของแอป
        • ลายนิ้วมือ SHA-1 เปรียบเสมือนรหัสคีย์การ์ดที่ไม่ซ้ำกันซึ่งลิงก์กับกุญแจจริง โดยเป็นรหัสเฉพาะที่ระบุคีย์นั้นๆ
        • รหัสไคลเอ็นต์ OAuth 2.0 เปรียบเสมือนรหัสเข้าถึงทรัพยากรหรือบริการของ Google ที่เฉพาะเจาะจง (เช่น Google Sign-In)
        • เมื่อระบลายนิ้วมือ SHA-1 ระหว่างการตั้งค่าไคลเอ็นต์ OAuth คุณกำลังบอก Google ว่า "เฉพาะคีย์การ์ดที่มีรหัสเฉพาะนี้ (SHA-1) เท่านั้นที่เปิดรหัสการเข้าถึงนี้ (รหัสไคลเอ็นต์) ได้" ซึ่งจะช่วยให้มั่นใจได้ว่ามีเพียงแอปของคุณเท่านั้นที่เข้าถึงบริการของ Google ที่ลิงก์กับรหัสรายการนั้นได้"

สำหรับไคลเอ็นต์บนเว็บ สิ่งที่เราต้องการคือชื่อที่คุณต้องการใช้เพื่อระบุไคลเอ็นต์ในคอนโซล

สร้างไคลเอ็นต์ OAuth 2.0 สำหรับ Android

  1. ไปที่หน้าไคลเอ็นต์ไคลเอ็นต์ GCP
  2. คลิกสร้างไคลเอ็นต์สร้างไคลเอ็นต์ GCP
  3. เลือก Android สำหรับประเภทแอปพลิเคชัน
  4. คุณจะต้องระบุชื่อแพ็กเกจของแอป
  5. จาก Android Studio เราจะต้องรับลายเซ็น SHA-1 ของแอปและคัดลอก/วางลงในที่นี้
    1. ไปที่ Android Studio แล้วเปิดเทอร์มินัล
    2. เรียกใช้คำสั่งนี้: Mac/Linux:
      keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
      
      Windows:
        keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
      
      คำสั่งนี้ออกแบบมาเพื่อแสดงรายละเอียดของรายการ (นามแฝง) ที่เฉพาะเจาะจงภายในที่เก็บคีย์
      • -list: ตัวเลือกนี้จะบอกให้ Keytool แสดงเนื้อหาของ Keystore
      • -v: ตัวเลือกนี้จะเปิดใช้เอาต์พุตแบบละเอียด ซึ่งจะให้ข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับรายการ
      • -keystore ~/.android/debug.keystore: ระบุเส้นทางไปยังไฟล์ที่เก็บคีย์
      • -alias androiddebugkey: ระบุชื่อแทน (ชื่อรายการ) ของคีย์ที่ต้องการตรวจสอบ
      • -storepass android: ระบุรหัสผ่านสำหรับไฟล์ที่เก็บคีย์
      • -keypass android: ระบุรหัสผ่านสำหรับคีย์ส่วนตัวของนามแฝงที่ระบุ
    3. คัดลอกค่าของลายเซ็น SHA-1
    ลายเซ็น SHA
    1. กลับไปที่หน้าต่าง Google Cloud แล้ววางค่าลายเซ็น SHA-1
  6. ตอนนี้หน้าจอของคุณควรมีลักษณะคล้ายกับภาพนี้ และคุณสามารถคลิกสร้างได้รายละเอียดไคลเอ็นต์ Androidไคลเอ็นต์ Android

สร้างไคลเอ็นต์ OAuth 2.0 สำหรับเว็บ

  1. หากต้องการสร้างรหัสไคลเอ็นต์ของเว็บแอปพลิเคชัน ให้ทำซ้ำขั้นตอนที่ 1-2 จากส่วนสร้างไคลเอ็นต์ Android แล้วเลือกเว็บแอปพลิเคชันสำหรับประเภทแอปพลิเคชัน
  2. ตั้งชื่อไคลเอ็นต์ (ซึ่งจะเป็นไคลเอ็นต์ OAuth) รายละเอียดไคลเอ็นต์เว็บ
  3. คลิกสร้างเว็บไคลเอ็นต์
  4. คัดลอกรหัสไคลเอ็นต์จากหน้าต่างป๊อปอัป คุณจะต้องใช้รหัสนี้ในภายหลังคัดลอกรหัสไคลเอ็นต์

ตอนนี้เราตั้งค่าไคลเอ็นต์ OAuth เรียบร้อยแล้ว เราสามารถกลับไปที่ Android Studio เพื่อสร้างแอป Android ที่ลงชื่อเข้าใช้ด้วย Google ได้

4. ตั้งค่าอุปกรณ์เสมือน Android

หากต้องการทดสอบแอปพลิเคชันอย่างรวดเร็วโดยไม่ต้องใช้อุปกรณ์ Android จริง คุณจะต้องสร้างอุปกรณ์ Android เสมือนที่สามารถสร้างและเรียกใช้แอปได้ทันทีจาก Android Studio หากต้องการทดสอบด้วยอุปกรณ์ Android จริง คุณสามารถทำตามวิธีการจากเอกสารสำหรับนักพัฒนาแอป Android

สร้างอุปกรณ์ Android เสมือน

  1. เปิด Device ManagerDevice Manager ใน Android Studio
  2. คลิกปุ่ม + > สร้างอุปกรณ์เสมือนสร้างอุปกรณ์เสมือน
  3. คุณเพิ่มอุปกรณ์ที่จำเป็นสำหรับโปรเจ็กต์ได้จากตรงนี้ สำหรับ Codelab นี้ ให้เลือกโทรศัพท์ขนาดกลาง แล้วคลิกถัดไปโทรศัพท์ขนาดกลาง
  4. ตอนนี้คุณสามารถกำหนดค่าอุปกรณ์สำหรับโปรเจ็กต์ได้โดยตั้งชื่อที่ไม่ซ้ำกัน เลือกเวอร์ชันของ Android ที่อุปกรณ์จะใช้ และอื่นๆ ตรวจสอบว่าได้ตั้งค่า API เป็น API 36 "Baklava"; Android 16 แล้วคลิกเสร็จสิ้นกำหนดค่าอุปกรณ์เสมือน
  5. คุณควรเห็นอุปกรณ์ใหม่ปรากฏในตัวจัดการอุปกรณ์ หากต้องการยืนยันว่าอุปกรณ์ทำงาน ให้คลิก เรียกใช้อุปกรณ์ ข้างอุปกรณ์ที่เพิ่งสร้างเรียกใช้อุปกรณ์ 2
  6. ตอนนี้อุปกรณ์ควรจะทำงานแล้วอุปกรณ์ที่ใช้

ลงชื่อเข้าใช้ Android Virtual Device

อุปกรณ์ที่คุณเพิ่งสร้างใช้งานได้แล้ว ตอนนี้เราจะต้องลงชื่อเข้าใช้อุปกรณ์ด้วยบัญชี Google เพื่อป้องกันข้อผิดพลาดเมื่อทดสอบการลงชื่อเข้าใช้ด้วย Google

  1. ไปที่การตั้งค่า
    1. คลิกตรงกลางหน้าจอในอุปกรณ์เสมือน แล้วปัดขึ้น
    คลิกและปัด
    1. มองหาแอปการตั้งค่าแล้วคลิก
    แอปการตั้งค่า
  2. คลิก Google ในการตั้งค่าค่ากำหนดและบริการของ Google
  3. คลิกลงชื่อเข้าใช้ แล้วทำตามข้อความแจ้งเพื่อลงชื่อเข้าใช้บัญชี Googleการลงชื่อเข้าใช้อุปกรณ์
  1. ตอนนี้คุณควรลงชื่อเข้าใช้ในอุปกรณ์แล้วลงชื่อเข้าใช้อุปกรณ์แล้ว

ตอนนี้อุปกรณ์ Android เสมือนพร้อมสำหรับการทดสอบแล้ว

5. เพิ่มทรัพยากร Dependency

ระยะเวลา 5:00

หากต้องการเรียกใช้ OAuth API เราต้องผสานรวมไลบรารีที่จำเป็นก่อน ซึ่งจะช่วยให้เราส่งคำขอการตรวจสอบสิทธิ์และใช้รหัส Google เพื่อส่งคำขอเหล่านั้นได้

  • libs.googleid
  • libs.play.services.auth
  1. ไปที่ File > Project Structure:โครงสร้างโปรเจ็กต์
  2. จากนั้นไปที่การขึ้นต่อกัน > app > '+' > การขึ้นต่อกันของไลบรารีแท็กเริ่มการทำงาน
  3. ตอนนี้เราต้องเพิ่มไลบรารี
    1. ในกล่องโต้ตอบการค้นหา ให้พิมพ์ googleid แล้วคลิกค้นหา
    2. ควรมีเพียง 1 รายการ ให้เลือกรายการนั้นและเวอร์ชันสูงสุดที่พร้อมใช้งาน (ณ เวลาที่ Codelab นี้คือ 1.1.1)
    3. คลิกตกลงแพ็กเกจ Google ID
    4. ทำขั้นตอนที่ 1-3 ซ้ำ แต่ค้นหา "play-services-auth" แทน แล้วเลือกบรรทัดที่มี "com.google.android.gms" เป็นรหัสกลุ่ม และ "play-services-auth" เป็นชื่ออาร์ติแฟกต์การตรวจสอบสิทธิ์ของบริการ Google Play
  4. คลิกตกลงทรัพยากร Dependency ที่เสร็จแล้ว

6. ขั้นตอนการทำงานของ Bottom Sheet

โฟลว์ของ Bottom Sheet

โฟลว์ชีตด้านล่างใช้ Credential Manager API เพื่อให้ผู้ใช้ลงชื่อเข้าใช้แอปโดยใช้บัญชี Google ใน Android ได้อย่างราบรื่น โดยออกแบบมาให้รวดเร็วและสะดวกสบาย โดยเฉพาะสำหรับผู้ใช้ที่กลับมา ควรทริกเกอร์โฟลว์นี้เมื่อเปิดแอป

สร้างคำขอลงชื่อเข้าใช้

  1. หากต้องการเริ่มต้นใช้งาน ให้นำฟังก์ชัน Greeting() และ GreetingPreview() ออกจาก MainActivity.kt เราไม่จำเป็นต้องใช้ฟังก์ชันดังกล่าว
  2. ตอนนี้เราต้องตรวจสอบว่าได้นำเข้าแพ็กเกจที่ต้องการสำหรับโปรเจ็กต์นี้แล้ว โปรดเพิ่มคำสั่ง import ต่อไปนี้หลังคำสั่งที่มีอยู่โดยเริ่มที่บรรทัดที่ 3::
    import android.content.ContentValues.TAG
    import android.content.Context
    import android.credentials.GetCredentialException
    import android.os.Build
    import android.util.Log
    import android.widget.Toast
    import androidx.annotation.RequiresApi
    import androidx.compose.foundation.clickable
    import androidx.compose.foundation.Image
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Surface
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.res.painterResource
    import androidx.credentials.CredentialManager
    import androidx.credentials.exceptions.GetCredentialCancellationException
    import androidx.credentials.exceptions.GetCredentialCustomException
    import androidx.credentials.exceptions.NoCredentialException
    import androidx.credentials.GetCredentialRequest
    import com.google.android.libraries.identity.googleid.GetGoogleIdOption
    import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
    import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
    import java.security.SecureRandom
    import java.util.Base64
    import kotlinx.coroutines.CoroutineScope
    import androidx.compose.runtime.LaunchedEffect
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
  3. จากนั้นเราต้องสร้างฟังก์ชันเพื่อสร้างคำขอ Bottom Sheet วางโค้ดนี้ไว้ใต้คลาส MainActivity
   //This line is not needed for the project to build, but you will see errors if it is not present.
   //This code will not work on Android versions < UpsideDownCake
   @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
   @Composable
    fun BottomSheet(webClientId: String) {
        val context = LocalContext.current

        // LaunchedEffect is used to run a suspend function when the composable is first launched.
        LaunchedEffect(Unit) {
            // Create a Google ID option with filtering by authorized accounts enabled.
            val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
                .setFilterByAuthorizedAccounts(true)
                .setServerClientId(webClientId)
                .setNonce(generateSecureRandomNonce())
                .build()

            // Create a credential request with the Google ID option.
            val request: GetCredentialRequest = GetCredentialRequest.Builder()
                .addCredentialOption(googleIdOption)
                .build()

            // Attempt to sign in with the created request using an authorized account
            val e = signIn(request, context)
            // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
            // In this case, we attempt to sign in again with filtering disabled.
            if (e is NoCredentialException) {
                val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                    .setFilterByAuthorizedAccounts(false)
                    .setServerClientId(webClientId)
                    .setNonce(generateSecureRandomNonce())
                    .build()

                val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                    .addCredentialOption(googleIdOptionFalse)
                    .build()

                //We will build out this function in a moment
                signIn(requestFalse, context)
            }
        }
    }

   //This function is used to generate a secure nonce to pass in with our request
   fun generateSecureRandomNonce(byteLength: Int = 32): String {
      val randomBytes = ByteArray(byteLength)
      SecureRandom.getInstanceStrong().nextBytes(randomBytes)
      return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
   }

มาดูรายละเอียดการทำงานของโค้ดนี้กัน

fun BottomSheet(webClientId: String) {...}: สร้างฟังก์ชันชื่อ BottomSheet ที่รับอาร์กิวเมนต์สตริง 1 รายการชื่อ webClientid

  • val context = LocalContext.current: เรียกข้อมูลบริบท Android ปัจจุบัน ซึ่งจำเป็นสำหรับการดำเนินการต่างๆ รวมถึงการเปิดใช้คอมโพเนนต์ UI
  • LaunchedEffect(Unit) { ... }: LaunchedEffect เป็นฟังก์ชันที่ประกอบกันได้ของ Jetpack Compose ซึ่งช่วยให้คุณเรียกใช้ฟังก์ชันระงับ (ฟังก์ชันที่หยุดชั่วคราวและดำเนินการต่อได้) ภายในวงจรของฟังก์ชันที่ประกอบกันได้ Unit เป็นคีย์หมายความว่าเอฟเฟ็กต์นี้จะทํางานเพียงครั้งเดียวเมื่อเปิดใช้ Composable เป็นครั้งแรก
    • val googleIdOption: GetGoogleIdOption = ...: สร้างออบเจ็กต์ GetGoogleIdOption ออบเจ็กต์นี้กำหนดค่าประเภทของข้อมูลเข้าสู่ระบบที่ขอจาก Google
      • .Builder(): ใช้รูปแบบ Builder เพื่อกำหนดค่าตัวเลือก
      • .setFilterByAuthorizedAccounts(true): ระบุว่าจะอนุญาตให้ผู้ใช้เลือกจากบัญชี Google ทั้งหมด หรือเฉพาะบัญชีที่ให้สิทธิ์แอปแล้ว ในกรณีนี้ ค่าเป็นจริง ซึ่งหมายความว่าระบบจะส่งคำขอโดยใช้ข้อมูลเข้าสู่ระบบที่ผู้ใช้เคยให้สิทธิ์เพื่อใช้กับแอปนี้ หากมี
      • .setServerClientId(webClientId): ตั้งค่ารหัสไคลเอ็นต์ของเซิร์ฟเวอร์ ซึ่งเป็นตัวระบุที่ไม่ซ้ำกันสำหรับแบ็กเอนด์ของแอป คุณต้องระบุข้อมูลนี้เพื่อรับโทเค็นรหัส
      • .setNonce(generateSecureRandomNonce()): ตั้งค่า Nonce ซึ่งเป็นค่าแบบสุ่มเพื่อป้องกันการโจมตีแบบเล่นซ้ำและตรวจสอบว่าโทเค็นรหัสเชื่อมโยงกับคำขอที่เฉพาะเจาะจง
      • .build(): สร้างออบเจ็กต์ GetGoogleIdOption ด้วยการกำหนดค่าที่ระบุ
    • val request: GetCredentialRequest = ...: สร้างออบเจ็กต์ GetCredentialRequest ออบเจ็กต์นี้ห่อหุ้มคำขอข้อมูลเข้าสู่ระบบทั้งหมด
      • .Builder(): เริ่มรูปแบบตัวสร้างเพื่อกำหนดค่าคำขอ
      • .addCredentialOption(googleIdOption): เพิ่ม googleIdOption ลงในคำขอ โดยระบุว่าเราต้องการขอโทเค็นรหัส Google
      • .build(): สร้างออบเจ็กต์ GetCredentialRequest
    • val e = signIn(request, context): การดำเนินการนี้จะพยายามลงชื่อเข้าใช้ของผู้ใช้ด้วยคำขอที่สร้างขึ้นและบริบทปัจจุบัน ระบบจะจัดเก็บผลลัพธ์ของฟังก์ชัน signIn ไว้ใน e ตัวแปรนี้จะมีผลลัพธ์ที่สำเร็จหรือข้อยกเว้น
    • if (e is NoCredentialException) { ... }: นี่เป็นการตรวจสอบแบบมีเงื่อนไข หากฟังก์ชัน signIn ล้มเหลวด้วย NoCredentialException แสดงว่าไม่มีบัญชีที่ได้รับอนุญาตก่อนหน้านี้
      • val googleIdOptionFalse: GetGoogleIdOption = ...: หาก signIn ก่อนหน้าล้มเหลว ส่วนนี้จะสร้าง GetGoogleIdOption ใหม่
      • .setFilterByAuthorizedAccounts(false): นี่คือความแตกต่างที่สำคัญจากตัวเลือกแรก ซึ่งจะปิดใช้การกรองบัญชีที่ได้รับอนุญาต ซึ่งหมายความว่าสามารถใช้บัญชี Google ใดก็ได้ในอุปกรณ์เพื่อลงชื่อเข้าใช้
      • val requestFalse: GetCredentialRequest = ...: ระบบจะสร้าง GetCredentialRequest ใหม่ด้วย googleIdOptionFalse
      • signIn(requestFalse, context): การดำเนินการนี้พยายามลงชื่อเข้าใช้ผู้ใช้ด้วยคำขอใหม่ที่อนุญาตให้ใช้บัญชีใดก็ได้

โดยพื้นฐานแล้ว โค้ดนี้จะเตรียมคำขอไปยัง Credential Manager API เพื่อดึงโทเค็นรหัส Google สำหรับผู้ใช้โดยใช้การกำหนดค่าที่ระบุ จากนั้นจะใช้ GetCredentialRequest เพื่อเปิด UI ของเครื่องมือจัดการข้อมูลเข้าสู่ระบบได้ ซึ่งผู้ใช้จะเลือกบัญชี Google และให้สิทธิ์ที่จำเป็นได้

fun generateSecureRandomNonce(byteLength: Int = 32): String: ซึ่งจะกำหนดฟังก์ชันชื่อ generateSecureRandomNonce โดยจะยอมรับอาร์กิวเมนต์จำนวนเต็ม byteLength (ค่าเริ่มต้นคือ 32) ที่ระบุความยาวที่ต้องการของ Nonce ในหน่วยไบต์ โดยจะแสดงผลสตริงซึ่งจะเป็นการแสดงไบต์แบบสุ่มที่เข้ารหัส Base64

  • val randomBytes = ByteArray(byteLength): สร้างอาร์เรย์ไบต์ที่มี byteLength ที่ระบุเพื่อเก็บไบต์แบบสุ่ม
  • SecureRandom.getInstanceStrong().nextBytes(randomBytes):
    • SecureRandom.getInstanceStrong(): ซึ่งจะได้รับเครื่องมือสร้างตัวเลขสุ่มที่เข้ารหัสอย่างแน่นหนา ซึ่งเป็นสิ่งสำคัญอย่างยิ่งต่อความปลอดภัย เนื่องจากช่วยให้มั่นใจได้ว่าตัวเลขที่สร้างขึ้นนั้นสุ่มอย่างแท้จริงและคาดเดาไม่ได้ โดยจะใช้แหล่งที่มาของเอนโทรปีที่แข็งแกร่งที่สุดในระบบ
    • .nextBytes(randomBytes): This populates the randomBytes array with random bytes generated by the SecureRandom instance.
  • return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes):
    • Base64.getUrlEncoder(): รับตัวเข้ารหัส Base64 ที่ใช้ตัวอักษรที่ปลอดภัยสำหรับ URL (ใช้ - และ _ แทน + และ /) ซึ่งมีความสำคัญเนื่องจากช่วยให้มั่นใจได้ว่าสตริงที่ได้สามารถใช้ใน URL ได้อย่างปลอดภัยโดยไม่ต้องเข้ารหัสเพิ่มเติม
    • .withoutPadding(): การดำเนินการนี้จะนำอักขระการเว้นวรรคออกจากสตริงที่เข้ารหัส Base64 ซึ่งมักเป็นที่ต้องการเพื่อทำให้ Nonce สั้นลงและกะทัดรัดขึ้นเล็กน้อย
    • .encodeToString(randomBytes): ซึ่งจะเข้ารหัส randomBytes เป็นสตริง Base64 และส่งคืน

โดยสรุป ฟังก์ชันนี้จะสร้าง Nonce แบบสุ่มที่มีการเข้ารหัสที่รัดกุมซึ่งมีความยาวที่ระบุ เข้ารหัสโดยใช้ Base64 ที่ปลอดภัยสำหรับ URL และแสดงผลสตริงที่ได้ นี่เป็นแนวทางปฏิบัติมาตรฐานสำหรับการสร้าง Nonce ที่ปลอดภัยต่อการใช้งานในบริบทที่ละเอียดอ่อนด้านความปลอดภัย

ส่งคำขอลงชื่อเข้าใช้

ตอนนี้เราสร้างคำขอลงชื่อเข้าใช้ได้แล้ว จึงใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบเพื่อใช้ในการลงชื่อเข้าใช้ได้ ในการดำเนินการนี้ เราต้องสร้างฟังก์ชันที่จัดการการส่งคำขอลงชื่อเข้าใช้โดยใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบ ขณะเดียวกันก็จัดการข้อยกเว้นทั่วไปที่เราอาจพบ

คุณวางฟังก์ชันนี้ไว้ใต้ฟังก์ชัน BottomSheet() เพื่อให้ดำเนินการนี้สำเร็จได้

//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    var e: Exception? = null
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")

    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)

    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)

    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        return e

    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)

    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
    }
    return e
}

ต่อไปเราจะมาดูรายละเอียดว่าโค้ดนี้ทำอะไร

suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?: ซึ่งกำหนดฟังก์ชันระงับชื่อ signIn ซึ่งหมายความว่าสามารถหยุดชั่วคราวและกลับมาทำงานต่อได้โดยไม่บล็อกเทรดหลัก โดยจะแสดงผล Exception? ซึ่งจะเป็นค่าว่างหากลงชื่อเข้าใช้สำเร็จ หรือข้อยกเว้นที่เฉพาะเจาะจงหากลงชื่อเข้าใช้ไม่สำเร็จ

โดยมีพารามิเตอร์ 2 ตัวดังนี้

  • request: ออบเจ็กต์ GetCredentialRequest ซึ่งมีการกำหนดค่าสำหรับประเภทของข้อมูลเข้าสู่ระบบที่จะดึงข้อมูล (เช่น รหัส Google)
  • context: บริบท Android ที่จำเป็นต่อการโต้ตอบกับระบบ

สำหรับเนื้อหาของฟังก์ชัน ให้ทำดังนี้

  • val credentialManager = CredentialManager.create(context): สร้างอินสแตนซ์ของ CredentialManager ซึ่งเป็นอินเทอร์เฟซหลักสําหรับการโต้ตอบกับ Credential Manager API แอปจะเริ่มขั้นตอนการลงชื่อเข้าใช้ด้วยวิธีนี้
  • val failureMessage = "Sign in failed!": กำหนดสตริง (failureMessage) ที่จะแสดงในข้อความป๊อปอัปเมื่อลงชื่อเข้าใช้ไม่สำเร็จ
  • var e: Exception? = null: บรรทัดนี้จะเริ่มต้นตัวแปร e เพื่อจัดเก็บข้อยกเว้นที่อาจเกิดขึ้นในระหว่างกระบวนการ โดยเริ่มต้นด้วยค่า null
  • delay(250): เพิ่มความล่าช้า 250 มิลลิวินาที นี่คือวิธีแก้ปัญหาที่อาจเกิดขึ้นซึ่ง NoCredentialException อาจเกิดขึ้นทันทีเมื่อแอปเริ่มต้น โดยเฉพาะเมื่อใช้โฟลว์ BottomSheet ซึ่งจะช่วยให้ระบบมีเวลาเริ่มต้นเครื่องมือจัดการข้อมูลเข้าสู่ระบบ
  • try { ... } catch (e: Exception) { ... }:ใช้บล็อก try-catch เพื่อการจัดการข้อผิดพลาดที่มีประสิทธิภาพ ซึ่งจะช่วยให้มั่นใจได้ว่าหากเกิดข้อผิดพลาดในกระบวนการลงชื่อเข้าใช้ แอปจะไม่ขัดข้องและจะจัดการข้อยกเว้นได้อย่างราบรื่น
    • val result = credentialManager.getCredential(request = request, context = context): ส่วนนี้คือที่ที่การเรียก Credential Manager API เกิดขึ้นจริงและเริ่มกระบวนการดึงข้อมูลเข้าสู่ระบบ โดยจะรับคำขอและบริบทเป็นข้อมูลนำเข้า และจะแสดง UI ให้ผู้ใช้เลือกข้อมูลเข้าสู่ระบบ หากสำเร็จ ระบบจะแสดงผลลัพธ์ที่มีข้อมูลเข้าสู่ระบบที่เลือก ผลลัพธ์ของการดำเนินการนี้ GetCredentialResponse จะจัดเก็บไว้ในตัวแปร result
    • Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show():แสดงข้อความโทสต์สั้นๆ ที่ระบุว่าการลงชื่อเข้าใช้สำเร็จ
    • Log.i(TAG, "Sign in Successful!"): บันทึกข้อความที่สนุกและสำเร็จไปยัง Logcat
    • catch (e: GetCredentialException): จัดการข้อยกเว้นประเภท GetCredentialException นี่คือคลาสหลักสำหรับข้อยกเว้นที่เฉพาะเจาะจงหลายรายการที่อาจเกิดขึ้นในระหว่างกระบวนการดึงข้อมูลข้อมูลเข้าสู่ระบบ
    • catch (e: GoogleIdTokenParsingException): จัดการข้อยกเว้นที่เกิดขึ้นเมื่อมีข้อผิดพลาดในการแยกวิเคราะห์โทเค็นรหัส Google
    • catch (e: NoCredentialException): จัดการ NoCredentialException ซึ่งจะเกิดขึ้นเมื่อไม่มีข้อมูลเข้าสู่ระบบสำหรับผู้ใช้ (เช่น ผู้ใช้ไม่ได้บันทึกข้อมูลเข้าสู่ระบบ หรือไม่มีบัญชี Google)
      • ที่สำคัญ ฟังก์ชันนี้จะแสดงผลข้อยกเว้นที่จัดเก็บไว้ใน eNoCredentialException ซึ่งช่วยให้ผู้เรียกจัดการกรณีเฉพาะได้หากไม่มีข้อมูลเข้าสู่ระบบ
    • catch (e: GetCredentialCustomException): จัดการข้อยกเว้นที่กำหนดเองซึ่งอาจเกิดจากผู้ให้บริการข้อมูลเข้าสู่ระบบ
    • catch (e: GetCredentialCancellationException): จัดการ GetCredentialCancellationException ซึ่งจะแสดงเมื่อผู้ใช้ยกเลิกกระบวนการลงชื่อเข้าใช้
    • Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show(): แสดงข้อความโทสต์ที่ระบุว่าลงชื่อเข้าใช้ไม่สำเร็จโดยใช้ failureMessage
    • Log.e(TAG, "", e): บันทึกข้อยกเว้นไปยัง logcat ของ Android โดยใช้ Log.e ซึ่งใช้สำหรับข้อผิดพลาด ซึ่งจะรวมถึง Stacktrace ของข้อยกเว้นเพื่อช่วยในการแก้ไขข้อบกพร่อง นอกจากนี้ ยังมีอีโมติคอนโกรธเพื่อความสนุกสนานด้วย
  • return e: ฟังก์ชันจะแสดงผลข้อยกเว้นหากมีการจับข้อยกเว้น หรือแสดงผล Null หากลงชื่อเข้าใช้สำเร็จ

โดยสรุปแล้ว โค้ดนี้มีวิธีจัดการการลงชื่อเข้าใช้ของผู้ใช้โดยใช้ Credential Manager API, จัดการการดำเนินการแบบไม่พร้อมกัน, จัดการข้อผิดพลาดที่อาจเกิดขึ้น และให้ความคิดเห็นแก่ผู้ใช้ผ่านข้อความแจ้งและบันทึก พร้อมทั้งเพิ่มความขำขันเล็กน้อยในการจัดการข้อผิดพลาด

ใช้โฟลว์ Bottom Sheet ในแอป

ตอนนี้เราสามารถตั้งค่าการเรียกเพื่อทริกเกอร์โฟลว์ BottomSheet ในคลาส MainActivity ได้โดยใช้โค้ดต่อไปนี้และรหัสไคลเอ็นต์ของเว็บแอปพลิเคชันที่เราคัดลอกจาก Google Cloud Console ก่อนหน้านี้

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    //This will trigger on launch
                    BottomSheet(webClientId)
                }
            }
        }
    }
}

ตอนนี้เราสามารถบันทึกโปรเจ็กต์ (ไฟล์ > บันทึก) และเรียกใช้ได้แล้ว

  1. กดปุ่มเรียกใช้:เรียกใช้โปรเจ็กต์
  2. เมื่อแอปทำงานในโปรแกรมจำลองแล้ว คุณควรเห็น BottomSheet การลงชื่อเข้าใช้ปรากฏขึ้น คลิกต่อไปเพื่อทดสอบการลงชื่อเข้าใช้Bottom Sheet
  3. คุณควรเห็นข้อความ Toast ที่แสดงว่าการลงชื่อเข้าใช้สำเร็จแล้วBottom Sheet สำเร็จ

7. ขั้นตอนการทำงานของปุ่ม

GIF การไหลของปุ่ม

ขั้นตอนปุ่มสำหรับฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ช่วยให้ผู้ใช้ลงชื่อสมัครใช้หรือเข้าสู่ระบบแอป Android ได้ง่ายขึ้นโดยใช้บัญชี Google ที่มีอยู่ ผู้ใช้จะเห็นปุ่มนี้หากปิดชีตด้านล่างหรือต้องการใช้บัญชี Google ของตนเองอย่างชัดเจนเพื่อลงชื่อเข้าใช้หรือลงชื่อสมัครใช้ สำหรับนักพัฒนาแอป การเปลี่ยนแปลงนี้หมายถึงการเริ่มต้นใช้งานที่ราบรื่นยิ่งขึ้นและลดอุปสรรคระหว่างการลงชื่อสมัครใช้

แม้ว่าคุณจะใช้ปุ่ม Jetpack Compose ที่พร้อมใช้งานได้ แต่เราจะใช้ไอคอนแบรนด์ที่ได้รับอนุมัติล่วงหน้าจากหน้าหลักเกณฑ์การสร้างแบรนด์ของฟีเจอร์ลงชื่อเข้าใช้ด้วย Google

เพิ่มไอคอนแบรนด์ลงในโปรเจ็กต์

  1. ดาวน์โหลดไฟล์ ZIP ของไอคอนแบรนด์ที่ได้รับอนุมัติล่วงหน้าที่นี่
  2. คลายการบีบอัด signin-assest.zip จากโฟลเดอร์ดาวน์โหลด (ขั้นตอนนี้จะแตกต่างกันไปตามระบบปฏิบัติการของคอมพิวเตอร์) ตอนนี้คุณสามารถเปิดโฟลเดอร์ signin-assets และดูไอคอนที่มีได้แล้ว สำหรับ Codelab นี้ เราจะใช้ signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
  3. คัดลอกไฟล์
  4. วางลงในโปรเจ็กต์ใน Android Studio ในส่วน res > drawable โดยคลิกขวาที่โฟลเดอร์ drawable แล้วคลิกวาง (คุณอาจต้องขยายโฟลเดอร์ res เพื่อดู)ถอนออกได้
  5. กล่องโต้ตอบจะปรากฏขึ้นเพื่อแจ้งให้คุณเปลี่ยนชื่อไฟล์และยืนยันไดเรกทอรีที่จะเพิ่มไฟล์ เปลี่ยนชื่อชิ้นงานเป็น siwg_button.png แล้วคลิกตกลงการเพิ่มปุ่ม

โค้ดโฟลว์ของปุ่ม

โค้ดนี้จะใช้ฟังก์ชัน signIn() เดียวกันกับที่ใช้สำหรับ BottomSheet() แต่จะใช้ GetSignInWithGoogleOption แทน GetGoogleIdOption เนื่องจากโฟลว์นี้ไม่ได้ใช้ประโยชน์จากข้อมูลเข้าสู่ระบบและพาสคีย์ที่จัดเก็บไว้ในอุปกรณ์เพื่อแสดงตัวเลือกการลงชื่อเข้าใช้ ต่อไปนี้คือโค้ดที่คุณวางใต้ฟังก์ชัน BottomSheet() ได้

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(signInWithGoogleOption)
            .build()

        coroutineScope.launch {
            signIn(request, context)
        }
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = true, onClick = onClick)
    )
}

โค้ดนี้ทำอะไรบ้าง

fun ButtonUI(webClientId: String): ประกาศฟังก์ชันชื่อ ButtonUI ที่รับ webClientId (รหัสไคลเอ็นต์ของโปรเจ็กต์ Google Cloud) เป็นอาร์กิวเมนต์

val context = LocalContext.current: เรียกข้อมูลบริบท Android ปัจจุบัน ซึ่งจำเป็นสำหรับการดำเนินการต่างๆ รวมถึงการเปิดใช้คอมโพเนนต์ UI

val coroutineScope = rememberCoroutineScope(): สร้างขอบเขตของโครูทีน ซึ่งใช้เพื่อจัดการงานแบบอะซิงโครนัส ทำให้โค้ดทำงานได้โดยไม่บล็อกเทรดหลัก rememberCoroutineScope() เป็นฟังก์ชันที่ประกอบกันได้จาก Jetpack Compose ซึ่งจะให้ขอบเขตที่เชื่อมโยงกับวงจรของ Composable

val onClick: () -> Unit = { ... }: ซึ่งจะสร้างฟังก์ชัน Lambda ที่จะดำเนินการเมื่อมีการคลิกปุ่ม ฟังก์ชัน Lambda จะทำสิ่งต่อไปนี้

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build(): ส่วนนี้สร้างออบเจ็กต์ GetSignInWithGoogleOption ออบเจ็กต์นี้ใช้เพื่อระบุพารามิเตอร์สำหรับกระบวนการ "ลงชื่อเข้าใช้ด้วย Google" โดยต้องมี webClientId และ Nonce (สตริงแบบสุ่มที่ใช้เพื่อความปลอดภัย)
  • val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build(): ซึ่งจะสร้างออบเจ็กต์ GetCredentialRequest ระบบจะใช้คำขอนี้เพื่อรับข้อมูลเข้าสู่ระบบของผู้ใช้โดยใช้ตัวจัดการข้อมูลเข้าสู่ระบบ GetCredentialRequest จะเพิ่ม GetSignInWithGoogleOption ที่สร้างไว้ก่อนหน้านี้เป็นตัวเลือกเพื่อขอข้อมูลเข้าสู่ระบบ "ลงชื่อเข้าใช้ด้วย Google"
  • coroutineScope.launch { ... }: CoroutineScope สำหรับจัดการการดำเนินการแบบอะซิงโครนัส (ใช้โครูทีน)
    • signIn(request, context): เรียกฟังก์ชัน signIn() ที่เรากำหนดไว้ก่อนหน้านี้

Image(...): การดำเนินการนี้จะแสดงรูปภาพโดยใช้ painterResource ที่โหลดรูปภาพ R.drawable.siwg_button

  • Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick):
    • fillMaxSize(): ทำให้รูปภาพเต็มพื้นที่ว่าง
    • clickable(enabled = true, onClick = onClick): ทำให้รูปภาพคลิกได้ และเมื่อคลิกแล้ว จะเรียกใช้ฟังก์ชัน Lambda onClick ที่กำหนดไว้ก่อนหน้านี้

โดยสรุปแล้ว โค้ดนี้จะตั้งค่าปุ่ม "ลงชื่อเข้าใช้ด้วย Google" ใน UI ของ Jetpack Compose เมื่อคลิกปุ่มนี้ ระบบจะเตรียมคำขอข้อมูลเข้าสู่ระบบเพื่อเปิด Credential Manager และอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้ด้วยบัญชี Google

ตอนนี้เราต้องอัปเดตคลาส MainActivity เพื่อเรียกใช้ฟังก์ชัน ButtonUI()

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

ตอนนี้เราสามารถบันทึกโปรเจ็กต์ (ไฟล์ > บันทึก) และเรียกใช้ได้แล้ว

  1. กดปุ่มเรียกใช้:เรียกใช้โปรเจ็กต์
  2. เมื่อแอปทำงานในโปรแกรมจำลองแล้ว BottomSheet ควรปรากฏขึ้น คลิกภายนอกเพื่อปิดแตะที่นี่
  3. ตอนนี้คุณควรเห็นปุ่มที่เราสร้างแสดงในแอปแล้ว ให้คลิกปุ่มดังกล่าวเพื่อดูกล่องโต้ตอบการลงชื่อเข้าใช้กล่องโต้ตอบการลงชื่อเข้าใช้
  4. คลิกบัญชีเพื่อลงชื่อเข้าใช้

8. บทสรุป

คุณทำ Codelab นี้เสร็จแล้ว ดูข้อมูลเพิ่มเติมหรือความช่วยเหลือเกี่ยวกับการลงชื่อเข้าใช้ด้วย Google ใน Android ได้ที่ส่วนคำถามที่พบบ่อยด้านล่าง

คำถามที่พบบ่อย

โค้ด MainActivity.kt แบบเต็ม

โค้ดแบบเต็มสำหรับ MainActivity.kt เพื่อใช้อ้างอิงมีดังนี้

package com.example.example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
    fun BottomSheet(webClientId: String) {
        val context = LocalContext.current

        // LaunchedEffect is used to run a suspend function when the composable is first launched.
        LaunchedEffect(Unit) {
            // Create a Google ID option with filtering by authorized accounts enabled.
            val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
                .setFilterByAuthorizedAccounts(true)
                .setServerClientId(webClientId)
                .setNonce(generateSecureRandomNonce())
                .build()

            // Create a credential request with the Google ID option.
            val request: GetCredentialRequest = GetCredentialRequest.Builder()
                .addCredentialOption(googleIdOption)
                .build()

            // Attempt to sign in with the created request using an authorized account
            val e = signIn(request, context)
            // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
            // In this case, we attempt to sign in again with filtering disabled.
            if (e is NoCredentialException) {
                val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                    .setFilterByAuthorizedAccounts(false)
                    .setServerClientId(webClientId)
                    .setNonce(generateSecureRandomNonce())
                    .build()

                val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                    .addCredentialOption(googleIdOptionFalse)
                    .build()

                signIn(requestFalse, context)
            }
        }
    }

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(signInWithGoogleOption)
            .build()

        signIn(coroutineScope, request, context)
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = true, onClick = onClick)
    )
}

fun generateSecureRandomNonce(byteLength: Int = 32): String {
    val randomBytes = ByteArray(byteLength)
    SecureRandom.getInstanceStrong().nextBytes(randomBytes)
    return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}

//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    var e: Exception? = null
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")

    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)

    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)

    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        return e

    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)

    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
    }
    return e
}