This is a continuation of a post for building an android app for displaying GitHub contributions of a user. We are pulling down profile and repository details of the user. As we are adding features to the app, we have been learning about android and its development model.
Currently we are at the point that we can download the data asynchronously and display it on the UI. We can also change user and the data is downloaded for the new user. In this post, we want to build up on this by moving all the heavy lifting for downloading the data to a background service.
What is Service?
Services are used to run a long-running operation in the background without affecting app’s responsiveness. Services are faceless i.e. there is no UI for a service. They can be used to download or process information. They also can provide feedback to the app. Services can be used by more than one app.
The lifetime of service can be independent of the app launching the service. They can also be bound the apps and die as soon as the number of apps bound to the service drops to zero. There are two non-mutually exclusive forms of service:
- Started: In this form, a service is invoked by calling startService(). In this way a service can run indefinitely. We will be using our service in this form as we want to keep the data current by downloading it periodically every 3 hours.
- Bound Service: This form is used for the service designed for client / server operations. They are used when an application needs to delegate some actions to the service and needs results back. An application uses bindService() to use a service in this form.
A service can be public or private. A private service can only be used by the app providing the service. On the other hand, a public service can be started by any app on the device using Intent.
Android Service & Threading
A service is invoked in the main thread of app hosting the service. But we can start a new thread in a service to run the operations in a non-blocking mode. In our case, we will keep our operations in AsyncTask for downloading user profile and repository data.
Showing Loading… before downloading data
Since we have decided to move the logic for downloading data from activity to a background service, we need to show some info to the user on the screen. Let’s keep it simple and show the text Loading… on UI.
Now if the user is not connected to the network and no data is available then we can show Not Connected… message. We included a separate resource definition for this message previously. Even if old data (older than 3 hours), we can still show this data to the user. In case, user is connected to the network and data is current (less than 3 hours old), we can just show this data. If the data is not current, then we can download the data and show it. The following is the resource definition:
The service needs to periodically download data from GitHub in order to keep it current.
GitHub Contributions App States
There are three states of our app. These states are as follows:
- Not Connected
There are few state transitions allowed. When the application is launched, it is in Loading state. In this state, it starts the service. As many times the service is started, it generates a connectivity intent to notify the starter with the state of network connectivity. This transitions the state to Not Connected or Connected.
The Connected state is kind of misnomer for network state. It is not network state. It is to show to the user that there is some data available to be shown on the screen. This might not be the most recent data if the device is currently not connected to the network. And if the device is not connected, I don’t think that the user concerns a lot by looking at the most recent and updated data.
The Not Connected state is only available when there is no suitable data available to be shown for the user set in the preference settings.
Service Vs IntentService
All android services inherit from Service. You can create a service type by directly inheriting from Service class. This allows multiple simultaneous use of the service in a multhreaded-multirequest scenario. But if your service is not expected to be used simultaneously (as is the general case), then you can extend IntentService (which also inherits from Service).
IntentService uses a work queue to handle requests in a serial fashion. It uses a single worker thread to handle all the requests. The request’s Intent is provided in the onHandleIntent() method, we can use it to pull the required parameters.
But IntentService automatically stops itself when the work is done. We want the service to be running even if the app terminates, so IntentService shouldn’t be our first choice in this case. Instead we can create a simple service inheriting from Service type. IntentService also inherits from Service.
Running Service when device starts up
There are a few things we need to do to startup service at device startup. They are as follows:
– Request permissions to handle device startup intent.
– Registering service description in AndroidManifest.xml.
– Handler to handle the startup intent so that the service can be starteup
– Definition of broadcast receiver and its registration in AndroidManifest.xml
Since a service cannot be directly started, we need a BroadcastReceiver to start the service. The receiver is registered with intent-filter to handle the device startup intent.
Using Intent to pass custom objects
We can add information to Intent, which can be pulled by a receiver. The added info is called Extra. There are a number of putExtra() overrides in Intent to add extras of various primitive types. In order to add custom objects, it becomes a little more challenging.
Since intents can be received by outside process, we cannot really expect the other process to have same type definitions. In order to pass an object of a custom type, there are a number of options.
First, if an object is serializable, it can simply be added as an extra. So implement your type as serializable, you should be good to go. You must realize that you wouldn’t be getting the same object on the other side but a serialized copy of the object. When you would deserialize it, this is a new object created on a different place in memory.
You can implement Parcelable for the type. Read more about Parcelable.
The easiest option is to serialize it using Json. Since we are already using Gson in our project, we can simply serialize it using Gson. On the other side, we can deserialize the Gson back to the original type.
Passing Data from Service to Activity
In our case we have an activity (GitHubCardActivity) and a service (GitHubSyncService). When the app is launched, the activity can start a service if it is not already running. This should also be the case when the app is launched on a device. In the other case, the service should also be started when the device starts up (when no activity is yet created). So service doesn’t know if there is a corresponding activity interested in the data just downloaded. In this case, Intent makes our job a lot easier as we can generate the Intent from the service, if there is a corresponding activity interested in receiving the data, receives it. In order to keep this responsibility separate, let’s create a BroadcastReceiver.
A BroadcastReceiver can receive broadcasts for a number of actions. Here we are using GitHubCardActivityReceiver to receive broadcasts for UpdateConnectedStatusAction, UpdateUserProfileAction and UpdateUserRepositoriesAction. These broadcasts would be generated from GitHubSyncService as we have already seen in the above code.
Instead of the whole JSON path, we can simply push that data to the storage anytime we download the info. We can always read the info in our service from storage. This would save us the conversion time and make it easier for us to read from storage (which we already have the code for). So let’s keep it simple in our case.
Updating Connectivity Receiver
The connectivity receiver also needs an update. Since service can run independently of the activity, it can be updated to notify the service of any changes in network connectivity.
We have also registered the BroadcastReceiver with the service. We are registering it for the intent ConnectivityManager.CONNECTIVITY_ACTION.
Service Creation Vs Start-up
A service is only created once. As it is created, its onCreate() method is called. We can override this method to perform the one-time things important for a service. This includes all the initializations required. Every time, the service is started, its onStartCommand method is called. Here is the definition of these methods for our service.