Location all the time with WorkManager!!

The WorkManager API makes it easy to specify deferrable, asynchronous tasks and when they should run. These APIs let you create a task and hand it off to WorkManager to run immediately or at an appropriate time.

Work Manager is used for running some background task which is not dependent on the state of the application. The task could run even when the application is in the background or even when it is closed.

We could achieve the same with other services such as JobScheduler, Firebase JobDispatcher or AlarmManager. But the benefit of the Work Manager is that you don’t have to write device logic to figure out what capabilities the device has and choose an appropriate API, instead, you can just hand your task off to WorkManager and let it choose the best option.

This article will demonstrate the working of the WorkManager by creating a simple android application.

Problem Statement

Creating a simple one screen application in which the last known location of the device will be recorded in every 15 minutes. The location should be stored locally into the application’s database even if it is closed or is in the background.

Let’s start with the solution.

First, add the necessary dependencies for the Work Manager into build.gradle file.

implementation "android.arch.work:work-runtime-ktx:1.0.0-alpha10"

That’s all you have to do to start using the library!!

Location ViewModel

Our ViewModel will contain the logic to enable location services, start and stop WorkManager and to get the locations stored in the Room database:

Companion object to create Work Manager TAG.

companion object {
const val LOCATION_WORK_TAG = "LOCATION_WORK_TAG"
}

Live data to get the updates.

// To get the status of Location Services
val enableLocation
: MutableLiveData<Response<Boolean>> = MutableLiveData()
// To get the saved location from Room database
val location: MutableLiveData<Response<List<Location>>> = MutableLiveData()

Setup Location Services: A simple method to enable GPS and safely access the location using Location Services. Location status is pushed into live data enableLocation which is observed in the View.

fun locationSetup() {
enableLocation.value = Response.loading()
LocationServices.getSettingsClient(application)
.checkLocationSettings(
LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest)
.setAlwaysShow(true)
.build())
.addOnSuccessListener {
enableLocation
.value = Response.success(true)
}
.addOnFailureListener {
Timber.e(it, "Gps not enabled")
enableLocation.value = Response.error(it)
}
}

Create a background task with WorkManager!!

fun trackLocation() {
val locationWorker =
PeriodicWorkRequestBuilder<TrackLocationWorker>(
15, TimeUnit.MINUTES)
.addTag(LOCATION_WORK_TAG)
.build()
WorkManager
.getInstance()
.enqueueUniquePeriodicWork(
LOCATION_WORK_TAG,
ExistingPeriodicWorkPolicy.KEEP,
locationWorker
)
}

Here I am using PeriodicWorkRequesBuilder as we have to repeat the task in every 15 minutes followed by adding the TAG using addTag(LOCATION_WORK_TAG) to later access locationWorker using the TAG.

Then start the WorkManager with:

WorkManager.getInstance()
.enqueueUniquePeriodicWork(
LOCATION_WORK_TAG,
ExistingPeriodicWorkPolicy.KEEP,
locationWorker
)

Here enqueueUniquePeriodicWork allows enqueueing a uniquely-named PeriodicWorkRequest, where only one PeriodicWorkRequest of a particular name can be active at a time.

Canceling the WorkManager: We can cancel the WorkManager by using cancelAllWorkByTag(LOCATION_WORK_TAG) with the TAG we added to locationWorker. You can also use cancelWorkById() or cancelUniqueWork() to cancel the task.

fun stopTrackLocation() {
WorkManager.getInstance().cancelAllWorkByTag(LOCATION_WORK_TAG)
}

Getting saved locations: Subscribe to the method created in the repository to get the locations from the Room database and pushing it into LiveData location which is observed in the View.

fun getSavedLocation() {
repository.location.getSavedLocation()
.fromWorkerToMain(scheduler)
.subscribeBy(
onNext = {
location
.value = Response.success(it)
},
onError = {
Timber.e(it, "Error in getting locations")
location.value = Response.error(it)
}
)
.addTo(getCompositeDisposable())
}

Work Manager

Create a TrackLocationWorker class that extends the abstract class Worker passing context and workerParams as parameters.

override onWork() method in which we will write the logic to fetch location and save location into the database.

I have created a method getLocation() in the location repository which helps in fetching the last device location and saving it into the database.

Return Result.SUCCESS if the work is performed properly else return Result.FAILURE. You can wrap the work in the try-catch block and return the execution state(SUCCESS or FAILURE) accordingly.

class TrackLocationWorker @Inject constructor(
context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
    @Inject lateinit var repository: Repository
    init {
Provider.appComponent?.inject(this)
}
    override fun doWork(): Result {
return try {
repository.location.getLocation()
Result.SUCCESS
} catch (e: Exception) {
Timber.e(e, "Failure in doing work")
Result.FAILURE
}
}
}

Note: I am using Dagger to inject the repository and to use it in the TrackLocationWorker class. To use WorkManager with Dagger we have to add some more code to the class. For that, start by adding this to TrackLocationWorker class.

init { Provider.appComponent?.inject(this) }

Create Provider object:

object Provider {
var appComponent: AppComponent? = null
}

And add this method to your Application class which is extending DaggerApplication()

override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
val appComponent = DaggerAppComponent.builder()
.application(this)
.build()
appComponent.inject(this)
Provider.appComponent = appComponent
return appComponent
}

Repository

In our repository we have the methods to fetch the location from the Location Services, to save the fetched location into the database and to get saved location from the database.

getLocation() method helps to get the last known location of the device and calls saveLocation() to save it into the database. You can change the code accordingly to get the latest location updates.

fun getLocation() {
if (isGPSEnabled() && checkLocationPermission()) {
LocationServices.getFusedLocationProviderClient(application)
?.lastLocation
?.addOnSuccessListener { location: Location? ->
if
(location != null) {
saveLocation(
Location(0,
location.latitude,
location.longitude,
System.currentTimeMillis()
)
)
}
}
}
}

saveLocation() is inserting the location object into the Room database. Kotlin’s Couritine is used to execute the task on the background thread and preventing database access on the main thread since database access on the main thread may potentially lock the UI for a long period of time.

private fun saveLocation(location: Location) = 
GlobalScope.launch { database.locationDao().insert(location) }

Now simply get the saved locations from the database.

fun getSavedLocation(): Flowable<List<Location>> =  database.locationDao().selectAll()

If you want to check the status of the WorkManager you can do that by observing the LiveData of the WorkManager.

Get the locationWorker by using the TAG LOCATION_WORK_TAG we added while creating the locationWorker object.

observe(WorkManager.getInstance()
.getStatusesByTagLiveData(MainViewModel.LOCATION_WORK_TAG)) {
it
?: return@observe
if (it.isEmpty()) return@observe
val status = it[0].state.name
Timber.d("Work Manager Status: $status")
}

Status could be one of the following at a time:

ENQUEUED -> Work is enqueued (hasn’t completed and isn’t running)

RUNNING -> Work is currently being executing.

SUCCEEDED -> Work has completed successfully.

FAILED -> Work has completed in a failure state.

BLOCKED -> Work that is currently blocked because its prerequisites haven’t finished successfully.

CANCELLED -> Work that has been canceled and will not execute.

Check out the project on Github.