การเรียกใช้งาน Activity ที่มีการส่งข้อมูลกลับด้วย Activity Result API

สำหรับการใช้งาน Activity บนแอปใด ๆ ก็ตาม มักจะมีการเรียก Activity ในแอปอื่น ๆ เพื่อขอข้อมูลบางอย่างผ่านแอปตัวนั้น ๆ เช่น ต้องการให้แอปสามารถถ่ายรูปได้ แต่ไม่ต้องการเขียนโค้ดเพื่อเรียกใช้งาน Camera API เอง จึงใช้วิธีสร้าง Implicit Intent สำหรับเปิดแอปกล้องที่อยู่ในเครื่องเพื่อถ่ายรูปแล้วส่งข้อมูลไฟล์ภาพกลับมาให้

จริง ๆ แล้วเราส่ง Implicit Intent ให้ Android System จากนั้น Android System ก็จะเป็นคนตัดสินใจเองว่าจะส่งต่อให้แอปไหน (ถ้ามีมากกว่า 1 แอป ก็จะแสดงหน้าต่างเพื่อให้ผู้ใช้กดเลือกก่อน)

โดยปกติแล้วนักพัฒนาจะต้องใช้คำสั่งอย่าง startActivityForResult แล้วประกาศ onActivityResult(...) เพื่อดักข้อมูลที่ส่งกลับมาจาก Activity ตัวนั้น

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_CODE_CAMERA = 1
    }

    private fun showContentChooser() {
        val intent: Intent = /* ... */
        startActivityForResult(intent, REQUEST_CODE_CAMERA)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        /* ... */
        if(requestCode == REQUEST_CODE_CAMERA) {
            // Check result code and retrieve data
        }
    }
}

ถึงแม้ว่าคำสั่งอาจจะดูไม่ซับซ้อนมากนักและใช้งานกันมาอย่างยาวนาน แต่วิธีนี้ก็ไม่ได้สะดวกอย่างที่คิด และไม่เหมาะกับกรณีที่ต้องการสร้าง Intent หลาย ๆ แบบ

เพราะจะต้องสร้าง Request Code ให้มีค่าต่างกัน และข้อมูลที่ได้ก็จะมารวมกันที่ onActivityResult ทำให้ต้องเช็ค Request Code ก่อนว่าเป็น Intent ตัวไหน แล้วค่อยเช็ค Result Code ว่าเป็น RESULT_OK หรือไม่ ถึงจะรับข้อมูลที่ส่งกลับมาได้

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_CODE_CAMERA = 1
        private const val REQUEST_CODE_GALLERY = 2
    }

    private fun selectImageFromCamera() {
        val intent: Intent = /* ... */
        startActivityForResult(intent, REQUEST_CODE_CAMERA)
    }
    
    private fun selectImageFromGallery() {
        val intent: Intent = /* ... */
        startActivityForResult(intent, REQUEST_CODE_GALLERY)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        /* ... */
        when (requestCode) {
            REQUEST_CODE_CAMERA -> {
                // Do something
            }
            REQUEST_CODE_GALLERY -> {
                // Do something
            }
        }
    }
}

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

โดย Activity Result API จะอยู่ใน AndroidX Activity และ AndroidX Fragment ตั้งแต่เวอร์ชัน 1.2.0 ขึ้นไป

implementation "androidx.activity:activity-ktx:<latest_version>"
implementation "androidx.activity:fragment-ktx:<latest_version>"

การใช้งาน Activity Result API

เบื้องหลังของ Activity Result API ก็คือการทำงานแบบเก่าเพื่อให้นำไปใช้งานได้ง่ายขึ้นเท่านั้น ดังนั้นการทำงานที่เกิดขึ้นจริงก็จะยังคงเหมือนเดิม

ซึ่ง Activity Result API จะมี 2 Component ด้วยกัน

  • Contract (ActivityResultContract) สำหรับกำหนด Intent ที่ต้องการ ซึ่งจะถูกแปลงเป็น Intent ในภายหลัง
  • Launcher (ActivityResultLauncher) สำหรับควบคุมการส่ง Intent ให้ Android System จัดการต่อให้ และรับข้อมูลที่ส่งกลับมาจาก Activity ปลายทาง เพื่อส่งกลับให้ Activity ต้นทางอีกที

โดยจะมีการเรียกใช้งานในลักษณะแบบนี้

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val getContent = registerForActivityResult(ActivityResultContracts.TakePicture()) { wasSaved: Boolean ->
        // Do something
    }

    private fun getImageFromCamera() {
        val uri: Uri = /* ... */
        getContent.launch(uri)
    }
}

เมื่อเทียบกับโค้ดแบบเดิม จะเห็นว่า Activity Result API มีคำสั่งที่สั้นกระชับกว่า และอ่านเข้าใจได้ง่ายกว่าอย่างเห็นได้ชัด

โดยโค้ดจะแบ่งเป็น 2 ส่วนด้วยกันคือ

  • ใช้ registerForActivityResult เพื่อสร้าง ActivityResultLauncher ขึ้นมาก่อน โดยจะต้องกำหนด ActivityResultContract ด้วย เพื่อบอกให้รู้ว่าจะเป็น Intent แบบไหน และผลลัพธ์ที่ได้ก็จะถูกส่งเข้ามาที่นี่ด้วยเช่นกัน
  • ใช้ launch ที่อยู่ใน ActivityResultLauncher เพื่อเริ่มส่ง Intent ให้ Android System

มี Contract แบบไหนให้ใช้งานบ้าง

โดย Contract ที่ Activity Result API ได้เตรียมไว้ให้ใช้งานจะมีทั้งหมดดังนี้

ActivityResultContracts.StartActivityForResult
ActivityResultContracts.StartIntentSenderForResult
ActivityResultContracts.RequestMultiplePermissions
ActivityResultContracts.RequestPermission
ActivityResultContracts.TakePicturePreview
ActivityResultContracts.TakePicture
ActivityResultContracts.TakeVideo
ActivityResultContracts.PickContact
ActivityResultContracts.GetContent
ActivityResultContracts.GetMultipleContents
ActivityResultContracts.OpenDocument
ActivityResultContracts.OpenMultipleDocuments
ActivityResultContracts.OpenDocumentTree
ActivityResultContracts.CreateDocument

สำหรับ Contract ที่นักพัฒนาจะได้ใช้ง่ายกันบ่อยที่สุดก็คงจะเป็น

  • RequestPermission, RequestMultiplePermissions - ขอสิทธิ์สำหรับ Runtime Permission
  • TakePicture, TakeVideo - เปิดใช้งานกล้องเพื่อถ่ายภาพหรือวีดีโอ
  • GetContent - เลือกไฟล์ต่าง ๆ ที่อยู่ในเครื่อง

และ Contract แต่ละตัวจะมี Input และ Output ที่แตกต่างกันออกไปตามจุดประสงค์ โดยสังเกตได้จาก ActivityResultContract ของ Contract ที่จะมีการกำหนด Type Parameter อยู่ 2 ตัวด้วยกัน

// ActivityResultContracts.java
public static final class RequestPermission extends ActivityResultContract<String, Boolean> { 
    /* ... */ 
}

จากตัวอย่างของ RequestPermission จะเห็นว่ามี Input เป็น String สำหรับ Permission ที่ต้องการขอสิทธิ์จากผู้ใช้ และมี Output เป็น Boolean เพื่อบอกให้รู้ว่าผู้ใช้ให้สิทธิ์หรือไม่

ถ้าอยากรู้ว่า Contract ตัวนั้นมี Intent เป็นแบบไหนก็ให้ดูโค้ดที่อยู่ในคำสั่ง createIntent(...) ของคลาสนั้น ๆ ได้เลย

// ActivityResultContracts.java
public static class TakePicture extends ActivityResultContract<Uri, Boolean> {

        @CallSuper
        @NonNull
        @Override
        public Intent createIntent(@NonNull Context context, @NonNull Uri input) {
            return new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                    .putExtra(MediaStore.EXTRA_OUTPUT, input);
        }

        /* ... */

        @NonNull
        @Override
        public final Boolean parseResult(int resultCode, @Nullable Intent intent) {
            return resultCode == Activity.RESULT_OK;
        }
    }

และถ้าอยากรู้ว่าใน Contract ตัวนั้นมีการแปลงข้อมูลออกมาเป็น Output ด้วยวิธีไหน ก็ให้ดูโค้ดที่อยู่ในคำสั่ง parseResult(...)  ได้เช่นกัน

สร้าง Contract ขึ้นมาใช้เอง

ในกรณีที่ Contract ที่มีให้ไม่ตรงกับความต้องการ นักพัฒนาก็สามารถสร้างขึ้นมาได้เช่นกัน

// GetImageContent.kt
class GetImageContent : ActivityResultContract<Unit, Uri?>() {
    override fun createIntent(context: Context, input: Unit) =
        Intent(Intent.ACTION_PICK)
            .setType("image/*")

    override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
        if(result == null || resultCode != Activity.RESULT_OK) {
            return null
        }
        return result.data
    }
}

อยากจะให้สร้าง Action แบบไหน และอยากจะแปลงข้อมูลที่ได้ยังไง ก็สามารถกำหนดได้เองตามใจชอบ แล้วนำไปใช้งานได้เลย

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val getImageContent = registerForActivityResult(GetImageContent()) { uri: Uri ->
        // Do something
    }

    private fun getImageFromGallery() {
        getImageContent.launch(Unit)
    }
}

สรุป

Activity Result API เป็นหนึ่งในความสามารถที่อยู่ใน AndroidX Activity และ AndroidX Fragment เพื่อช่วยให้นักพัฒนาสามารถจัดการกับคำสั่ง startActivityForResult(...) เพื่อลดความซับซ้อนของโค้ด และทำให้นักพัฒนาอ่านโค้ดได้ง่ายขึ้นนั่นเอง

โดยจะเลือก Contract ที่มีให้อยู่แล้ว หรือจะสร้างขึ้นมาเองก็ได้ จากนั้นก็เรียกใช้งานผ่าน Launcher และผลลัพธ์ที่ได้ก็จะถูกส่งกลับมาที่ Launcher ให้ทันที ไม่ต้องใช้คำสั่ง startActivityForResult(...) ที่จะต้องกำหนด Request Code ทุกครั้งให้ยุ่งยาก และไม่ต้องแยกโค้ดอีกชุดเพื่อรับข้อมูลใน onActivityResult(...) อีกต่อไป

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