Wednesday, March 13, 2019

Android WorkManager in practice Part I: One Time Workers

WorkManager is an API to schedule deferrable, asyncronous jobs. This is one of the JetPack's best component and it's released only one week ago. Now I want to introduce it in a nutshell. To be honest, I've been waiting for a library like this FOR YEARS.

Test Environment:

  • Windows 7 64Bit
  • Android Studio 3.3.2
  • Java 8

Add the WorkManager dependencies:
Open your project level build.gradle, and put into the repositories before all sub elements like this:


As a next step, you should add the library to your app level build.gradle:

The second is required only if you want to create tests for your WorkManager classes. Press "Sync now" to download dependencies.

Create a Worker 

Worker is a runnable WorkManager based task. You don't have to worry about starting or managing runner threads, because everything is handled by WorkManager. I'll create an example, which downloads a remote JSON file. I'll use Retrofit2 as a HTTP client.

package ...;
import android.content.Context;
import android.support.annotation.NonNull;

import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class JSONLoaderWorker extends Worker {

    JSONLoaderWorker(
            @NonNull Context context,
            @NonNull WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Result doWork() {
        // TODO Impl logic
        return Result.success();
    }
}

As you can see, when you create a new Worker, you have to override the onWork() method, which contains the concrete task, and it returns with a Result object, which has 3 possible output:
  • Use Result.success(), when your job run successfully, without any problem.
  • Use Result.failure(), when your job failed, and you don't want to restart it immediately after the failure.
  • Use Result.retry(), when the job needs to be retried later.

Implement the Worker logic

As I mentioned, the Worker'll download a JSON file via Retrofit2. You can check it here:


So, if you don't have it, add Retrofit2 as a dependency in your app level build.gradle:

Then press "Sync now". 

Next, open the AndroidManifest.xml, and add the INTERNET permission to your app:

<uses-permission android:name="android.permission.INTERNET" />

Without this, your app will not be able to connect to the internet.

Ok, it's time to implement the Retrofit components. First, create a Config file, which'll contain some necessary information:

public class Config {
    public static final String LOG_TAG = "WorkManager Demo Part 1";
    public static final String JSON_URL = "http://mysafeinfo.com/api/";
}

Then, we have to create the DTO:

public class King {
    private long id;
    private String nm;
    private String cty;
    private String hse;
    private String yrs;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getNm() {
        return nm;
    }

    public void setNm(String nm) {
        this.nm = nm;
    }

    public String getCty() {
        return cty;
    }

    public void setCty(String cty) {
        this.cty = cty;
    }

    public String getHse() {
        return hse;
    }

    public void setHse(String hse) {
        this.hse = hse;
    }

    public String getYrs() {
        return yrs;
    }

    public void setYrs(String yrs) {
        this.yrs = yrs;
    }

    @Override
    public String toString() {
        return "King{" +
                "id=" + id +
                ", nm='" + nm + '\'' +
                ", cty='" + cty + '\'' +
                ", hse='" + hse + '\'' +
                ", yrs='" + yrs + '\'' +
                '}';
    }
}

Let's create a Retrofit helper class, a singleton, which'll responsible for managing the Retrofit instance:

import android.content.Context;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {

    private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
    private static RetrofitClient instance;
    final Retrofit retrofit;

    private RetrofitClient(Context context) {
        final OkHttpClient.Builder client = new OkHttpClient.Builder();
        client.connectTimeout(15, TimeUnit.SECONDS);
        client.readTimeout(15, TimeUnit.SECONDS);
        client.writeTimeout(15, TimeUnit.SECONDS);

        final Gson gson = new GsonBuilder().setDateFormat(DATE_FORMAT).create();
        retrofit = new Retrofit.Builder().client(client.build()).baseUrl(Config.JSON_URL)
                .addConverterFactory(GsonConverterFactory.create(gson)).build();
    }

    public static RetrofitClient getInstance(Context context) {
        if(instance == null) {
            instance = new RetrofitClient(context);
        }
        return instance;
    }

    public Retrofit getRetrofit() {
        return retrofit;
    }
}

Now we'll create the Retrofit interface, which'll download the data:

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;

public interface GetDataService {

    @GET("data?list=englishmonarchs&format=json")
    Call<List<King>> listKings();
}
Finally we have to use the Retrofit in our Worker class:
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;

import java.io.IOException;
import java.util.List;

import androidx.work.Worker;
import androidx.work.WorkerParameters;
import retrofit2.Response;

public class JSONLoaderWorker extends Worker {

    // (1) Add the endpoint as a private class variable
    private GetDataService endpointService;

    JSONLoaderWorker(
            @NonNull Context context,
            @NonNull WorkerParameters params) {
        super(context, params);
        // (2) Initialize the variable with the RetrofitClient
        endpointService = RetrofitClient.getInstance(context).getRetrofit().create(GetDataService.class);
    }

    @Override
    @NonNull
    public Result doWork() {
        Log.i(Config.LOG_TAG, "Worker started");
        try {
            // (3) Call the endpoint
            Response<List<King>> response = endpointService.listKings().execute();
            List<King> resultList = response.body();
            if(resultList != null) {
                for(King king : resultList) {
                    Log.d(Config.LOG_TAG, king.toString());
                }
            }
        } catch (IOException e) {
            Log.e(Config.LOG_TAG, "Exception occured: " + e.getLocalizedMessage());
            e.printStackTrace();
            return Result.failure();
        }
        return Result.success();
    }
}

And that's it.

Run the Worker

Ok, you have a Worker, but it's not running, it needs to be scheduled. One time Workers  can scheduled the following way:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        OneTimeWorkRequest configCheckerWorkRequest = new OneTimeWorkRequest.Builder(
                JSONLoaderWorker.class)
                .build();
        WorkManager.getInstance().enqueue(configCheckerWorkRequest);
    }
}

If you start your application, you can find the downloaded data in your Logcat console:


Cancel the Worker

If you want to stop the running Worker, you can do it like this:

WorkManager.cancelWorkById(configCheckerWorkRequest.getId());

If you do not have the reference to the WorkerRequest, you can cancel it by the tag. If you want to do it on this way, first you should set a unique tag for this request:

OneTimeWorkRequest configCheckerWorkRequest = new OneTimeWorkRequest.Builder(JSONLoaderWorker.class)
 .addTag("unique_tag")
 .build();

When you cancel a Worker by tag, the WorkManager will cancel all Worker which has the same tag:

WorkManager.cancelAllWorkByTag("unique_tag");

Summary

You can find the whole project on GitHub:

In the next article, we'll discuss the recurring Workers and constraints.

Configure and use VSCode for Java web development

Embarking on Java web development often starts with choosing the right tools that streamline the coding process while enhancing productivity...