Tìm hiểu về WorkManager trong Android

WorkManager Trong Android.

1: WorkManager là gì?

Workmanager là một thư viện của gói Architecture trong Android Jetpack, workmanager được xây dựng nhằm mục đích cho việc lập lịch và quản lý các tác vụ chạy ngầm.

2: Khi nào sử dụng WorkManager?

  • Trước khi tìm hiểu xem khi nào sử dụng workmanager chúng ta cần phải hiểu về các tác vụ nền.
    Tác vụ nền là bất kỳ tác vụ nào liên quan đến ứng dụng không được thực hiện trên luồng chính của giao diện người dùng (UI) của Android. Thông thường, bạn cần thực thi một tác vụ trên một luồng nền khi mà các tác vụ là một hoạt động tốn kém ví dụ như áp dụng bộ lọc cho một bitmap hoặc tác vụ phụ thuộc vào yêu cầu mạng để truy vấn hoặc đăng dữ liệu. Vì bạn không muốn chặn luồng chính hoặc bạn muốn tác vụ tiếp tục chạy ngay cả khi bạn đóng ứng dụng, bạn phải gửi tác vụ đến một luồng ở background.
  • Các loại công việc nền có thể được phân loại theo hai chiều:

    Trục tung biểu thị cho thời gian của công việc: "Công việc có cần phải được thực hiện tại một thời điểm chính xác hay nó có thể được hoãn lại không?" Trục hoành thể hiện mức độ quan trọng của công việc: "Bạn có cần đảm bảo rằng công việc sẽ chắc chắn được thực thi hay không?"
  • Ở bên trái, đối với các tác vụ không cần bảo đảm thực thi, bạn có thể sử dụng ThreadPools, RxJava hoặc coroutines. Ở phía trên bên phải, cho thời gian chính xác và thực hiện được bảo đảm, bạn nên sử dụng ForegroundService. Ở phía dưới bên phải, để thực thi được duy trì và bảo đảm, bạn có một số tùy chọn: JobScheduler, JobDispatcher Firebase và Alarm Manager + Broadcast receivers. Workmanager nằm trong vùng này, tức là các tác vụ có thể trì hoãn và cần được đảm bảo là sẽ được thực thi, với workmanager các tác cụ có thể kèm theo các điều kiện ràng buộc kèm theo. Từ đó ta có sơ đồ sau:

  • Từ trên ta thấy workmanager có thể được dùng trong các trường hợp ví dụ như:
    • Tải tệp lên máy chủ.
    • Đồng bộ hóa dữ liệu đến từ một máy chủ và lưu nó vào một cơ sở dữ liệu phòng.
    • Gửi log tới máy chủ.
    • Thực hiện các thao tác nặng trên dữ liệu.

3: Tại sao lại cần WorkManager?

Trước khi workmanager xuất hiện đã có rất nhiều công vụ để chúng ta có thể làm được những tác vụ ngầm như vậy, vậy thì tại sao lại cần workmanager?
Từ Android phiên bản 6.0 google đã giới thiệu Doze, cơ chế mà thiết bị sẽ ngắt các kết nối như interner, wifi, bluetooth v.v khi thiết bị tắt màn hình và chỉ chạy lại khi thiết bị được mở lên hay được kết nối vào nguồn điện nhằm tiết kiệm pin cho thiết bị sang tới phiên bản Android 8.0 trở đi google tiến xa hơn bằng cách siết chặt các dịch vụ chạy nền, hệ thống sẽ áp đặt các hạn chế với các dịch vụ chạy nền này nếu bản thân ứng dụng đang không ở trạng thái foreground. Đối với hầu hết các trường hợp như thế google khuyên chúng ta nên sử dụng lập lịch để thay thế, và workmanager là một công cụ tốt để làm việc này. Ngoài ra workmanager còn có những ưu điểm như dễ sử dụng, dễ dàng truy vấn hay hủy bỏ và quan trọng nhất là hỗ trợ ngược các phiên bản Android cũ hơn từ Api 14 trở lên.

Nhìn qua bảng so sánh này chắc các bạn đã biết là mình nên dùng cái nào rồi ạ =)).

4: Các thành phần trong workmanager.


  • Các thành phần chính mà chũng ta cần quan tâm ở đây là:
    • Worker: Là lớp sẽ xác định tác vụ nào bạn cần thực hiện. Bạn extend lớp này và thực hiện công việc ở đây.
    • Work request: Xác định những thứ như ngữ cảnh mà tác vụ sẽ chạy.
    • WorkStatus: Trạng thái chứa thông tin về tác vụ như id, tag, state, outputdata.
    • State: Trạng thái hiện tại của request: ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCELLED.

4.1: Typical workflow.

  • Đầu tiên, bạn sẽ định nghĩa lớp Worker của mình và ghi đè phương thức doWork () của nó. Lớp thực thi của bạn chỉ định cách thực hiện thao tác, nhưng không có bất kỳ thông tin nào về thời điểm tác vụ sẽ chạy.
class CompressWorker(context : Context, params : WorkerParameters)
    : Worker(context, params) {

    override fun doWork(): Result {

        // Do the work here--in this case, compress the stored images.
        // In this example no parameters are passed; the task is
        // assumed to be "compress the whole library."
        myCompress()

        // Indicate success or failure with your return value:
        return Result.SUCCESS

        // (Returning RETRY tells WorkManager to try this task again
        // later; FAILURE says not to try again.)

    }

}
  • Tiếp theo, bạn tạo một đối tượng OneTimeWorkRequest dựa trên Worker đó, sau đó enqueue nhiệm vụ với WorkManager:
val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>().build()
WorkManager.getInstance().enqueue(compressionWork)
  • WorkManager lúc này sẽ chọn một thời điểm thích hợp để chạy tác vụ. Trong hầu hết các trường hợp, nếu bạn không chỉ định bất kỳ ràng buộc nào, WorkManager sẽ chạy tác vụ của bạn ngay lập tức. Nếu bạn cần kiểm tra trạng thái tác vụ, bạn có thể lấy đối tượng WorkInfo bằng cách xử lý một LiveData thích hợp <WorkInfo>. Ví dụ: nếu bạn muốn kiểm tra xem tác vụ đã hoàn tất chưa, bạn có thể sử dụng mã như sau:
WorkManager.getInstance().getWorkInfoByIdLiveData(compressionWork.id)
                .observe(lifecycleOwner, Observer { workInfo ->
                    // Do something with the status
                    if (workInfo != null && workInfo.state.isFinished) {
                        // ...
                    }
                })
  • Tác vụ có ràng buộc. Nếu muốn, bạn có thể chỉ định các ràng buộc khi nhiệm vụ được chạy. Ví dụ: bạn có thể muốn chỉ định rằng tác vụ chỉ nên chạy khi thiết bị ở chế độ chờ và kết nối với nguồn điện. Trong trường hợp này, bạn cần tạo một đối tượng OneTimeWorkRequest.Builder và sử dụng trình tạo đó để tạo OneTimeWorkRequest:
    // Create a Constraints object that defines when the task should run
val myConstraints = Constraints.Builder()
        .setRequiresDeviceIdle(true)
        .setRequiresCharging(true)
        // Many other constraints are available, see the
        // Constraints.Builder reference
        .build()

// ...then create a OneTimeWorkRequest that uses those constraints
val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>()
        .setConstraints(myConstraints)
        .build()
  • Hủy tác vụ. Bạn có thể hủy một công việc sau khi bạn enqueue nó. Để hủy tác vụ, bạn cần ID công việc của nó, mà bạn có thể nhận được từ đối tượng WorkRequest. Ví dụ, đoạn mã sau hủy bỏ yêu cầu compressionWork từ phần trước:
  val compressionWorkId:UUID = compressionWork.getId()
WorkManager.getInstance().cancelWorkById(compressionWorkId)

4.2: Advanced workmanager.

  • Ngoài việc thực thi các tác như đã nêu ở trên workmanager còn có thể thực thi chuỗi các tác vụ (chained tasks). Ứng dụng của bạn có thể cần chạy một số tác vụ theo một thứ tự cụ thể. WorkManager cho phép bạn tạo và enqueue một chuỗi công việc xác định nhiều nhiệm vụ và thứ tự chúng sẽ chạy.

    Ví dụ ở dưới đây ta có một chuỗi 3 tác vụ A, B, C được thực hiện lần lượt:
    WorkManager.getInstance()
    .beginWith(workA)
        // Note: WorkManager.beginWith() returns a
        // WorkContinuation object; the following calls are
        // to WorkContinuation methods
    .then(workB)    // FYI, then() returns a new WorkContinuation instance
    .then(workC)
    .enqueue()
  • Hoặc bạn có thể tạo các chuỗi phức tạp hơn bằng cách nối nhiều chuỗi với các phương thức WorkContinuation.combine ().
    val chain1 = WorkManager.getInstance()
    .beginWith(workA)
    .then(workB)
val chain2 = WorkManager.getInstance()
    .beginWith(workC)
    .then(workD)
val chain3 = WorkContinuation
    .combine(chain1, chain2)
    .then(workE)
chain3.enqueue()

Lý thuyết như vậy là đủ, cúng ta hãy cùng làm 1 ví dụ cơ bản có sử dụng workmanager.

5: Demo

  • Ở đây mình xin làm một ví dụ nho nhỏ về cách mà workermanager hoạt động
  • Đầu tiên các bạn cần add thư viện cho project của bạn, các bạn thêm đoạn code sau vào build.gradle trong module app.
def work_version = "1.0.0-alpha11"

    implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin

    // optional - Firebase JobDispatcher support
    implementation "android.arch.work:work-firebase:$work_version"

    // optional - Test helpers
    androidTestImplementation "android.arch.work:work-testing:$work_version"
  • Sau đó mình tạo class MyWorker thực hiện việc cộng 2 số và log ra kết quả.
    class MyWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
companion object {
    private const val TAG = "MyWorker"
}

override fun doWork(): Result =
    try {
        val a= 1
        val b = 1
        Log.i(TAG, "${a.plus(b)}")
        Result.SUCCESS
    } catch (exception: Exception) {
        Log.i(TAG, "Fail")
        Result.FAILURE
    }
}
  • Ở layout main mình tạo 3 button doWorker, doWorkerWithConstraint và cancelWork, khi click doWorker thì sẽ thực hiện phép tính, còn doWorkerWithConstraint thì sẽ thực hiện phép tính khi có kết nối mạng, còn cancelWork sẽ hủy work được gọi ở workManager.enqueue


  • Sau đó tại MainActivity ta có:
class MainActivity : AppCompatActivity(), View.OnClickListener {

    private lateinit var workManager: WorkManager
    private lateinit var constraints: Constraints
    private lateinit var workId: UUID
    private lateinit var compressionWork: OneTimeWorkRequest

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        workManager = WorkManager.getInstance()
        registerListener()
    }

    private fun registerListener() {
        button_do_work.setOnClickListener(this)
        button_do_work_constraint.setOnClickListener(this)
        button_cancel_work.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when (v) {
            button_do_work -> doWorker()
            button_do_work_constraint -> doWorkerWithConstraint()
            button_cancel_work -> cancelWork()
        }
    }

    private fun doWorker() {
        compressionWork = OneTimeWorkRequest.Builder(MyWorker::class.java)
            .build()
        workManager.enqueue(compressionWork)
    }

    private fun doWorkerWithConstraint() {
        constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()

        compressionWork = OneTimeWorkRequest.Builder(MyWorker::class.java)
            .setConstraints(constraints)
            .build()

        workManager.enqueue(compressionWork)
    }

    private fun cancelWork() {
        workId = compressionWork.id
        workManager.cancelWorkById(workId)
    }
}
  • Ở đây mình chỉ cần worker thực hiện một lần duy nhất nên sử dụng OneTimeWorkRequest, workManager được khởi tạo tại onCreate(), lần lượt bắt sự kiện cho 3 nút ở hàm registerListener().
  • Khi phương thức doWorker() được gọi thì sẽ khởi tạo compressionWork từ class MyWorker mình đã tạo trước đó, worker khởi chạy và ở màn hình log mình sẽ có kết quả:
- 11-20 15:16:39.756 10489-10549/com.example.framgianguyenthanhtungh.workmanager I/MyWorker: 2
11-20 15:16:39.756 10489-10539/com.example.framgianguyenthanhtungh.workmanager I/WorkerWrapper: Worker result SUCCESS for Work [ id=004ae674-202a-4072-8aa2-576ceb1618f8, tags={ com.example.framgianguyenthanhtungh.workmanager.MyWorker } ]
  • Ở phương thức thứ hai, mình sẽ setConstraints cho worker với điều kiện là thiết bị phải được kết nối mạng, lúc này khi click chạy thì worker sẽ không chạy ngay mà chỉ khi mình bật mạng thì worker mới chạy.
  • Ở phương thức cuối cùng mình lấy địa chỉ id của worker rồi cho workManager hủy đi worker đó, lúc này khi mình click doWorkerWithConstraint() sau đó hủy thì đi bật mạng lại check log thì worker không chạy vì đã bị hủy bởi workManager() rồi.

Trên đây là ví dụ hết sức cơ bản để các bạn hình dung cách thức mà workmanager hoạt động, Ví dụ chi tiết và đầy đủ hơn về workmanager xin hẹn các bạn ở bài viết sau. Bài viết dựa trên kiến thức mình tìm hiểu vậy nên không thể tránh khỏi sai sót mong các bạn đóng góp ý kiến. Mình xin cảm ơn!