สมมติว่าผู้ที่หลงเข้ามาอ่านอยากจะทำแอปพลิเคชันที่สามารถเปิดเว็ปของตัวเองได้ในตัวเอง จะต้องทำยังไงครับ? ใช่ครับ ก็แค่สร้าง WebView ขึ้นมาภายในแอปแล้วกำหนด URL ลงไปใช่มั้ยล่ะ

แต่วันนี้เจ้าของบล็อกจะมาแนะนำสิ่งใหม่ที่ดีกว่านั้นอีก นั่นก็คือ Chrome Custom Tabs ที่จะเพิ่มความสามารถให้มากขึ้น โดยไม่ต้องเสียเวลาเขียนโค้ดเพิ่มใน WebView เอง
Chrome Custom Tabs เป็นลูกเล่นอย่างหนึ่งของ Chrome ที่จะให้นักพัฒนาสามารถเรียกใช้งาน Chrome จากในแอปได้ โดยไม่ต้องเด้งไปเปิดแอป Chrome เพื่อใช้แทนการเปิด WebView ข้างในแอป

โดย Chrome Custom Tabs ถูกเพิ่มเข้ามาใน Chrome บนแอนดรอยด์ตั้งแต่เวอร์ชัน 45 ขึ้นไป ซึ่งในตอนนี้ก็ไม่น่าจะมีใครใช้เวอร์ชันต่ำกว่านี้แล้วล่ะ

เจ้าของบล็อกขอเรียก Chrome Custom Tabs แบบสั้นๆว่า CCT

แล้ว Chrome Custom Tabs มันดีกว่ายังไง?

การเปิดเว็ปบน WebView จะทำงานบางอย่างไม่ได้ จะต้องเขียนคำสั่งเพิ่มเข้าไปใน WebView เพื่อให้รองรับการทำงานนั้นๆ แต่สำหรับ CCT จะได้ความสามารถของ Chrome เลย รวมไปถึง Safe Browsing, Data Saver หรือแม้แต่ Autofill บน Chrome

ซึ่งเว็ปสามารถเรียกใช้งาน Chrome API ได้เต็มที่ สามารถใช้ Cache และ Cookies ร่วมกับ Chrome ได้เลย จึงช่วยให้นักพัฒนาเว็ปไม่ต้องปวดหัวกับการเขียนเว็ปให้รองรับ Chrome และ WebView ของแอนดรอยด์แยกกัน

และจุดเด่นที่น่าสนใจที่สุดก็คือการทำ Prefetching ที่จะดาวน์โหลดข้อมูลจาก URL มาเตรียมไว้ล่วงหน้า เมื่ออยากแสดงก็จะแสดงได้ทันทีโดยไม่ต้องรอโหลดจนเสร็จ

ถ้านึกไม่ออกมาว่าดียังไงก็ลองดูภาพข้างล่างประกอบเลย เป็นการเปรียบเทียบระหว่าง CCT ที่ทำ Prefetch ไว้แล้ว โดยเทียบกับ Chrome และ WebView

จะเห็นว่าความเร็วในการแสดงผลนั้นไวกว่าอย่างเห็นได้ชัด (ก็แหงน่ะสิ เพราะโหลดข้อมูลรอไว้ล่วงหน้าแล้ว)

ต่อไปก็มาดูวิธีการเรียกใช้ Chrome Custom Tabs กันเถอะ

เพิ่ม Dependency ของ Chrome Custom Tabs ลงใน Gradle

CCT อยู่ใน Android Jetpack ที่ชื่อว่า Browser โดยมี Artifact ดังนี้

dependencies {
    ...
    implementation "androidx.browser:browser:1.2.0"
}

คำสั่งสำหรับการเรียกใช้งาน Chrome Custom Tabs

ก่อนที่จะเริ่มสั่งให้เปิดหน้าเว็ปด้วย CCT จะต้องทำเตรียม Connection Service ของ CCT ให้เรียบร้อยเสียก่อน (แค่ครั้งแรกเท่านั้น ใส่ไว้ใน onCreate(…) ก็ได้) ด้วยคำสั่ง connectAndInitialize(…) โดยมีรูปแบบการเรียกใช้งานแบบนี้

val context: Context = ...
CustomTabsClient.getPackageName(context, null)?.let { chromePackageName ->
    val isBound = CustomTabsClient.connectAndInitialize(context, chromePackageName)
    if(isBound) {
        // Successful
    } else {
        // Fallback, Can't connect and initialize the CCT
    }
} ?: run {
    // Fallback, There's no Chrome installed
}

คำสั่งดังกล่าวจะต้องระบุ Package Name ของ Chrome เข้าไปในคำสั่งดังกล่าวด้วย แต่เนื่องจากผู้ใช้อาจจะไม่ได้ติดตั้ง Chrome ไว้ในเครื่อง หรือลง Chrome ที่เป็น Beta Version ไว้ในเครื่อง จึงไม่ควรกำหนดชื่อ Package Name ของ Chrome ลงไปตรงๆ แต่ให้ใช้คำสั่งของ CustomTabsClient แทน จึงทำให้มีโอกาสที่จะเกิด Fallback (ไม่มี Chrome ติดตั้งในเครื่องหรือเชื่อมต่อ Connection Service ของ CCT ไม่ได้) ดังนั้นทางที่ดีควรเขียนคำสั่งเผื่อในกรณีของ Fallback ไว้ด้วย เพื่อให้รองรับกับอุปกรณ์แอนดรอยด์ที่ไม่มี Chrome ในเครื่อง

และเมื่อต้องการเปิดหน้าเว็ปด้วย CCT ก็ให้ใช้คำสั่งแบบนี้ได้เลย

val context: Context = ...
val uri = Uri.parse("https://www.akexorcist.com")
val customTabsIntent: CustomTabsIntent = CustomTabsIntent.Builder().build()
customTabsIntent.launchUrl(context, uri)

คำสั่งดังกล่าวจะทำการเปิด CCT ขึ้นมาพร้อมกับแสดงหน้าเว็ปที่กำหนดไว้ให้ทันที

การทำ Prefetch ให้กับเว็ปที่ต้องการเปิดใน Chrome Custom Tabs

น่าเศร้านิดหน่อยตรงที่คำสั่งที่ผ่านมาจะไม่สามารถทำ Prefetch ได้ ดังนั้นถ้าต้องการทำ Prefetch ด้วย จะต้องเขียนโค้ดแบบเดียวกับคำสั่ง CustomTabsClient.connectAnInitialize(…) ด้วยตัวเอง เพื่อให้สามารถกำหนดเว็ปที่ต้องการให้ Prefetch ได้

var session: CustomTabsSession? = null
val sessionCallback = object : CustomTabsCallback() { }

fun connectAndInitialize(context: Context, packageName: String): Boolean {
    val connection = object : CustomTabsServiceConnection() {
        override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
            client.warmup(0)
            context.applicationContext.unbindService(this)
            session = client.newSession(sessionCallback)
        }

        override fun onServiceDisconnected(name: ComponentName?) {
        }
    }
    return try {
        CustomTabsClient.bindCustomTabsService(context.applicationContext, packageName, connection)
    } catch (e: SecurityException) {
        false
    }
}

นั่นก็เพราะว่าการทำ Prefetch จะต้องเรียกคำสั่งที่อยู่ใน CustomTabsSession นั่นเอง ซึ่งปกติแล้วคำสั่งใน CustomTabsClient ไม่ได้สร้าง Session ขึ้นมาให้

เมื่อต้องการ Prefetch ข้อมูลของเว็ปใดๆก็ตาม สามารถเรียกคำสั่งแบบนี้ได้เลย

var session: CustomTabsSession? = null

val uri = Uri.parse("https://www.akexorcist.com")
session?.mayLaunchUrl(uri, null, null)

จากนั้น CCT ก็จะทำการ Prefetch ข้อมูลของเว็ปนั้นๆให้ทันที

แต่ทำ Prefetch จะเป็นการสร้าง Custom Session ขึ้นมาเอง ดังนั้นเวลาสั่งให้เปิดหน้าเว็ปจะต้องส่ง Session ตัวนี้เข้าไปใน Builder ของ CustomTabsIntent ด้วย

val context: Context = ...
var session: CustomTabsSession? = null

val uri = Uri.parse("https://www.akexorcist.com")
val customTabsIntent: CustomTabsIntent = CustomTabsIntent.Builder(session).build()
customTabsIntent.launchUrl(context, uri)

สามารถปรับ UI ใน Chrome Custom Tabs ได้ด้วยนะ

เพื่อให้การแสดงผลของ CCT สามารถรองรับการใช้งานตามความต้องการของแต่ละแอปได้หลากหลาย จึงมีคำสั่งเพื่อให้นักพัฒนาสามารถกำหนดค่าต่างๆให้กับ CCT ได้ด้วย

โดยสามารถกำหนดได้ในตอนที่สร้าง Builder ของ CustomTabsIntent นั่นเอง

val context: Context = ...
val uri = Uri.parse("https://www.akexorcist.com")
val customTabsIntent: CustomTabsIntent = CustomTabsIntent.Builder()
    .addDefaultShareMenuItem()
    .enableUrlBarHiding()
    .setNavigationBarColor(ContextCompat.getColor(context, R.color.colorPrimary))
    .setToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary))
    .setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary))
    .setShowTitle(true)
    .build()
customTabsIntent.launchUrl(context, uri)

และนอกจากปรับ UI แล้ว ยังสามารถเพิ่ม Action Button และ Menu Item เข้าไปใน CCT ได้อีกด้วย

ในการกำหนด Action Button จะกำหนดได้แค่เพียงปุ่มเดียว โดยกำหนดได้ทั้งภาพปุ่มและ PendingIntent เพื่อกำหนด Intent สำหรับตอนที่ผู้ใช้กดที่ปุ่มนั้นๆ

val context: Context = ...
val actionIntent: Intent = ...
val actionPendingIntent = PendingIntent.getActivity(context, 0, actionIntent, 0)
val actionIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_done) 
val customTabsIntent: CustomTabsIntent = CustomTabsIntent.Builder()
    .setActionButton(actionIcon, "Done", actionPendingIntent, true)
    ...
    .build()

และสำหรับการเพิ่ม Menu Item จะกำหนดได้แค่ข้อความและ PendingIntent สำหรับเมนูนั้นๆ โดยมีคำสั่งเป็นแบบนี้

val markAsReadPendingIntent: PendingIntent = ...
val favouritePendingIntent: PendingIntent = ...
val customTabsIntent: CustomTabsIntent = CustomTabsIntent.Builder()
    .addMenuItem("Mark as read", markAsReadPendingIntent)
    .addMenuItem("Favourite", favouritePendingIntent)
    ...
    .build()

สรุป

Chrome Custom Tabs ก็เป็นอีกหนึ่ง Library จาก Android Jetpack ที่จะช่วยให้นักพัฒนาสามารถทำแอปที่รองรับการเปิดเว็ปได้สะดวกขึ้น โดยที่ใช้ความสามารถของ Chrome แทนการใช้ WebView ซึ่งจะทำให้หน้าเว็ปนั้นทำงานได้เสมือนกับการเปิดบน Chrome โดยตรง

แต่ก็อย่าลืมว่าไม่ใช่ทุกเครื่องที่จะติดตั้ง Chrome ไว้ ดังนั้นจึงควรเขียนเผื่อในกรณีของ Fallback ไว้ให้เหมาะสมด้วย