1. บทนำ
ในโค้ดแล็บนี้ คุณจะมุ่งเน้นที่การสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียว การผสานรวมแอปกับ Play Billing Library (PBL) และวิเคราะห์สาเหตุที่ทำให้การซื้อลดลง
หมายเหตุ: คุณต้องมีสิทธิ์เข้าถึงฟีเจอร์ตัวเลือกการซื้อและข้อเสนอหลายรายการสำหรับผลิตภัณฑ์แบบครั้งเดียวจึงจะทำ Codelab นี้ให้เสร็จสมบูรณ์ได้ ฟีเจอร์นี้อยู่ในโปรแกรมทดลองใช้ก่อนเปิดตัว (EAP) ผลิตภัณฑ์และฟีเจอร์ใน EAP พร้อมใช้งาน "ตามที่เป็น" และอาจมีการสนับสนุนที่จำกัด หากต้องการเข้าถึงฟีเจอร์ EAP ให้ส่งคำขอโดยใช้แบบฟอร์มแสดงความสนใจ EAP สำหรับผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียว อย่างไรก็ตาม หากคุณต้องการทราบวิธีวิเคราะห์การหยุดซื้อโดยใช้รหัสการตอบกลับของ Play Billing เพียงอย่างเดียว ให้ไปที่ส่วนวิเคราะห์การหยุดซื้อของ Codelab นี้โดยตรง
ผู้ชม
Codelab นี้มีไว้สำหรับนักพัฒนาแอป Android ที่ใช้ Play Billing Library (PBL) หรือต้องการใช้ PBL เพื่อสร้างรายได้จากผลิตภัณฑ์แบบครั้งเดียว
สิ่งที่คุณจะได้เรียนรู้...
- วิธีสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวใน Google Play Console
- วิธีผสานรวมแอปกับ PBL
- วิธีประมวลผลการซื้อไอเทมแบบเรียกเก็บเงินครั้งเดียวที่ใช้แล้วหมดไปและไอเทมที่อยู่ตลอดไปใน PBL
- วิธีวิเคราะห์การเลิกทำกลางคันระหว่างการซื้อ
สิ่งที่คุณต้องมี...
- สิทธิ์เข้าถึง Google Play Console ด้วยบัญชีนักพัฒนาแอป หากยังไม่มีบัญชีนักพัฒนาแอป คุณจะต้องสร้างบัญชี
- แอปตัวอย่างสำหรับโค้ดแล็บนี้ ซึ่งคุณดาวน์โหลดจาก GitHub ได้
- Android Studio
2. สร้างแอปตัวอย่าง
แอปตัวอย่างออกแบบมาให้เป็นแอป Android ที่ทำงานได้อย่างสมบูรณ์ซึ่งมีซอร์สโค้ดทั้งหมดที่แสดงให้เห็นถึงแง่มุมต่อไปนี้
- การผสานรวมแอปกับ PBL
- ดึงข้อมูลไอเทมแบบเรียกเก็บเงินครั้งเดียว
- เปิดตัวขั้นตอนการซื้อสำหรับไอเทมแบบเรียกเก็บเงินครั้งเดียว
- สถานการณ์การซื้อที่ทำให้เกิดการตอบกลับการเรียกเก็บเงินต่อไปนี้
BILLING_UNAVAILABLEUSER_CANCELLEDOKITEM_ALREADY_OWNED
วิดีโอเดโมต่อไปนี้แสดงลักษณะและการทำงานของแอปตัวอย่างหลังจากที่ติดตั้งใช้งานและเรียกใช้
สิ่งที่ต้องดำเนินการก่อน
ก่อนที่จะสร้างและติดตั้งใช้งานแอปตัวอย่าง ให้ทำดังนี้
- สร้างบัญชีนักพัฒนาแอป Google Play Console หากมีบัญชีนักพัฒนาแอปอยู่แล้ว ให้ข้ามขั้นตอนนี้
- สร้างแอปใหม่ใน Play Console เมื่อสร้างแอป คุณจะระบุชื่อแอปใดก็ได้สำหรับแอปตัวอย่าง
- ติดตั้ง Android Studio
สร้าง
วัตถุประสงค์ของขั้นตอนการสร้างนี้คือการสร้างไฟล์ Android App Bundle ที่ลงนามแล้วของแอปตัวอย่าง
หากต้องการสร้าง Android App Bundle ให้ทำตามขั้นตอนต่อไปนี้
- ดาวน์โหลดแอปตัวอย่างจาก GitHub
- สร้างแอปตัวอย่าง ก่อนสร้าง ให้เปลี่ยนชื่อแพ็กเกจของแอปตัวอย่าง แล้วจึงสร้าง หากคุณมีแพ็กเกจของแอปอื่นๆ ใน Play Console โปรดตรวจสอบว่าชื่อแพ็กเกจที่คุณระบุสำหรับแอปตัวอย่างนั้นไม่ซ้ำกัน
หมายเหตุ: การสร้างแอปตัวอย่างจะสร้างเฉพาะไฟล์ APK ที่คุณใช้สำหรับการทดสอบในเครื่องได้ อย่างไรก็ตาม การเรียกใช้แอปจะไม่ดึงผลิตภัณฑ์และราคาเนื่องจากยังไม่ได้กำหนดค่าผลิตภัณฑ์ใน Play Console ซึ่งคุณจะได้ทำในโค้ดแล็บนี้ - สร้าง Android App Bundle ที่ลงนามแล้ว
ขั้นตอนถัดไปคือการอัปโหลด Android App Bundle ไปยัง Google Play Console
3. สร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวใน Play Console
หากต้องการสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวใน Google Play Console คุณต้องมีแอปใน Play Console สร้างแอปใน Play Console แล้วอัปโหลด App Bundle ที่ลงนามแล้วซึ่งสร้างไว้ก่อนหน้านี้
สร้างแอป
วิธีสร้างแอป
- เข้าสู่ระบบ Google Play Console โดยใช้บัญชีนักพัฒนาแอป
- คลิกสร้างแอป ซึ่งจะเปิดหน้าสร้างแอป
- ป้อนชื่อแอป เลือกภาษาเริ่มต้น และรายละเอียดอื่นๆ ที่เกี่ยวข้องกับแอป
- คลิกสร้างแอป ซึ่งจะเป็นการสร้างแอปใน Google Play Console
ตอนนี้คุณอัปโหลด App Bundle ที่ลงนามแล้วของแอปตัวอย่างได้แล้ว
อัปโหลด App Bundle ที่ลงนามแล้ว
- อัปโหลด App Bundle ที่ลงนามแล้วไปยังแทร็กทดสอบภายในของ Google Play Console หลังจากอัปโหลดแล้วเท่านั้น คุณจึงจะกำหนดค่าฟีเจอร์ที่เกี่ยวข้องกับการสร้างรายได้ใน Play Console ได้
- คลิกทดสอบและเผยแพร่ > การทดสอบ > รุ่นภายใน > สร้างรุ่นใหม่
- ป้อนชื่อรุ่นแล้วอัปโหลดไฟล์ App Bundle ที่ลงชื่อแล้ว
- คลิกถัดไป แล้วคลิกบันทึกและเผยแพร่
ตอนนี้คุณสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวได้แล้ว
สร้างไอเทมแบบเรียกเก็บเงินครั้งเดียว
วิธีสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียว
- ใน Google Play Console ให้ไปที่สร้างรายได้ด้วย Play > ผลิตภัณฑ์ > ผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวจากเมนูการนำทางด้านซ้าย
- คลิกสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียว
- ป้อนรายละเอียดผลิตภัณฑ์ต่อไปนี้
- รหัสสินค้า: ป้อนรหัสสินค้าที่ไม่ซ้ำกัน ป้อน
one_time_product_01 - (ไม่บังคับ) แท็ก: เพิ่มแท็กที่เกี่ยวข้อง
- ชื่อ: ป้อนชื่อผลิตภัณฑ์ เช่น
Product name - คำอธิบาย: ป้อนคำอธิบายผลิตภัณฑ์ เช่น
Product description - (ไม่บังคับ) เพิ่มรูปภาพไอคอน: อัปโหลดไอคอนที่แสดงถึงผลิตภัณฑ์
- รหัสสินค้า: ป้อนรหัสสินค้าที่ไม่ซ้ำกัน ป้อน
- คลิกถัดไป
- เพิ่มตัวเลือกการซื้อและกำหนดค่าความพร้อมจำหน่ายสินค้าระดับภูมิภาค ผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวต้องมีตัวเลือกการซื้ออย่างน้อย 1 รายการ ซึ่งจะกำหนดวิธีการให้สิทธิ์ ราคา และความพร้อมจำหน่ายสินค้าระดับภูมิภาค สำหรับโค้ดแล็บนี้ เราจะเพิ่มตัวเลือกซื้อมาตรฐานสำหรับผลิตภัณฑ์ ในส่วนตัวเลือกการซื้อ ให้ป้อนรายละเอียดต่อไปนี้
- รหัสตัวเลือกการซื้อ: ป้อนรหัสตัวเลือกการซื้อ เช่น
buy - ประเภทการซื้อ: เลือกซื้อ
- (ไม่บังคับ) แท็ก: เพิ่มแท็กที่เฉพาะเจาะจงสำหรับตัวเลือกการซื้อนี้
- (ไม่บังคับ) คลิกตัวเลือกขั้นสูงเพื่อกำหนดค่าตัวเลือกขั้นสูง คุณข้ามการกำหนดค่าตัวเลือกขั้นสูงได้เพื่อวัตถุประสงค์ของโค้ดแล็บนี้
- รหัสตัวเลือกการซื้อ: ป้อนรหัสตัวเลือกการซื้อ เช่น
- ในส่วนความพร้อมให้บริการและการกำหนดราคา ให้คลิกกำหนดราคา > แก้ไขราคาแบบเป็นกลุ่ม
- เลือกตัวเลือกประเทศ / ภูมิภาค ซึ่งจะเป็นการเลือกทุกภูมิภาค
- คลิกต่อไป ซึ่งจะเปิดกล่องโต้ตอบสำหรับป้อนราคา ป้อน 100 บาท แล้วคลิกใช้
- คลิกบันทึก แล้วคลิกเปิดใช้งาน ซึ่งจะสร้างและเปิดใช้งานตัวเลือกการซื้อ
สำหรับ Codelab นี้ ให้สร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียวเพิ่มเติม 3 รายการโดยใช้รหัสผลิตภัณฑ์ต่อไปนี้
- consumable_product_01
- consumable_product_02
- consumable_product_03
แอปตัวอย่างได้รับการกำหนดค่าให้ใช้รหัสผลิตภัณฑ์เหล่านี้ คุณระบุรหัสผลิตภัณฑ์ที่แตกต่างกันได้ ในกรณีนี้ คุณจะต้องแก้ไขแอปตัวอย่างเพื่อใช้รหัสผลิตภัณฑ์ที่คุณระบุ
เปิดแอปตัวอย่างใน Google Play Console แล้วไปที่สร้างรายได้ด้วย Play > ผลิตภัณฑ์ > ผลิตภัณฑ์แบบครั้งเดียว จากนั้นคลิกสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียว แล้วทำซ้ำขั้นตอนที่ 3-9
วิดีโอการสร้างผลิตภัณฑ์แบบเรียกเก็บเงินครั้งเดียว
วิดีโอตัวอย่างต่อไปนี้แสดงขั้นตอนการสร้างไอเทมแบบเรียกเก็บเงินครั้งเดียวที่อธิบายไว้ก่อนหน้านี้
4. ผสานรวมกับ PBL
ตอนนี้เราจะมาดูวิธีผสานรวมแอปกับ Play Billing Library (PBL) ส่วนนี้จะอธิบายขั้นตอนระดับสูงสำหรับการผสานรวม และแสดงข้อมูลโค้ดสำหรับแต่ละขั้นตอน คุณสามารถใช้ข้อมูลโค้ดเหล่านี้เป็นแนวทางในการติดตั้งใช้งานจริงได้
หากต้องการผสานรวมแอปกับ PBL ให้ทำตามขั้นตอนต่อไปนี้
- เพิ่มทรัพยากร Dependency ของ Play Billing Library ลงในแอปตัวอย่าง
dependencies { val billing_version = "8.0.0" implementation("com.android.billingclient:billing-ktx:$billing_version") } - เริ่มต้น BillingClient BillingClient คือ SDK ของไคลเอ็นต์ที่อยู่ในแอปและสื่อสารกับ Play Billing Library ข้อมูลโค้ดต่อไปนี้แสดงวิธีเริ่มต้นไคลเอ็นต์การเรียกเก็บเงิน
protected BillingClient createBillingClient() { return BillingClient.newBuilder(activity) .setListener(purchasesUpdatedListener) .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()) .enableAutoServiceReconnection() .build(); } - เชื่อมต่อกับ Google Play ข้อมูลโค้ดต่อไปนี้แสดงวิธีเชื่อมต่อกับ Google Play
public void startBillingConnection(ImmutableList<Product> productList) { Log.i(TAG, "Product list sent: " + productList); Log.i(TAG, "Starting connection"); billingClient.startConnection( new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { if (billingResult.getResponseCode() == BillingResponseCode.OK) { // Query product details to get the product details list. queryProductDetails(productList); } else { // BillingClient.enableAutoServiceReconnection() will retry the connection on // transient errors automatically. // We don't need to retry on terminal errors (e.g., BILLING_UNAVAILABLE, // DEVELOPER_ERROR). Log.e(TAG, "Billing connection failed: " + billingResult.getDebugMessage()); Log.e(TAG, "Billing response code: " + billingResult.getResponseCode()); } } @Override public void onBillingServiceDisconnected() { Log.e(TAG, "Billing Service connection lost."); } }); } - ดึงข้อมูลรายละเอียดสินค้าแบบเรียกเก็บเงินครั้งเดียว หลังจากผสานรวมแอปกับ PBL แล้ว คุณต้องดึงข้อมูลรายละเอียดสินค้าแบบเรียกเก็บเงินครั้งเดียวลงในแอป ข้อมูลโค้ดต่อไปนี้แสดงวิธีดึงข้อมูลรายละเอียดสินค้าแบบเรียกเก็บเงินครั้งเดียวในแอป
การดึงข้อมูลprivate void queryProductDetails(ImmutableList<Product> productList) { Log.i(TAG, "Querying products for: " + productList); QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(productList).build(); billingClient.queryProductDetailsAsync( queryProductDetailsParams, new ProductDetailsResponseListener() { @Override public void onProductDetailsResponse( BillingResult billingResult, QueryProductDetailsResult productDetailsResponse) { // check billingResult Log.i(TAG, "Billing result after querying: " + billingResult.getResponseCode()); // process returned productDetailsList Log.i( TAG, "Print unfetched products: " + productDetailsResponse.getUnfetchedProductList()); setupProductDetailsMap(productDetailsResponse.getProductDetailsList()); billingServiceClientListener.onProductDetailsFetched(productDetailsMap); } }); }ProductDetailsจะให้การตอบกลับที่คล้ายกับข้อความต่อไปนี้{ "productId": "consumable_product_01", "type": "inapp", "title": "Shadow Coat (Yolo's Realm | Play Samples)", "name": "Shadow Coat", "description": "A sleek, obsidian coat for stealth and ambushes", "skuDetailsToken": "<---skuDetailsToken--->", "oneTimePurchaseOfferDetails": {}, "oneTimePurchaseOfferDetailsList": [ { "priceAmountMicros": 1990000, "priceCurrencyCode": "USD", "formattedPrice": "$1.99", "offerIdToken": "<--offerIdToken-->", "purchaseOptionId": "buy", "offerTags": [] } ] }, { "productId": "consumable_product_02", "type": "inapp", "title": "Emperor Den (Yolo's Realm | Play Samples)", "name": "Emperor Den", "description": "A fair lair glowing with molten rock and embers", "skuDetailsToken": "<---skuDetailsToken--->", "oneTimePurchaseOfferDetails": {}, "oneTimePurchaseOfferDetailsList": [ { "priceAmountMicros": 2990000, "priceCurrencyCode": "USD", "formattedPrice": "$2.99", "offerIdToken": "<--offerIdToken-->", "purchaseOptionId": "buy", "offerTags": [] } ] } - เปิดตัวขั้นตอนการเรียกเก็บเงิน
public void launchBillingFlow(String productId) { ProductDetails productDetails = productDetailsMap.get(productId); if (productDetails == null) { Log.e( TAG, "Cannot launch billing flow: ProductDetails not found for productId: " + productId); billingServiceClientListener.onBillingResponse( BillingResponseCode.ITEM_UNAVAILABLE, BillingResult.newBuilder().setResponseCode(BillingResponseCode.ITEM_UNAVAILABLE).build()); return; } ImmutableList<ProductDetailsParams> productDetailsParamsList = ImmutableList.of( ProductDetailsParams.newBuilder().setProductDetails(productDetails).build()); BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetailsParamsList) .build(); billingClient.launchBillingFlow(activity, billingFlowParams); } - ตรวจหาและประมวลผลการซื้อ ในขั้นตอนนี้ คุณต้องทำสิ่งต่อไปนี้
- ยืนยันการซื้อ
- ให้สิทธิ์แก่ผู้ใช้
- แจ้งเตือนผู้ใช้
- แจ้งให้ Google ทราบถึงกระบวนการซื้อ
private void handlePurchase(Purchase purchase) { // Step 1: Send the purchase to your secure backend to verify the purchase following // https://developer.android.com/google/play/billing/security#verify // Step 2: Update your entitlement storage with the purchase. If purchase is // in PENDING state then ensure the entitlement is marked as pending and the // user does not receive benefits yet. It is recommended that this step is // done on your secure backend and can combine in the API call to your // backend in step 1. // Step 3: Notify the user using appropriate messaging. if (purchase.getPurchaseState() == PurchaseState.PURCHASED) { for (String product : purchase.getProducts()) { Log.d(TAG, product + " purchased successfully! "); } } // Step 4: Notify Google the purchase was processed. // For one-time products, acknowledge the purchase. // This sample app (client-only) uses billingClient.acknowledgePurchase(). // For consumable one-time products, consume the purchase // This sample app (client-only) uses billingClient.consumeAsync() // If you have a secure backend, you must acknowledge purchases on your server using the // server-side API. // See https://developer.android.com/google/play/billing/security#acknowledge if (purchase.getPurchaseState() == PurchaseState.PURCHASED && !purchase.isAcknowledged()) { if (shouldConsume(purchase)) { ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); billingClient.consumeAsync(consumeParams, consumeResponseListener); } else { AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); billingClient.acknowledgePurchase( acknowledgePurchaseParams, acknowledgePurchaseResponseListener); } } }
5. วิเคราะห์การเลิกทำกลางคันระหว่างการซื้อ
ที่ผ่านมาในโค้ดแล็บ การตอบกลับการเรียกเก็บเงินของ Play มุ่งเน้นไปที่สถานการณ์ที่จำกัด เช่น การตอบกลับ USER_CANCELLED, BILLING_UNAVAILABLE, OK และ ITEM_ALREADY_OWNED อย่างไรก็ตาม การเรียกเก็บเงินของ Play สามารถแสดงรหัสการตอบกลับที่แตกต่างกัน 13 รายการ ซึ่งอาจเกิดจากปัจจัยต่างๆ ในโลกแห่งความเป็นจริง
ส่วนนี้จะอธิบายสาเหตุของการตอบกลับข้อผิดพลาด USER_CANCELLED และ BILLING_UNAVAILABLE พร้อมแนะนำการดำเนินการแก้ไขที่เป็นไปได้ซึ่งคุณสามารถนำไปใช้ได้
รหัสข้อผิดพลาดในการตอบกลับ USER_CANCELED
รหัสการตอบกลับนี้บ่งชี้ว่าผู้ใช้ละทิ้ง UI ของขั้นตอนการซื้อก่อนที่จะทำการซื้อให้เสร็จสมบูรณ์
สาเหตุที่เป็นไปได้ | คุณทำอะไรได้บ้าง |
|
|
รหัสข้อผิดพลาดในการตอบกลับ BILLING_UNAVAILABLE
รหัสการตอบกลับนี้หมายความว่าการซื้อไม่สำเร็จเนื่องจากมีปัญหาเกี่ยวกับผู้ให้บริการชำระเงินของผู้ใช้หรือรูปแบบการชำระเงินที่ผู้ใช้เลือก เช่น บัตรเครดิตของผู้ใช้หมดอายุแล้ว หรือผู้ใช้อยู่ในประเทศที่ไม่รองรับ รหัสนี้ไม่ได้บ่งบอกถึงข้อผิดพลาดเกี่ยวกับระบบการเรียกเก็บเงินของ Play เอง
สาเหตุที่เป็นไปได้ | คุณทำอะไรได้บ้าง |
|
|
กลยุทธ์การลองอีกครั้งสำหรับรหัสข้อผิดพลาดในการตอบกลับ
กลยุทธ์การลองใหม่ที่มีประสิทธิภาพสำหรับข้อผิดพลาดที่กู้คืนได้จาก Play Billing Library (PBL) จะแตกต่างกันไปตามบริบท เช่น การโต้ตอบของผู้ใช้ในเซสชัน (เช่น ระหว่างการซื้อ) กับการดำเนินการในเบื้องหลัง (เช่น การค้นหาการซื้อเมื่อแอปกลับมาทำงานต่อ) การใช้กลยุทธ์เหล่านี้มีความสำคัญเนื่องจากBillingResponseCodeค่าบางค่าบ่งบอกถึงปัญหาชั่วคราวที่แก้ไขได้โดยการลองอีกครั้ง ในขณะที่ค่าอื่นๆ เป็นค่าถาวรและไม่จำเป็นต้องลองอีกครั้ง
สำหรับข้อผิดพลาดที่พบเมื่อผู้ใช้อยู่ในเซสชัน เราขอแนะนำให้ใช้กลยุทธ์การลองใหม่แบบง่ายๆ โดยกำหนดจำนวนครั้งสูงสุดที่พยายามเพื่อลดการรบกวนประสบการณ์ของผู้ใช้ ในทางกลับกัน สำหรับการดำเนินการในเบื้องหลัง เช่น การรับทราบการซื้อใหม่ ซึ่งไม่จำเป็นต้องดำเนินการทันที เราขอแนะนำให้ใช้แนวทางการถอยแบบทวีคูณ
ดูข้อมูลโดยละเอียดเกี่ยวกับรหัสการตอบกลับที่เฉพาะเจาะจงและกลยุทธ์การลองใหม่ที่แนะนำที่เกี่ยวข้องได้ที่จัดการรหัสการตอบกลับ BillingResult
6. ขั้นตอนถัดไป
- ดูวิธีเพิ่มประสิทธิภาพการผสานรวมการเรียกเก็บเงินใน Play
- อย่าลืมทำตามแนวทางปฏิบัติแนะนำสำหรับการยืนยันและประมวลผลการซื้อในแบ็กเอนด์ที่ปลอดภัยเมื่อผู้ใช้เริ่มซื้อผลิตภัณฑ์เหล่านี้
เอกสารอ้างอิง
7. ยินดีด้วย
ยินดีด้วย คุณได้ไปยังส่วนต่างๆ ของ Google Play Console เพื่อสร้างผลิตภัณฑ์แบบครั้งเดียวใหม่ ทดสอบรหัสการตอบกลับการเรียกเก็บเงิน และวิเคราะห์การเลิกซื้อเรียบร้อยแล้ว
แบบสำรวจ
ความคิดเห็นของคุณเกี่ยวกับ Codelab นี้มีค่าอย่างยิ่ง โปรดสละเวลาสักครู่เพื่อตอบแบบสำรวจของเรา