Application guide and project practice of WorkManager of Android architecture components

Posted by davidforbes on Mon, 07 Feb 2022 14:26:51 +0100

Let's start with an introduction video of WorkManger: Chinese official introduction video (mainly because the little sister looks good ~)

Usage scenario

WorkManager is applicable to the needs that tasks need to continue to be executed after the application exits (such as the application data reporting server). For those cases that tasks also need to be terminated after the application exits, ThreadPool and AsyncTask need to be selected to implement.

definition

Official introduction:

WorkManger is a compatible, flexible and simple deferred background task.

In the end what is it?

WorkManager is an API that allows you to easily schedule deferred asynchronous tasks that should run even when you exit the application or restart the device.

To put it simply:

WorkManager is a function library encapsulated by asynchronous tasks.

Why choose WorkManager?

There are many options for processing background tasks in Android, such as Service, DownloadManager, AlarmManager, JobScheduler, etc. What are the reasons for choosing WorkManager?

  • Strong version compatibility, backward compatibility to API 14.

  • You can specify constraints. For example, you can choose to execute under the condition of network.

  • It can be executed regularly or once.

  • Monitor and manage task status.

  • Multiple tasks can use task chains.

  • Ensure the execution of the task. If the current execution conditions are not met or the App process is killed, it will wait until the next time the conditions are met or the App process is opened.

  • Support power saving mode.

How to select multithreaded tasks

Background tasks will consume the system resources of the device. If not handled properly, it may cause a sharp consumption of power of the device and bring a bad experience to users. Therefore, each developer should pay attention to selecting the correct background processing method. The following is the official selection method:

This decision tree helps you determine which category is best for your background tasks.

Recommended solutions:

The following sections describe the recommended solutions for each background task type.

Immediate task

For tasks that should end when the user leaves a specific scope or completes an interaction, we recommend using Kotlin synergy . many Android KTX Libraries contain components that are suitable for common applications, such as ViewModel )And common applications life cycle Out of the box collaboration scope.

If you are a Java programming language user, see Thread processing on Android , learn about the recommended options.

For tasks that should be executed immediately and need to continue to be processed, we recommend using it even if the user runs the application in the background or restarts the device WorkManager And use it to Long running tasks Support.

In certain circumstances, such as when using media playback or active navigation, you may want to use it directly Front desk service.

Deferred task

All tasks that are not directly related to user interaction and can be run at any time in the future can be postponed. Recommended for deferred tasks WorkManager Solution.

If you want some deferred asynchronous tasks to run normally even after the application exits or the device restarts, you can easily schedule these tasks with WorkManager. To learn how to schedule these types of tasks, see WorkManager Relevant documents.

Precise task

Tasks that need to be performed at a precise point in time can be used AlarmManager.

For more information on AlarmManager, see Set repeated alarm time.

Enter the source code

Introduction to WorkManager related classes

Worker

Worker is used to specify specific tasks to be performed. The specific logic of the task is written in the worker. Worker is an abstract class. So we need to inherit and implement this class to define our own tasks.

public abstract class Worker extends ListenableWorker {
    // Package-private to avoid synthetic accessor.
    SettableFuture<Result> mFuture;
    @Keep
    @SuppressLint("BanKeepAnnotation")
    public Worker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }
 	 /**
     * Task logic
     * @return The execution of the task, success, failure, or need to be re executed
     */
    @WorkerThread
    public abstract @NonNull Result doWork();

    @Override
    public final @NonNull ListenableFuture<Result> startWork() {
        mFuture = SettableFuture.create();
        getBackgroundExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result result = doWork();
                    mFuture.set(result);
                } catch (Throwable throwable) {
                    mFuture.setException(throwable);
                }

            }
        });
        return mFuture;
    }
}

In the parent class ListenableWorker:

/**
     * The input data of the task may need us to transfer parameters sometimes. For example, when downloading files, we need to transfer the file path,
     * Get the parameters passed to us in the function getdoinput()
     * @return Data parameter
     */
    public final @NonNull Data getInputData() {
        return mExtras.getInputData();
    }

    /**
     * Set task output results
     * @param outputData result
     */
    public final void setOutputData(@NonNull Data outputData) {
        mOutputData = outputData;
    }

Return value of doWork() function:

  • Worker.Result.SUCCESS: the task was executed successfully.
  • Worker.Result.FAILURE: task execution failed.
  • Worker.Result.RETRY: the task needs to be re executed and needs to cooperate with workrequest The setBackoffCriteria() function in builder is used.

WorkRequest

WorkRequest represents a separate task and is the packaging of Worker tasks. A WorkRequest corresponds to a Worker class.

We can add constraint details to the Worker class through WorkRequest, such as specifying the environment in which the task should run, the input parameters of the task, and the task can be executed only when there is a network.

The base class for specifying parameters for work that should be enqueued in WorkManager. There are two concrete implementations of this class: OneTimeWorkRequest and PeriodicWorkRequest.

WorkRequest is an abstract class. There are also two corresponding subclasses in the component:

  • Onetimeworkrequest (the task is executed only once)

  • Periodicworkrequest (periodic execution of tasks).

Introduction to common functions in WorkRequest

/**
     * Get UUID corresponding to WorkRequest
     */
    public @NonNull UUID getId();

    /**
     * Get UUID string corresponding to WorkRequest
     */
    public @NonNull String getStringId();

    /**
     * Obtain the workspec corresponding to WorkRequest (including some details of the task)
     */
    public @NonNull WorkSpec getWorkSpec();

    /**
     * Get the tag corresponding to WorkRequest
     */
    public @NonNull Set<String> getTags();

    public abstract static class Builder<B extends WorkRequest.Builder, W extends WorkRequest> {
        ...

        /**
         * Set the backoff / retry policy of the task. For example, we return result in the doWork() function of the Worker class Retry to rejoin the mission.
         */
        public @NonNull B setBackoffCriteria(
            @NonNull BackoffPolicy backoffPolicy,
            long backoffDelay,
            @NonNull TimeUnit timeUnit);


        /**
         * Set the restrictions on the operation of tasks, such as executing tasks when there is a network, not when the power is low
         */
        public @NonNull B setConstraints(@NonNull Constraints constraints);

        /**
         * Set the input parameters of the task
         */
        public @NonNull B setInputData(@NonNull Data inputData);

        /**
         * Set the tag of the task
         */
        public @NonNull B addTag(@NonNull String tag);

        /**
         * Set the saving time of task results
         */
        public @NonNull B keepResultsForAtLeast(long duration, @NonNull TimeUnit timeUnit);
        @RequiresApi(26)
        public @NonNull B keepResultsForAtLeast(@NonNull Duration duration);
        ...
    }

Note: the usage scenarios of Builder's setBackoffCriteria() function are quite common here.

Generally, this function is used when the task needs to be retried when the task execution fails. When the task execution fails, the doWork() function of the Worker class returns result Retry tells the task to retry.

The retry strategy is set through the setBackoffCriteria() function.

BackoffPolicy has two values: linear (the time of each retry increases linearly, such as 10 minutes for the first time and 20 minutes for the second time) and exponential (the time of each retry increases exponentially).

WorkManager

To manage task requests and task queues, we need to pass the WorkRequest object to WorkManager to queue tasks. Scheduling tasks through WorkManager to distribute the load of system resources.

Introduction to common functions of WorkManager

    /**
     * Task team
     */
    public final void enqueue(@NonNull WorkRequest... workRequests);
    public abstract void enqueue(@NonNull List<? extends WorkRequest> workRequests);

    /**
     * When using a chain structure, start with which tasks.
     * For example, we have three tasks a, B and C, which we need to perform in sequence. Then we can work manager getInstance(). beginWith(A). then(B). then(C). enqueue();
     */
    public final @NonNull WorkContinuation beginWith(@NonNull OneTimeWorkRequest...work);
    public abstract @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);


    /**
     * Create a unique work queue. Tasks in the unique work queue cannot be added repeatedly
     */
    public final @NonNull WorkContinuation beginUniqueWork(
        @NonNull String uniqueWorkName,
        @NonNull ExistingWorkPolicy existingWorkPolicy,
        @NonNull OneTimeWorkRequest... work);
    public abstract @NonNull WorkContinuation beginUniqueWork(
        @NonNull String uniqueWorkName,
        @NonNull ExistingWorkPolicy existingWorkPolicy,
        @NonNull List<OneTimeWorkRequest> work);

    /**
     * It is allowed to put a PeriodicWorkRequest task into a unique work sequence, but when there is this task in the queue, you need to provide a replacement strategy.
     */
    public abstract void enqueueUniquePeriodicWork(
        @NonNull String uniqueWorkName,
        @NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
        @NonNull PeriodicWorkRequest periodicWork);

    /**
     * Cancel task via UUID
     */
    public abstract void cancelWorkById(@NonNull UUID id);

    /**
     * Cancel a task by tag
     */
    public abstract void cancelAllWorkByTag(@NonNull String tag);

    /**
     * Cancel all tasks in the unique queue (beginUniqueWork)
     */
    public abstract void cancelUniqueWork(@NonNull String uniqueWorkName);

    /**
     * Cancel all tasks
     */
    public abstract void cancelAllWork();

    /**
     * Gets the WorkStatus of the task. Generally, the return value is obtained through WorkStatus. LiveData can sense the changes of WorkStatus data
     */
    public abstract @NonNull LiveData<WorkStatus> getStatusById(@NonNull UUID id);
    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag);

    /**
     * Get the WorkStatus of all tasks (beginUniqueWork) in the unique queue
     */
    public abstract @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWork(@NonNull String uniqueWorkName);

The only difference between the queues opened by the beginWith(), beginUniqueWork() functions is whether the tasks in the queue can be repeated.

  • The tasks in the queue started by beginWith() can be repeated;

  • The tasks in the queue started by beginUniqueWork() cannot be repeated.

Data

Data is used to set input parameters and output parameters for workers.

For example, if we need to download a picture on the network, we need to pass in the download address (input parameters) to the Worker. After the Worker executes successfully, we need to obtain the local holding path (output parameters) of the picture. The incoming and outgoing are realized through Data.

Data is a lightweight container (no more than 10KB). Data saves information in the form of key value.

Other categories are omitted

WorkStatus

It contains the status of the task and the information of the task, which is provided to the observer in the form of LiveData.

Demo quick use

  1. Add dependency

    def versions = "2.2.0"
    implementation "androidx.work:work-runtime:$versions"
    
  2. Define Worker

    We define the MainWorker to inherit the Worker. We find that the doWork method needs to be rewritten and the status of the task needs to be returned. WorkerResult:

    class MainWorker : Worker() {
        override fun doWork(): WorkerResult {
            // Tasks to perform
            return WorkerResult.SUCCESS
        }
    }
    
  3. Define WorkRequest

    val request = OneTimeWorkRequest.Builder(MainWorker::class.java).build()
    
  4. Join task queue

    WorkManager.getInstance(context).enqueue(request)
    

After joining the task queue, the task will be executed immediately. Whether it is really implemented depends on whether the environment meets the constraints (such as networking).

Other scenarios:

Such as chain call:

WorkManager.getInstance(context)
        .beginWith(workA)
        .then(workB)
        .then(workC)
        .enqueue()

Environmental constraints:

val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)  // Network status
        .setRequiresBatteryNotLow(true)                 // Not performed when the battery is low
        .setRequiresCharging(true)                      // During charging
        .setRequiresStorageNotLow(true)                 // Do not execute when storage capacity is insufficient
        .setRequiresDeviceIdle(true)                    // API 23 is required to execute in standby mode
        .build()

val request = OneTimeWorkRequest.Builder(MainWorker::class.java)
        .setConstraints(constraints)
        .build()

Periodic tasks:

val request = PeriodicWorkRequest
        .Builder(MainWorker::class.java, 15, TimeUnit.MINUTES)
        .setConstraints(constraints)
        .setInputData(data)
        .build()

Use test:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()

        val dateFormat = SimpleDateFormat("hh:mm:ss", Locale.getDefault())
        val data = Data.Builder()
                .putString("date", dateFormat.format(Date()))
                .build()

        val request = OneTimeWorkRequest
                .Builder(MainWorker::class.java)
                .setConstraints(constraints)
                .setInputData(data)
                .build()

        WorkManager.getInstance(context).enqueue(request)

        WorkManager.getInstance(context)
                .getStatusById(request.id)
                .observe(this, Observer<WorkStatus> { workStatus ->
                    if (workStatus != null && workStatus.state.isFinished) {
                        Log.d("MainActivity",
                                workStatus.outputData.getString("name", ""))
                    }
                })

    }
}

Before opening the application, first turn off the network. After opening, it is found that the Worker has no printing time. Then connect the network and you will see the printing time.

That's why we talked about workmanager getInstance(context). Enqueue (request) is to add a task to the task queue, which does not mean to execute the task immediately, because the task may not be executed until the environmental conditions are met.

Project practice

Demand background: after receiving the instruction from the server, the device will upload the relevant performance log server to facilitate the analysis of the performance of the device at the server.

Pseudo code implementation:

class RequestLogCommand : IPushActionCommand {
		// After receiving the relevant instructions from the system, start to perform log related operations in the background and upload them to the server
    override fun onMessageArrived(context: Context, entity: MqttPushEntity) {
        val workManager: WorkManager = WorkManager.getInstance(AppContext.getContext())
        val uploadSystemLog: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java).build()
        workManager.enqueue(uploadSystemLog)
    }

    class UploadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
        override fun doWork(): Result {
            return try {
              // do something
                Log.i(TAG, "UploadWorker - success")
                Result.success()
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, "UploadWorker failure", e)
                Result.failure()
            }  finally {
              // do something
            }
        }

    }

    companion object {
        private const val TAG = "RequestLogCommand"
    }
}

The call timing of this class is implemented according to the needs of the project. In the project, it is executed after receiving the message. This code focuses on the use characteristics of several important classes.

Other scenes will be improved later~

reference resources:

Background processing Guide

WorkManager basics

Topics: Android kotlin architecture