ZOFTINO.COM android and web dev tutorials

Scheduling Tasks with WorkManager in Android

If there are certain tasks in your app which must be executed regardless of whether the app is open or closed, then you can use android WorkManager library to schedule them. WorkManager API allows you to schedule tasks with constraints such as battery level, network connection, etc...

WorkManager API uses either JobScheduler or Firebase JobDispatcher or AlarmManager and BroadcastReceiver for scheduling tasks based on the availability of those services on the device. If JobScheduler is available on the device, it is used otherwise Firebase JobDispatcher is used. If Firebase JobDispatcher is not available on the device, AlarmManager and BroadcastReceiver option is used for scheduling tasks.

So with WorkManager API you don’t need to worry about the issue of backward compatibility when scheduling jobs on android.

For tasks which should execute only when the app is open or in the app’s process, you can use thread pool, see thread pool executor examples for more information.

Table of Contents

Features of WorkManager

WorkManger allows you to set constrains such as network connection, minimum battery level, state of charging of the device, idleness of the device, and storage level. Job will be run if all the defined constraints for it are met.

With WrokManger, you can chain tasks meaning after executing one task, next task can be configured to be executed and then next one.

Task can be passed arguments and it can return values.

Setup

To use work manager in your project, you need to add following dependency to module’s build.gradle file.

implementation 'android.arch.work:work-runtime:1.0.0-alpha09'

Steps to schedule Tasks Using WorkManger

Following are the steps to create a task and schedule it using worker manager library.

First, we need to create worker class by extending Worker and implementing doWork() method which contains the work that needs to executed.

public class RefreshDataWorker extends Worker {
    @NonNull
    @Override
    public Worker.Result doWork() {
        Context context = getApplicationContext();

        Log.d("RefreshDataWorker", "refreshing data....");
        return Worker.Result.SUCCESS;
    }
}

Then create work request using work request builder. There are two types of work requests which can be created, OneTimeWorkRequest and PeriodicWorkRequest. OneTimeWorkRequest is used for scheduling one time task. PeriodicWorkRequest is used for scheduling recurring tasks which need to be executed periodically.

OneTimeWorkRequest and PeriodicWorkRequest objects can be created using corresponding request builders.

OneTimeWorkRequest refreshWork =
                new OneTimeWorkRequest.Builder(RefreshDataWorker.class)
                        .build();

Then get WorkManager instance and queue the work request by calling enqueue() method on it.

WorkManager.getInstance().enqueue(refreshCpnWork);

You can call the code containing implementation for the steps mentioned above from the event handlers or when each time your app is started. For example, you can call from ViewModel the code which enqueues one time work request.

For recurring tasks, you need to make sure that the periodic task is scheduled once on a device. Once periodic task is scheduled and app is open, it will run every time after the specified time interval expires. You can see how this is implemented in the example.

Periodic Work Request

If work needs to be executed every time after expiry of a specified amount of time, PeriodicWorkRequest.Builder needs to be used to create periodic work request. The builder constructor takes repeat interval and time unit as arguments.

The minimum time interval between reruns of a task is 15 minute or 900000 seconds.

PeriodicWorkRequest refreshWork =
                new PeriodicWorkRequest.Builder(RefreshDataWorker.class, 25, TimeUnit.MINUTES)
                        .build();

Work Constraints

Constraints can be applied to work request to make the work run only when constraints are met. You can add constraints to make the job run only when the device uses a particular type of network, when device is idle, when device is charging, when device battery has certain minimum level of charging and/or when device storage is not low.

You can use Constraints.Builder to create Constraints object. It provides various methods such as setRequiredNetworkType, setRequiresBatteryNotLow, setRequiresCharging, setRequiresDeviceIdle and setRequiresStorageNotLow to define constraints.

To apply constraints to work, Constraints object needs to be added to work request builder by calling setConstraints method.

    Constraints constraints = new Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .setRequiresStorageNotLow(false)
            .build();


    OneTimeWorkRequest refreshCpnWork =
              new OneTimeWorkRequest.Builder(RefreshWorker.class)
			.setConstraints(constraints)
                        .build();

Works Chain

WorkManger allows you to specify a chain of tasks which needs to be executed in the specified order. The work sequence can contain only one time work requests. To define sequence of works, you need to create WorkContinuation object.

You can create WorkContinuation object using beginWith() method of WorkManger by adding the first task or work request object in the sequence to it. Next tasks in the sequence can be added to the WorkContinuation object by calling then() method on it and passing next work requests to it.

To enqueue the sequence, you need to call enqueue method on WorkContinuation.

        WorkManager.getInstance()
                .beginWith(refreshWork)
                .then(uploadWork)
                .enqueue();

Two or more work sequences can be combined into one sequence by using combine() method of WorkContinuation object. The main use of combining work sequences is that you can specify a task which needs to be run after completion of combined sequences independently. The works in the each combined sequence execute in the defined order. The work, added to workcontinuation object which is obtained by combine operation, will run after completion of the tasks defined in the sequences.

WorkContinuation sequenceOne = WorkManager.getInstance()
    .beginWith(refreshCouponsWork)
    .then(refreshDealsWork);
WorkContinuation sequenceTwo = WorkManager.getInstance()
    .beginWith(refreshSaleWork)
    .then(refreshCashbackWork);
WorkContinuation sequenceThree = WorkContinuation
    .combine(sequenceOne, sequenceTwo)
    .then(refreshBestOffersWork);
sequenceThree.enqueue();

Passing Arguments to Worker

You can pass data to worker by adding it to work request. To do that, first create Data object using Data.Builder and setting data. Then add the data object to work request object by calling setInputData() method.

Data source = new Data.Builder()
        .putString("workType", "OneTime")
        .build();
OneTimeWorkRequest refreshCpnWork =
        new OneTimeWorkRequest.Builder(RefreshLatestCouponWorker.class)
                .setInputData(source)
                .build();

Data can be retrieved in worker by calling getInputData() method.

String workType = getInputData().getString("workType");

Results from Worker

Worker can return status and data. To output data from worker, first data object with data needs to created and then by calling setOutputData() method and passing data object to it, the data can be made available to the caller.

Data refreshTime = new Data.Builder()
        .putString("refreshTime", ""+System.currentTimeMillis())
        .build();
setOutputData(refreshTime);

Caller can listen to the status of the work and get results from the worker by listening to live data object which can be obtained by calling getStatusById() method on WorkManger passing the work request id.

The live data object emits WorkStatus object which contains status and output data. Observe method of LiveData takes lifecycle owner as one of the arguments. You need to pass reference of your activity or fragment which implements LifeCycleOwner interface to it, see ViewModel class in the example below to know how it is implemented.

    WorkManager.getInstance().getStatusById(refreshCpnWork.getId())
        .observe(lifecycleOwner, status -> {
    if (status != null && status.getState().isFinished()) {
        String refreshTime = status.getOutputData().getString("refreshTime");
        Log.i("refreshCouponWork","refresh time"+refreshTime);
    }
});

Canceling Work

Tasks which are submitted to work manger can be cancelled by calling cancelWorkById() method passing worker id argument to it. You can cancel all the tasks submitted to work manger by using cancelAllWork method.

WorkManager.getInstance().cancelWorkById(refreshCpnWork.getId());

Scheduling Tasks using WorkManger Example

The example takes coupon app and shows how to schedule one time task and periodic task using work manger. The example task calls remote service to get latest coupon and updates the local database with latest coupon using Room.

One time work request is called from view model and periodic work request is scheduled once when app is started first time on the device. It uses shared preference to indentify whether the app is started first time or not.

android

Worker

import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;

import androidx.work.Data;
import androidx.work.Worker;

public class RefreshLatestCouponWorker extends Worker {

    private String url = "https://us-central1-zoftino-stores.cloudfunctions.net/";

    @NonNull
    @Override
    public Worker.Result doWork() {
        //read input argument
        String workType = getInputData().getString("workType");
        Log.i("refresh cpn work", "type of work request: "+workType);

        Context context = getApplicationContext();

        //get coupon and update local db using okhttp and room
        //run on a separate thread
        CouponsAPI couponsAPI = new CouponsAPI(url, context);
        try {
            couponsAPI.getLatestCoupon();
        }catch (Exception e){
            Log.e("refresh cpn work", "failed to refresh coupons");
        }

        //sending data to the caller
        Data refreshTime = new Data.Builder()
                .putString("refreshTime", ""+System.currentTimeMillis())
                .build();
        setOutputData(refreshTime);

        //sending work status to caller
        return Worker.Result.SUCCESS;
    }

}

Service Client

public class CouponsAPI {
    private String TAG = "CouponsAPI";
    private static final String couponService = "LatestCoupons";
    private String couponsServiceUrl = "";

    private Context context;

    public CouponsAPI(String url, Context ctx){
        couponsServiceUrl = url+couponService;
        context = ctx;
    }
    //get latest coupons from remote service using okhttp
    public void getLatestCoupon() {

        OkHttpClient httpClient = new OkHttpClient();

        HttpUrl.Builder httpBuilder =
                HttpUrl.parse(couponsServiceUrl).newBuilder();

        Request request = new Request.Builder().
                url(httpBuilder.build()).build();

        httpClient.newCall(request).enqueue(new Callback() {
            @Override public void onFailure(Call call, IOException e) {
                Log.e(TAG, "failed to get coupons");
            }
            @Override public void onResponse(Call call, Response response){
                ResponseBody responseBody = response.body();
                String resp = "";
                if (!response.isSuccessful()) {
                    Log.e(TAG, "failed response");
                }else {
                    try {
                        resp = responseBody.string();
                        saveOffer(resp);
                    } catch (IOException e) {
                        Log.e(TAG, "failed to read response " + e);
                    }
                }
            }
        });
    }

    //save latest coupon to room db on the device
    private void saveOffer(String resp){
        OfferDAO offerDAO = LocalRepository.getOfferDatabase(context).offerDAO();
        offerDAO.deleteOffers();

        offerDAO.insertLatestOffer(prepareDataObj(resp));
    }

    private Offer prepareDataObj(String resp){
        String[] offer = resp.trim().split("\\|");
        return new Offer(offer[0], offer[1]);
    }
}

Enqueue Task

import android.arch.lifecycle.LifecycleOwner;
import android.util.Log;

import java.util.concurrent.TimeUnit;

import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;

public class RefreshScheduler {

    public static void refreshCouponOneTimeWork(LifecycleOwner lifecycleOwner) {

        //worker input
        Data source = new Data.Builder()
                .putString("workType", "OneTime")
                .build();

        //One time work request
        OneTimeWorkRequest refreshCpnWork =
                new OneTimeWorkRequest.Builder(RefreshLatestCouponWorker.class)
                        .setInputData(source)
                        .build();
        //enqueue the work request
        WorkManager.getInstance().enqueue(refreshCpnWork);

        //listen to status and data from worker
        WorkManager.getInstance().getStatusById(refreshCpnWork.getId())
                .observe(lifecycleOwner, status -> {
                    if (status != null && status.getState().isFinished()) {
                        String refreshTime = status.getOutputData().getString("refreshTime");
                        Log.i("refreshCouponWork","refresh time: "+refreshTime);
                    }
                });
    }

    public static void refreshCouponPeriodicWork() {

        //define constraints
        Constraints myConstraints = new Constraints.Builder()
                .setRequiresDeviceIdle(false)
                .setRequiresCharging(false)
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .setRequiresBatteryNotLow(true)
                .setRequiresStorageNotLow(true)
                .build();

        Data source = new Data.Builder()
                .putString("workType", "PeriodicTime")
                .build();

        PeriodicWorkRequest refreshCpnWork =
                new PeriodicWorkRequest.Builder(RefreshLatestCouponWorker.class, 16, TimeUnit.MINUTES)
                        .setConstraints(myConstraints)
                        .setInputData(source)
                        .build();

        WorkManager.getInstance().enqueue(refreshCpnWork);
    }
}

ViewModel

public class OffersViewModel extends ViewModel {
    public LiveData<Offer> offer;
    private OfferDAO offerDAO;

    public OffersViewModel(Context ctx){
        offerDAO = LocalRepository.getOfferDatabase(ctx).offerDAO();

        //start one time task using work manager
        RefreshScheduler.refreshCouponOneTimeWork((LifecycleOwner)ctx);
    }

    public LiveData<Offer> getLatestCoupon(){
        if(offer == null){
            offer = offerDAO.getLatestOffer();
        }
        return offer;
    }

    public void setupPeriodicCpnRefreshWork(){
        RefreshScheduler.refreshCouponPeriodicWork();
    }
    public static class OffersViewModelFactory extends
            ViewModelProvider.NewInstanceFactory {
        private Context context;
        public OffersViewModelFactory(Context ctx) {
            context = ctx;
        }
        @Override
        public <T extends ViewModel> T create(Class<T> viewModel) {
            return (T) new OffersViewModel(context);
        }
    }
}

Activity

public class MainActivity extends AppCompatActivity {

    private OffersViewModel viewModel;
    private TextView latestCpn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        latestCpn = findViewById(R.id.latest_coupon);
        viewModel = ViewModelProviders.of(this,
                new OffersViewModel.OffersViewModelFactory(this))
                .get(OffersViewModel.class);

        viewModel.getLatestCoupon().observe(this, lcpn -> {
            if(lcpn != null) {
                latestCpn.setText(lcpn.getStore() + " " + lcpn.getOffer());
            }
        });
    }

    public void schedulePeriodicWork(View v){
        setupCouponRefreshPeriodicTask();
    }

    private void setupCouponRefreshPeriodicTask(){

        SharedPreferences preferences = PreferenceManager.
                 getDefaultSharedPreferences(this);

        //schedule recurring task only once
        if(!preferences.getBoolean("refreshTask", false)){
            viewModel.setupPeriodicCpnRefreshWork();

            SharedPreferences.Editor editor = preferences.edit();
            editor.putBoolean("refreshTask", true);
            editor.commit();
        }
    }
}

Complete code is available on github.