04 April 2018

Service in Android - [ตอนที่ 6] JobIntentService จาก Support Library เพื่อใช้แทน IntentService

Updated on


        หลังจากสนุกสนานไปกับ Bound Service กันในบทความก่อนหน้า ก่อนจะพูดถึงในเรื่องต่อไปก็ขอคั่นโฆษณากันด้วยของเล่นจาก Android Support Library ที่ช่วยให้นักพัฒนาจัดการกับคลาส IntentService ได้สะดวกขึ้น นั่นก็คือ JobIntentService นั่นเองงงงงงง

บทความที่เกี่ยวข้อง

        • [ตอนที่ 1] พื้นฐานของ Service
        • [ตอนที่ 2] Lifecycle ของ Service
        • [ตอนที่ 3] เจาะลึกการเรียกใช้งาน Service และ Intent Service
        • [ตอนที่ 4] มาสร้าง Foreground Service กันเถอะ
        • [ตอนที่ 5] มาสร้าง Bound Service กันเถอะ

        ถ้าผู้ที่หลงเข้ามาอ่านลืมไปแล้วว่า Intent Service คืออะไร ก็จงย้อนกลับไปอ่านบทความซีรีย์นี้ในตอนที่ 3 ซะนะ

        อย่างที่เจ้าของบล็อกได้เล่าไปแล้วว่า IntentService เป็นคลาสสำเร็จรูปสำหรับสร้าง Service ให้ทำงานเป็นแบบ Queue และก็เคยยกตัวอย่างการเรียกใช้งานไปแล้วในบทความ Foreground Service แต่ทว่าถ้าต้องการสร้าง Intent Service เป็นแบบ Background Service สุดท้ายก็จะเจอปัญหา Background Service Limits ใน Android 8.0 Oreo อยู่ดี

เฮ้ พ่อหนุ่ม! อยากทำ Background Service ด้วย Intent Service หรอ? ลองใช้ JobIntentService ดูสิ

        JobIntentService เป็นหนึ่งในของเล่นจาก Support Library ที่ถูกเพิ่มเข้ามาใหม่ในเวอร์ชัน 26.1.0 โดยมีความสามารถเท่ๆดังนี้

        • เมื่อทำงานบน API 26 ขึ้นไป จะทำงานด้วย JobScheduler ที่นักพัฒนาส่วนใหญ่ไม่ยอมใช้กันซักที

        • แต่ถ้าต่ำกว่า API 26 ลงไป จะทำงานเป็น Service ธรรมดาๆที่สร้างด้วยคลาส IntentService


        พูดง่ายๆก็คือ JobIntentService จะมาช่วยแก้ปัญหา Service อายุไม่ยืนยาวเมื่อทำงานบน Android 8.0 Oreo นั่นเอง ซึ่งต้องแก้ปัญหาด้วยการย้ายไปใช้ JobSchduler แทน แต่ทว่ารองรับเฉพาะ Android 5.0 Lollipop (API 21) ขึ้นไปเท่านั้น จึงทำให้หลายๆคนไม่อยากใช้ เพราะต้องมานั่งเขียนโค้ดแยกเวอร์ชันกันให้วุ่นวาย

        ทีม Support Library ก็เลยทำให้นั่นเอง โดยที่นักพัฒนาไม่ต้องไปนั่งเขียนแยกเองระหว่าง Background Service ธรรมดากับ JobScheduler

        ถ้าอยากจะลองใช้จนทนไม่ไหวแล้วก็ให้เพิ่ม Support Library เข้าไปในโปรเจคเลยจ้า ซึ่งโดยปกติจะมาพร้อมกับ AppCompat v7 หรือ Library ตัวอื่นๆในตระกูล Support Library อยู่แล้วนะ สามารถเรียกใช้งานได้เลย

// build.gradle (app)

dependencies {
    ..
    implementation 'com.android.support:support-compat:27.1.0'
}

สร้าง Service ด้วย JobIntentService

         ให้สร้างคลาส Service ที่ Extends มาจากคลาส JobIntentService แล้วจะโดนบังคับให้ประกาศ Override Method ที่ชื่อว่า onHandleWork(...) ด้วย

// AwesomeJobIntentService.kt

import android.content.Intent
import android.support.v4.app.JobIntentService

class AwesomeJobIntentService : JobIntentService() {
    ...
    override fun onHandleWork(intent: Intent) {
        // Do something
    }
}

        ความรู้สึกคล้าย IntentService เลยเนอะ ต่างกันแค่ชื่อ Override Method เท่านั้นเอง แต่รูปแบบการทำงานก็เหมือนๆกันน่ะแหละ ดังนั้นถ้าผู้ที่หลงเข้ามาอ่านคนไหนคุ้นเคยกับ IntentService มาก่อน ก็จะรู้ว่าการย้ายโค้ดจาก IntentService มาเป็น JobIntentService นั้นโคตรง่ายเลยล่ะ

        เวลาที่ Service ตัวนี้ถูกเรียกใช้งาน onHandleWork(...) ก็จะถูกเรียกทุกครั้ง พร้อมกับส่ง Intent มาให้ด้วย เผื่อว่ามีข้อมูลอะไรที่อยากจะส่งเข้ามา

        และอยากจะเขียนคำสั่งอะไรไว้ใน onCreate() หรือ onDestroy() ก็เพิ่มเข้าไปได้เลยเช่นกัน

// AwesomeJobIntentService.kt

class AwesomeJobIntentService : JobIntentService() {
    ...
    override fun onCreate() {
        super.onCreate()
        // Do something when service was created
    }

    override fun onDestroy() {
        super.onDestroy()
        // Do something when service will destroy
    }
}

        จุดหนึ่งที่ต่างจาก IntentService ก็คือเวลา Component จะเรียกใช้งาน JobIntentService จะไม่ได้ใช้คำสั่ง startService(...) อีกต่อไปแล้ว แต่จะมีคำสั่งเป็นของตัวเองที่ชื่อว่า enqueueWork(...) เพราะว่า JobIntentService จะไปจัดการเองว่าจะสร้าง Service หรือ JobScheduler

JobIntentService.enqueueWork(context: Context, component: ComponentName, jobId: Int, work: Intent)
JobIntentService.enqueueWork(context: Context, cls Class<*>, jobId: Int, work: Intent)

        คำสั่ง enqueueWork(...) นั้นเป็น Static Method นะ ดังนั้นจึงสามารถเรียกได้ทันทีโดยไม่ต้องสร้าง Instance ของ JobIntentService ขึ้นมาก่อน

val context = ...
val JOB_ID = 1313
val intent = ...
JobIntentService.enqueueWork(context, AwesomeJobIntentService::class.java, JOB_ID, intent)

        มาถึงจุดนี้อาจจะสงสัยกันว่า Job ID ที่กำหนดลงไปคืออะไรกันนะ? มันคือ Unique ID ที่เอาไว้ใช้ใน JobSchduler นั่นเอง และทำเป็น Dynamic ID ไม่ได้นะ ต้อง Fixed ค่าลงไปแบบนี้เท่านั้น ส่วนจะเป็นเลขอะไรก็แล้วแต่ผู้ที่หลงเข้ามาอ่านเลย ถ้ามี JobIntentService มากกว่า 1 ตัวก็ควรกำหนด Job ID ของแต่ละตัวให้ต่างกันออกไปด้วยนะ

        แต่เพื่อให้โค้ดดูกระชับกว่านี้ แนะนำให้สร้างทำเป็น Static Method ซ้อนข้างในอีกทีดีกว่า แล้วค่อยไปเรียกคำสั่ง enqueueWork(...) จากในนั้นเอา

// AwesomeJobIntentService.kt

class AwesomeJobIntentService : JobIntentService() {
    companion object {
        private const val JOB_ID = 1313

        fun enqueueWork(context: Context, work: Intent) {
            enqueueWork(context, AwesomeJobIntentService::class.java, JOB_ID, work)
        }
    }
    ...
}

        เวลาเรียกใช้งานก็จะได้เหลือแค่นี้แทน

val context = ...
val intent = ...
AwesomeJobIntentService.enqueueWork(context, intent)

        เข้าใจง่ายขึ้นเยอะ รู้ทันทีว่าเป็น JobIntentService ตัวไหน และไม่ต้องมานั่งกำหนด Job ID เองทุกครั้ง ทีนี้ก็เรียกใช้งานได้ตามใจชอบ อยากจะส่งค่าอะไรเข้าไปด้วย ก็ให้ส่งผ่าน Intent ทุกครั้งนะจ๊ะ

val fileName = ...
val intent = Intent()
intent.putExtra(AwesomeJobIntentService.EXTRA_PHOTO_PATH, "/akexorcist/awesome/$fileName")
AwesomeJobIntentService.enqueueWork(this, intent)

        และถ้าจะให้ถูกต้อง Key ที่ใช้ใน Intent ก็เอาไปเก็บไว้ใน IntentJobService ตัวนั้นนะ

// AwesomeJobIntentService.kt

class AwesomeJobIntentService : JobIntentService() {

    companion object {
        private const val JOB_ID = 1313
        const val EXTRA_PHOTO_PATH = "photo_path"

        fun enqueueWork(context: Context, work: Intent) {
            enqueueWork(context, AwesomeJobIntentService::class.java, JOB_ID, work)
        }
    }

    override fun onHandleWork(intent: Intent) {
        val photoPath = intent.getStringExtra(EXTRA_PHOTO_PATH)
        photoPath?.let {
            uploadPhotoToWebServer(photoPath)
        }
    }

    private fun uploadPhotoToWebServer(photoPath: String) {
        ...
    }
}

        เสร็จแล้ว พร้อมใช้งานได้เลย

สรุป

        ผู้ที่หลงเข้ามาอ่านคนใดที่ใช้ IntentService อยู่ ก็แนะนำให้ย้ายไป JobIntentService แทน จะได้ทำงานได้ปกติสุขโดยเขียนโค้ดเพิ่มแค่นิดหน่อย เมื่อทำงานอยู่บนเครื่องที่ต่ำกว่า API 26 ก็เป็น IntentService แบบเดิมๆที่เคยเรียกใช้งาน และพอไปอยู่บน API 26 ขึ้นไปก็จะอยู่ในเงื่อนไขการทำงานของ JobSchduler ที่รองรับกับ Doze Mode และ Background Service Limits ที่จะทำงานเมื่อเครื่องอยู่ในสถานะ Active

        ลองใช้เถอะครับ ชีวิตดีแบบง่ายๆ ไม่ต้องกังวลอีกต่อไป

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

        • JobIntentService [Android Developers]
        • JobIntentService for background processing on Android O [Medium]
        • Keep those background Services working when targeting Android Oreo (26) [Medium]