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

บทความในซีรีย์เดียวกัน

อย่างที่เคยเล่าไปแล้วว่า IntentService เป็นคลาสสำเร็จรูปสำหรับสร้าง Service ให้ทำงานเป็นแบบ Queue แต่ทว่าถ้าต้องการสร้าง 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

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

// build.gradle (app)
dependencies {
    ..
    implementation 'androidx.core:core:<latest_version>'
}

สร้าง Service ด้วย JobIntentService

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

// AwesomeJobIntentService.kt
import android.content.Intent
import androidx.core.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

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

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