Background Tasks

Processing data in the background is a crucial aspect of developing an Android application that is responsive to users and adheres to Android platform standards. Performing tasks solely on the main thread can result in subpar performance, ultimately leading to a negative user experience and we all want to avoid that, right?

Moving blocking tasks away from the UI thread is a must-do if you want to have a smooth-running app. These tasks often involve time-consuming processes such as decoding bitmaps, accessing storage, working with machine learning models, or making network requests. By offloading these tasks, the UI thread can remain responsive, ensuring a smooth user experience.

So let’s start by establishing a few concepts.

What defines background work?

An app is running in the background when both the following conditions are satisfied:

  • None of the app’s activities are currently visible to the user.
  • The app isn’t running any foreground services – operations that are noticeable to the user – that started while an activity from the app was visible to the user.

    Otherwise, the app is running in the foreground.

Common types of background work

Background work falls into one of three primary categories:

Immediate: Needs to execute right away and complete soon.
Long Running: May take some time to complete.
Deferrable: Does not need to run right away.

Likewise, background work in each of these three categories can be either persistent or impersistent:

Persistent work: Remains scheduled through app restarts and device reboots.
Impersistent work: No longer scheduled after the process ends.

The bottom line is that persistent and impersistent tasks should be approached in different ways.

All persistent work: You should use WorkManager or Services for all forms of persistent work.
Immediate impersistent work: You should use Kotlin coroutines for immediate impersistent work.
Long-running and deferrable impersistent work: You shouldn’t use long-running and deferrable impersistent work. You should instead complete such tasks through persistent work using WorkManager or services.

WorkManager

Jetpack WorkManager is a powerful library provided by Jetpack, which is part of the Android Architecture Components. It is designed to help us schedule and manage background tasks in our Android app.

WorkManager offers a flexible and efficient way to execute tasks, making it easier to handle situations where the timing and execution conditions of your tasks might vary. It provides a unified API for different versions of Android, allowing your app to choose the most appropriate way to run background work.

It ensures that your tasks are executed, even if the app is closed or the device is rebooted. It also takes care of retrying failed tasks according to predefined retry policies.

You can define various constraints, such as network availability, charging status, and device idle state, to ensure that your tasks run at the most suitable times. WorkManager manages these constraints to optimize battery life and system resources.

Another good thing is that it uses the most appropriate implementation available on the device for running background tasks. It leverages JobScheduler on API 23+, Firebase JobDispatcher on Google Play Services devices, and a combination of AlarmManager and BroadcastReceiver for API 14-22.

It also integrates seamlessly with LiveData, allowing you to observe the state and progress of your tasks. You can also chain and schedule dependent tasks, define custom workers, and handle data passing between tasks.

📌 There are two types of work supported by WorkManager: OneTimeWorkRequest and PeriodicWorkRequest.

Implementation

To be able to use the WorkManager library, the first thing we need to do is to add the WorkManager dependency to your app’s build.gradle file. You can do this by adding the following line to the dependencies section:

Then we can define a worker class that extends the Worker class. This is where you’ll implement the background task logic. For example:

import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters 

class MyWorker(
context: Context, 
params: WorkerParameters
) : Worker(context, params) {     

override fun doWork(): Result {

 // TODO: Perform your background task here         
// Return the desired Result (SUCCESS, FAILURE, or RETRY) 
        
return Result.success()     
   }
 } 

Create an instance of your worker class and schedule it to run using the WorkManager. For example, you can schedule the task to run once using OneTimeWorkRequest:

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

...
val workRequest = OneTimeWorkRequest.Builder(MyWorker::class.java).build() WorkManager.getInstance(context).enqueue(workRequest) 

You can also add additional constraints to your work request to control when the task should run. For example, you can require network connectivity, device charging, or device idle state. Here’s an example:

That’s it! Your background task is now scheduled to run using WorkManager. You can observe its progress and state using LiveData and handle data passing between tasks using Data objects.

Please note that this is just a basic example. WorkManager provides many more features and capabilities, such as work chains, custom workers, data persistence, and more. Be sure to refer to the official Android documentation for more details and advanced usage.

Overall, Jetpack WorkManager simplifies the implementation of background tasks in your Android app, providing a reliable and efficient solution for executing work asynchronously.

Services

A Service is an application component that can perform long-running operations in the background.

Once initiated, a service can remain active for an extended period, even when the user switches to a different application. A Service can be useful for example for downloading a file. Additionally, other components can establish a connection with the service to interact with it and facilitate interprocess communication (IPC). For instance, a service can manage network transactions, play audio, manage file input/output operations, or interact with a content provider, all while operating in the background.

📌 Even though we are talking about background tasks, services can also perform foreground tasks, so I will briefly mention it below.

So, there are three types of services:

Foreground
A foreground service performs some operation that is noticeable to the user. For example, an audio app would use a foreground service to play an audio track. Foreground services must display a Notification. Foreground services continue running even when the user isn’t interacting with the app.

When you use a foreground service, you must display a notification so that users are actively aware that the service is running. This notification cannot be dismissed unless the service is either stopped or removed from the foreground.

Background
A background service performs an operation that isn’t directly noticed by the user. For example, if an app used a service to compact its storage, that would usually be a background service.

Bound
A service is bound when an application component binds to it by calling bindService(). A bound service offers a client-server interface that allows components to interact with the service, send requests, receive results, and even do so across processes with interprocess communication (IPC). A bound service runs only as long as another application component is bound to it. Multiple components can bind to the service at once, but when all of them unbind, the service is destroyed.

📌 Note: If you must perform work outside of your main thread, but only while the user is interacting with your application, you should probably use a Coroutine instead of a service.

Starting a Service

There are three main ways to create a service.

You can start a service from an activity or other application component by passing an Intent to startService() or startForegroundService(). The Android system calls the service’s onStartCommand() method and passes it the Intent, which specifies which service to start

📌 Note: To ensure that your app is secure, always use explicit intent when starting a Service and don’t declare intent filters for your services. Using an implicit intent to start a service is a security hazard because you cannot be certain of the service that responds to the intent, and the user cannot see which service starts. Beginning with Android 5.0 (API level 21), the system throws an exception if you call bindService() with an implicit intent.

A started service is responsible for handling its own lifecycle. In other words, the system doesn’t automatically stop or destroy the service unless it needs to free up system memory. The service will keep running even after the onStartCommand() method returns. To stop the service, it can either call stopSelf(), or it can be stopped by another component using stopService().

A bound service enables application components to establish a long-standing connection by using the bindService() method. It is primarily used for interaction with the service from activities and other components in the application, as well as for exposing certain functionalities of the application to other applications through interprocess communication (IPC).

To create a bound service, you need to implement the onBind() callback method. This method should return an IBinder object that defines the interface for communication with the service. By calling bindService(), other application components can obtain the interface and start invoking methods on the service.

A scheduled service can be used if we want to have a service that executes at some point in the future or when some condition is met we can create a special type of service called a jobService() and then we define when the job should run using a scheduler like jobScheduler or Firebase jobDispatcher.

But when you should use a Service and when you should use WorkManager?

Both Android Services and WorkManager are tools for performing background tasks in Android applications, but they are designed for different use cases and have different characteristics. It does not mean it’s simple, though. Is not as easy as with coroutines. If you need impersistent, use coroutines, if you need persistent, use Services.

You can use a Service if you need to perform long-running operations, such as downloading large files, processing data, or continuously monitoring sensors, that don’t require strict periodic scheduling. Another good example is when you want to perform tasks in the background while keeping the user aware of them. For example, playing music, tracking the user’s location, or performing real-time data synchronization. Or if you need a way for your app’s components (like Activities or Fragments) to directly communicate with a background task or service. Bound Services allow for this kind of interaction as I mentioned above. Last but not least, if you require fine-grained control over the execution of background tasks, including threading and concurrency management, using Services would be a good option.

The best use cases for a WorkManager are when you need to schedule tasks to run at specific times or under certain conditions, such as when the device is idle, connected to Wi-Fi, or has sufficient battery. WorkManager also provides mechanisms to ensure that tasks are executed, even if the app is killed or the device is rebooted. It handles retries and back-off policies by default. WorkManager takes care of executing tasks efficiently to minimize battery consumption and resource usage, respecting Doze mode, and other Android resource optimization features and it lets you define complex chains of tasks.

When it comes to background tasks in Android development, it is crucial to prioritize efficient and responsive user experiences. By offloading blocking tasks from the UI thread and embracing background work techniques, such as the powerful Jetpack WorkManager library and Services, we can ensure that our apps run smoothly, even during complex and time-consuming operations. Whether it’s immediate, long-running, or deferrable work, implementing the right approach using WorkManager and Services will help maintain performance, optimize battery life, and provide a reliable solution for executing tasks asynchronously. With these tools at our disposal, we can enhance our Android applications and deliver a seamless user experience.

Discover more from { Code Journey; }

Subscribe now to keep reading and get access to the full archive.

Continue reading