Every Android developer eventually encounters the need to manage threads within their applications.
Upon launching, an Android application initiates its first execution thread, referred to as the “main” thread. This main thread is responsible for directing events to the appropriate user interface elements and interacting with components from the Android UI toolkit.
To maintain a responsive application, it is crucial to avoid burdening the main thread with operations that could potentially block it.
Network requests, database interactions, and loading specific components are prime examples of operations that should be offloaded from the main thread. Executing these operations on the main thread leads to synchronous execution, rendering the UI unresponsive until their completion. Therefore, these operations are typically delegated to separate threads, preventing UI blockage during their execution (i.e., they run asynchronously from the UI).
Android offers numerous methods for thread creation and management, and many third-party libraries simplify this process. However, with this abundance of approaches, selecting the most appropriate one can be perplexing.
This article will guide you through common Android development scenarios where threading is crucial and present straightforward solutions applicable to these and other situations.
Threading in Android
In Android, threading components can be broadly classified into two categories:
- Threads tethered to an activity/fragment: These threads are bound to the lifecycle of the activity/fragment, terminating upon its destruction.
- Threads independent of any activity/fragment: These threads persist beyond the lifespan of the activity/fragment that spawned them.
Threading Components Tied to an Activity/Fragment
AsyncTask
AsyncTask is the most fundamental Android threading component. Its simplicity makes it suitable for basic scenarios.
Sample usage:
| |
However, AsyncTask falls short if your deferred task needs to outlive the activity/fragment. Even a simple screen rotation can trigger activity destruction.
Loaders
Loaders address the limitation mentioned above. They halt automatically upon activity destruction and restart when the activity is recreated.
Two primary types of loaders exist: AsyncTaskLoader and CursorLoader. We’ll delve into CursorLoader later in this article.
AsyncTaskLoader resembles AsyncTask but with added complexity.
Sample usage:
| |
Threading Components Independent of Activities/Fragments
Service
Service proves useful for executing prolonged (or potentially lengthy) operations without a UI.
Service runs on the main thread of its hosting process; it doesn’t spawn its own thread or run in a separate process unless explicitly specified.
Sample usage:
| |
With Service, the onus of stopping it after task completion falls on you, by invoking either stopSelf() or stopService().
IntentService
Similar to Service, IntentService operates on a separate thread and automatically terminates upon finishing its work.
IntentService typically handles short, UI-independent tasks.
Sample usage:
| |
Seven Threading Patterns in Android
Use Case No. 1: Sending a Network Request Without Server Response
Situations arise where you need to send an API request without requiring a response. For instance, sending a push registration token to your application’s backend.
Network communication necessitates offloading this task from the main thread.
Option 1: AsyncTask or Loaders
While AsyncTask or loaders can handle the call, their dependence on the activity lifecycle poses a challenge. You either wait for call execution and try to prevent the user from leaving the activity or hope for execution before activity destruction.
Option 2: Service
Service appears more suitable as it’s not bound to any activity, enabling it to continue the network call even after activity destruction. The lack of a required server response further strengthens its case.
However, since services run on the UI thread by default, manual thread management is necessary. Ensuring service stoppage post-call adds to the complexity, making it more effort than warranted for such a simple task.
Option 3: IntentService
This emerges as the optimal choice. Its independence from activities, execution on a non-UI thread, and automatic termination perfectly align with our requirements.
Use Case No. 2: Making a Network Call and Handling the Server Response
This use case is more prevalent, such as invoking a backend API to populate screen fields with the response data.
Option 1: Service or IntentService
While effective in the previous case, using them here is inadvisable. Extracting data from them into the main UI thread introduces significant complexity.
Option 2: AsyncTask or Loaders
These appear as the intuitive solution – straightforward and easy to use.
However, they introduce boilerplate code and make error handling cumbersome. Even simple networking calls necessitate exception handling, requiring response wrapping in custom classes with error information and success flags.
This overhead for every call is addressed by a superior solution: RxJava.
Option 3: RxJava
RxJava is a library developed by Netflix. It’s practically magic in Java.
RxAndroid](https://github.com/ReactiveX/RxAndroid) brings RxJava to Android, simplifying asynchronous task management. Learn more about RxJava on Android here.
RxJava introduces two key components: Observer and Subscriber.
An observer encapsulates an action. It executes the action, returning the result upon success or an error upon failure.
A subscriber, on the other hand, receives the result (or error) from an observable by subscribing to it.
With RxJava, you first define an observable:
| |
Once the observable is set up, you subscribe to it.
RxAndroid grants you control over the threads for both action execution within the observable and response reception (result or error).
This is achieved by chaining the observable with these two functions:
| |
Schedulers dictate the thread on which actions execute. AndroidSchedulers.mainThread() represents the scheduler associated with the main thread.
Assuming your API call is mRestApi.getData() and returns a Data object, a basic call would resemble this:
| |
Even without delving into its other advantages, RxJava’s ability to abstract away threading complexity allows for more robust code.
Use Case No. 3: Chaining Network Calls
Sequential network calls, where each operation depends on the previous one’s output, demand caution to avoid spaghetti code.
Consider fetching a token through one API call and using it in a subsequent call.
Option 1: AsyncTask or Loaders
This approach almost inevitably leads to spaghetti code, making the logic convoluted and requiring excessive redundant code throughout your project.
Option 2: RxJava using flatMap
RxJava’s flatMap operator takes an emitted value from the source observable and generates another observable. This enables observable chaining, where each observable leverages the previous one’s output.
Step 1. Define the observable to fetch the token:
| |
Step 2. Create the observable to retrieve data using the token:
| |
Step 3. Chain the observables and subscribe:
| |
This approach extends beyond network calls; it applies to any set of actions requiring sequential execution on separate threads.
All the above use cases are relatively simple, involving thread switching after task completion. This approach also accommodates more advanced scenarios requiring active inter-thread communication.
Use Case No. 4: Communicating with the UI Thread from Another Thread
Imagine uploading a file and updating the UI upon completion.
Since file uploads can be time-consuming, a background service, preferably IntentService, is suitable.
The challenge lies in invoking a UI thread method after the background file upload finishes.
Option 1: RxJava Inside the Service
This approach is not ideal. Callbacks are needed when subscribing to the Observable, and IntentService is designed for synchronous calls, not callbacks.
Using a Service necessitates manual service stoppage, increasing complexity.
Option 2: BroadcastReceiver
Android provides this component for listening to global and custom events. You can create a custom event triggered upon upload completion.
This involves creating a custom BroadcastReceiver subclass, registering it in the manifest, and using Intent and IntentFilter to define the custom event. The sendBroadcast method triggers the event.
Manifest:
| |
Receiver:
| |
Sender:
| |
While viable, this approach demands effort, and excessive broadcasts can hinder performance.
Option 3: Using Handler
A Handler attaches to a thread and enables action execution on that thread via messages or Runnable tasks. It collaborates with Looper, responsible for message processing within a thread.
When creating a Handler, you can specify the associated thread’s Looper via the constructor. To target the main thread, use Looper.getMainLooper().
To update the UI from a background thread, create a handler attached to the UI thread and post an action as a Runnable:
| |
This surpasses the first option, but an even simpler method exists…
Option 3: Using EventBus
EventBus, a popular library by GreenRobot, facilitates safe inter-component communication. Given our need for UI updates, it’s the simplest and safest choice.
Step 1. Define an event class (e.g., UIEvent).
Step 2. Subscribe to the event:
| |
Step 3. Post the event: EventBus.getDefault().post(new UIEvent());
The ThreadMode annotation parameter specifies the subscription thread. Here, we choose the main thread to enable UI updates upon event reception.
You can augment the UIEvent class with additional information as needed.
In the service:
| |
In the activity/fragment:
| |
EventBus significantly streamlines inter-thread communication.
Use Case No. 5: Two-Way Communication Between Threads Based on User Interactions
Consider a media player that continues playback even when the application is in the background. This requires UI interaction with the media thread (play, pause) and media thread updates to the UI (errors, buffering status).
A comprehensive media player example is outside this article’s scope. You can find excellent tutorials here and here.
Option 1: Using EventBus
While possible, using EventBus here is generally unsafe. Posting events from the UI thread to a service lacks certainty about the service’s running state.
Option 2: Using BoundService
A BoundService is directly connected to an activity/fragment, ensuring the activity/fragment is aware of the service’s status and has access to its public methods.
Implementation involves creating a custom Binder within the service and a method that returns the service instance:
| |
Binding the activity to the service requires implementing ServiceConnection, which monitors the service status, and using the bindService method:
| |
A complete example can be found here.
User interactions like Play/Pause are handled by binding to the service and invoking the appropriate public method.
Communicating media events back to the activity/fragment can be achieved using previously discussed techniques (BroadcastReceiver, Handler, or EventBus).
Use Case No. 6: Parallel Action Execution with Result Retrieval
Imagine building a tourist app that displays attractions from various data providers on a map. Since providers can be unreliable, you aim to handle individual failures gracefully.
Parallel execution necessitates running each API call on a separate thread.
Option 1: Using RxJava
RxJava’s merge() or concat() operators combine multiple observables. Subscribing to the “merged” observable provides all results.
However, a single API call failure results in an overall failure.
Option 2: Using Native Java Components
Java’s ExecutorService creates a configurable pool of threads for concurrent task execution. It returns a Future object that eventually yields all results via the invokeAll() method.
Tasks submitted to the ExecutorService are encapsulated within the Callable interface, allowing for exception handling.
Upon receiving results from invokeAll(), you can individually inspect and process them.
For example, fetching three attraction types from different endpoints in parallel:
| |
This enables error checking for each action independently, allowing you to ignore specific failures.
This approach surpasses RxJava in simplicity, conciseness, and resilience to single-point failures.
Use Case #7: Querying a Local SQLite Database
Due to their potential for long execution times, database operations are best performed on a background thread to prevent UI freezes.
Querying an SQLite database returns a Cursor for data retrieval.
| |
Option 1: Using RxJava
Similar to fetching backend data, RxJava can be employed for database interactions:
| |
The observable from getLocalDataObservable() can be used as follows:
| |
While effective, a more specialized solution exists for this scenario.
Option 2: Using CursorLoader + ContentProvider
Android provides CursorLoader, a native component for loading SQLite data and managing the associated thread. This Loader returns a Cursor for data access using methods like getString(), getLong(), etc.
| |
CursorLoader integrates with ContentProvider, a component offering real-time database features (change notifications, triggers) for enhanced user experiences.
No One-Size-Fits-All Solution for Android Threading
Android offers various threading mechanisms, each with strengths and weaknesses.
Selecting the appropriate approach based on your use case is crucial for maintainability and ease of implementation. Native components excel in some situations, while third-party solutions shine in others.
I hope this article proves valuable in your future Android endeavors. Feel free to share your threading experiences and any use cases where these solutions thrive or fall short in the comments below.