Simple app to explore different ways of scheduling background task using WorkManager and post a notification to the user when the task is complete
This is just a very simple app that covers some basic WorkManager
usage. It shows you how to schedule:
one-time work request
periodic work request
and
- post a notification when the task is done.
For a complete guide, refer to the official WorkManager
guide here.
Add WorkManager Dependency
build.gradle.kts example
dependencies {
/*...*/
implementation ("androidx.work:work-runtime-ktx:2.7.1")
}
Inherit CoroutineWorker Class
To run a background task, you need to create a worker class that inherits the CoroutineWorker
class. The overridden doWork()
method is where you perform your background tasks.
class DemoWorker(
private val appContext: Context,
params: WorkerParameters)
: CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
delay(5000) //simulate background task
Log.d("DemoWorker", "do work done!")
return Result.success()
}
}
By default, the coroutine runs on
Dispatchers.Default
. To switch to a different coroutine dispatcher you can useCoroutineScope.withContext()
. For more details, you can visit my previous blog post here.
Instantiate WorkManager
WorkManager
is a Singleton. You can retrieve it by passing the application context to the WorkManager.getInstance()
API.
WorkManager.getInstance(applicationContext)
You can pass in activity context too, it gets converted to application context anyway.
Once you have the WorkManager
, you can set the work constraints and schedule the work request.
Set Work Constraints
You can specify work constraints for the work request. The following is an example of creating NetworkType
constraints. NetworkType.CONNECTED
means the work runs on when your phone is connected to the internet.
private val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
For other work constraints, refer to the official document here.
Schedule One-time Work Request
The following examples assume you have the following variable setup.
private lateinit var workManager: WorkManager
private val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
private val workName = "DemoWorker
This creates a one-time work request, with the NetworkType.CONNECTED
constraint. To schedule the work request, you can call WorkManager.enqqueneUniqueWork().
val workRequest = OneTimeWorkRequestBuilder<DemoWorker>()
.setConstraints(constraints)
.build()
workManager.enqueueUniqueWork(
workName,
ExistingWorkPolicy.REPLACE,
workRequest)
ExistingWorkPolicy.REPLACE
enum value means if the same work exists, it cancels the existing one and replaces it.
If your work requires higher priority to run, you can call the WorkRequest.Builder.setExpedited()
API. OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
means if the app is run out of expedited quota, it falls back to non-expedited/regular work requests.
val workRequest = OneTimeWorkRequestBuilder<DemoWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(constraints)
.build()
Schedule Periodic Work Request
val workRequest = PeriodicWorkRequestBuilder<DemoWorker>(
repeatInterval = 15,
TimeUnit.MINUTES
)
.build()
workManager.enqueueUniquePeriodicWork(
workName,
ExistingPeriodicWorkPolicy.REPLACE,
workRequest)
You use PeriodicWorkRequestBuilder<>()
to build a periodic work request. However, one very important note is the repeatInterval
must be equal to or greater than 15 minutes. This doesn't seem to be documented anywhere.
If you specify repeatInterval
less than 15 minutes, it just ignores your work request silently. Your app won't crash. There is this warning in your log, but I bet you likely won't see it. Bad decision, it should just crash the app in my opinion.
Interval duration lesser than minimum allowed value; Changed to 900000
When you call workManager.enqueueUniquePeriodicWork()
, your task runs immediately and runs again at a specified repeatInterval
. However, if you don't want to run the tasks immediately, you call the WorkRequest.Builder.setInitialDelay()
API.
val workRequest = PeriodicWorkRequestBuilder<DemoWorker>(
repeatInterval = 16,
TimeUnit.MINUTES
)
.setInitialDelay(5, TimeUnit.SECONDS)
.build()
The above code runs the first task after 5 seconds and repeats the task every 15 minutes.
Cancel Work Request
You can cancel the work request by passing in a unique work name parameter to the WorkManager.cancelUniqueWork()
API.
workManager.cancelUniqueWork(workName)
Declare POST_NOTIFICATIONS Permission
Starting on Android 13 (API level 33 / Tiramisu), you need to declare the notification permission and request it at run time.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--application element-->
</manifest>
Request Runtime Permission
This is a simple runtime permission dialog composable function that launches the permission request dialog in your app.
@Composable
fun RuntimePermissionsDialog(
permission: String,
onPermissionGranted: () -> Unit,
onPermissionDenied: () -> Unit,
) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
LocalContext.current,
permission) != PackageManager.PERMISSION_GRANTED) {
val requestLocationPermissionLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
onPermissionGranted()
} else {
onPermissionDenied()
}
}
SideEffect {
requestLocationPermissionLauncher.launch(permission)
}
}
}
}
To call it, you can just pass in the permission string and the callback tells you whether your permission is granted or denied.
@Composable
fun MainScreen(viewModel: MainViewModel) {
RuntimePermissionsDialog(
Manifest.permission.POST_NOTIFICATIONS,
onPermissionDenied = {},
onPermissionGranted = {},
)
}
[Update - July 15, 2023]: To properly implement the runtime permissions, you may want to read the following blog post.
Create Notification Channel
Starting from API 26 / Android Orea (Oeatmeal Cookie), a notification channel is required if you want to post a notification.
This is an example of creating a notification channel in the DemoWorker
coroutine worker class.
class DemoWorker(
private val appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
private val notificationChannelId = "DemoNotificationChannelId"
/*...*/
private fun createNotificationChannel()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
notificationChannelId,
"DemoWorker",
NotificationManager.IMPORTANCE_DEFAULT,
)
val notificationManager: NotificationManager? =
getSystemService(
applicationContext,
NotificationManager::class.java)
notificationManager?.createNotificationChannel(
notificationChannel
)
}
}
}
Create the Notification
To create the notification, you call NotificationCompat.Builder()
by passing in the application context and the notificationChannelId
that is used to create the notification channel in the previous step.
class DemoWorker(
private val appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
private val notificationChannelId = "DemoNotificationChannelId"
/*...*/
private fun createNotification() : Notification {
createNotificationChannel()
val mainActivityIntent = Intent(
applicationContext,
MainActivity::class.java)
var pendingIntentFlag by Delegates.notNull<Int>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingIntentFlag = PendingIntent.FLAG_IMMUTABLE
} else {
pendingIntentFlag = PendingIntent.FLAG_UPDATE_CURRENT
}
val mainActivityPendingIntent = PendingIntent.getActivity(
applicationContext,
0,
mainActivityIntent,
pendingIntentFlag)
return NotificationCompat.Builder(
applicationContext,
notificationChannelId
)
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle(applicationContext.getString(R.string.app_name))
.setContentText("Work Request Done!")
.setContentIntent(mainActivityPendingIntent)
.setAutoCancel(true)
.build()
}
/*...*/
}
The mainActivityPendingIntent
is used to start your app's main activity when the notification is clicked.
Override getForegroundInfo()
If you use notifications in your worker class, you need to also override the getForegroundInfo()
suspend function. Your app crashes without this override.
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
0, createNotification()
)
}
Post the Notification
To post a notification, you use NotificationManagerCompat.Notify()
API.
class DemoWorker(
private val appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
private val notificationChannelId = "DemoNotificationChannelId"
override suspend fun doWork(): Result {
/* task is complete */
if (ActivityCompat.checkSelfPermission(
appContext,
Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) {
with(NotificationManagerCompat.from(applicationContext)) {
notify(0, createNotification())
}
}
return Result.success()
}
/*...*/
}
Done
I also applied a very similar code to my simple RSS feed reader app to perform background article sync every 24 hours.
This is done by this SyncWorker class.
Source Code
GitHub Repository: Demo_WorkManager
Originally published at https://vtsen.hashnode.dev.
Top comments (0)