Threads, Handlers, Executors
HTTP Basics & Threads introduced the golden rule: never do blocking work – network calls, disk I/O, heavy computation – on the main (UI) thread, or the app will freeze and Android may show "Application Not Responding". This page goes deeper into the general Java concurrency tools available on Android, beyond just networking, for whenever RecipeBox needs to do real work off the main thread.
Raw Thread and Runnable
The most basic building block is Java's own Thread, running a Runnable. For example, resizing a single recipe photo before saving it:
// Processing one thumbnail on a raw background thread
new Thread(new Runnable() {
@Override
public void run() {
Bitmap thumbnail = ThumbnailUtils.resize(recipe.getPhotoUri(), 200, 200);
saveThumbnailToDisk(recipe.getId(), thumbnail);
}
}).start();
ExecutorService: A More Manageable Way
ExecutorService, created via the Executors factory, is the recommended way to run background work in plain Java code. It manages a pool of threads for you, so you submit tasks instead of creating threads by hand:
// A pool that can process several thumbnails concurrently
ExecutorService thumbnailExecutor = Executors.newFixedThreadPool(4);
for (Recipe recipe : recipesNeedingThumbnails) {
thumbnailExecutor.execute(() -> {
Bitmap thumbnail = ThumbnailUtils.resize(recipe.getPhotoUri(), 200, 200);
saveThumbnailToDisk(recipe.getId(), thumbnail);
});
}
// When the screen no longer needs it, e.g. in onDestroy()
thumbnailExecutor.shutdown();
- newFixedThreadPool(4) caps concurrent work at 4 threads, reusing them instead of creating a new thread per task.
- newSingleThreadExecutor() is useful when tasks must run one at a time, in order (e.g. writing recipes to a log file).
- Calling shutdown() lets submitted tasks finish but rejects new ones – important to avoid leaking the pool.
Handler: Getting Back to the UI Thread
Background work eventually needs to update the UI – and UI updates must happen on the main thread. A Handler tied to the main thread's Looper lets you post a Runnable back onto it from anywhere:
// Inside the background task, once a thumbnail is ready
Bitmap thumbnail = ThumbnailUtils.resize(recipe.getPhotoUri(), 200, 200);
new Handler(Looper.getMainLooper()).post(() -> {
recipeThumbnailView.setImageBitmap(thumbnail); // safe: now running on the UI thread
});
HandlerThread: A Background Thread With Its Own Looper
A plain Thread has no message queue of its own – it just runs one block of code and exits. A HandlerThread is a background thread that does have its own Looper, so you can keep posting tasks to it over time, the same way you post tasks to the main thread. It's a more specialized tool, useful when you need a single dedicated background thread that processes a stream of tasks in order (for example, sequential writes to a local recipe cache) rather than a pool that runs tasks in parallel.
Comparing the Options
| Tool | Good for | Caveat |
|---|---|---|
| Thread / Runnable | Quick one-off background work, learning the basics | No cancellation, no pooling, easy to leak |
| ExecutorService | Managed, reusable background work with a bounded thread pool | You still own its lifecycle – must shut it down |
| Handler + Looper | Hopping back onto the main thread to update UI | Doesn't do background work itself – pair it with a Thread/Executor |
| HandlerThread | One background thread processing an ordered stream of tasks | Only one thread – no parallelism |
| WorkManager | Deferrable work that must survive process death/reboot | Overkill for immediate, in-memory work like the thumbnail example above |
Choosing the Right Tool
- Need the work done right now, off the main thread, while the screen is open? Use an ExecutorService (or a raw Thread for the simplest cases) – like the thumbnail processing example above.
- Need to update a TextView/ImageView once background work finishes? Post back with a Handler tied to Looper.getMainLooper().
- Need the work to survive the app closing or the device rebooting, with constraints like "only on WiFi"? That's exactly what WorkManager is for – not raw threads.
Troubleshooting
"Only the original thread that created a view hierarchy can touch its views"? → You updated a View from a background thread directly; wrap the update in a Handler.post() call as shown above.
App still freezes despite using a background thread? → Double-check the heavy work isn't accidentally still happening before the thread starts, or that you aren't blocking on Thread.join() from the main thread.
ExecutorService tasks pile up forever? → A fixed pool only runs as many tasks concurrently as its size; extra tasks queue up. Size the pool to the work, and call shutdown() when done.