Android SharedPreferences Tutorial and Example

Debugging Multithreading Issues In Android Apps

Android applications often perform several jobs at once: loading network data, reading SQLite records, decoding images and responding to touch events. When those jobs use the wrong thread, an app may freeze, display stale information or fail only once every few minutes.

The hardest bugs are usually timing bugs. A clean test on a developer’s Pixel may hide a race condition that appears on an older Samsung handset, a busy train connection in Melbourne or a device running several background services.

A reliable debugging process combines thread awareness, reproducible tests and small, observable units of work. The aim is to identify which thread owns each operation, where shared data changes and why the timing produces an unexpected result.

Symptom Likely cause Useful test Common fix
NetworkOnMainThreadException Network work on the UI thread Inspect the stack trace Move work to an executor or coroutine
Randomly missing list items Race between loading and rendering Add timestamps to callbacks Serialise updates and use immutable results
Occasional crash after leaving a screen Worker retains a dead Activity Rotate or close the screen during work Cancel work and use lifecycle-aware scopes
UI freezes for several seconds Database or image work on the main thread Use StrictMode and frame profiling Shift expensive operations off the main thread
Incorrect counter or saved value Unsynchronised shared state Repeat the operation many times Use locks, atomics or a single owner

Understand What Runs Where

The main thread, commonly called the UI thread, handles drawing, input events and view changes. It should remain available for short operations. A database query, large JSON conversion or bitmap resize can consume enough time to create dropped frames and an unresponsive interface.

Background threads are suitable for blocking or CPU-heavy work, but they cannot directly change most Android views. The usual pattern is to perform the operation away from the UI thread, then post a compact result back to the main thread.

Thread names are valuable evidence. Give executors meaningful names such as DatabaseWorker or ImageDecoder, then include the current thread in diagnostic logs. A message from main tells a different story from one produced by pool-2-thread-1.

Reproduce Timing-Dependent Failures

A race condition may disappear when a debugger pauses execution. Start by making the operation happen repeatedly: tap a button rapidly, rotate the device during a request, open and close a screen quickly, or load a large set of records several times.

Australian testing conditions can expose different timing. Test on both Wi-Fi and mobile data, including a weak regional connection around Bendigo or Cairns, rather than relying only on fast office broadband. Android users also move between networks frequently, so callbacks can arrive after a screen has been replaced.

Add timestamps, request IDs and object identifiers to each relevant log entry. For example, record when a request starts, when data arrives and when the adapter receives the result. This creates a timeline instead of a collection of disconnected messages.

Protect Shared State From Races

Shared mutable state is a common source of multithreading problems. Two workers may read the same value, calculate different results and then overwrite one another. A volatile field can make a value visible between threads, but it does not make compound operations such as incrementing safe.

Choose one owner for important state whenever possible. An executor that processes tasks sequentially can be simpler than several workers competing for the same collection. If multiple threads truly need access, consider synchronized blocks, AtomicInteger, thread-safe collections or a carefully designed lock.

Keep critical sections small. Holding a lock while performing a network request or database query can block unrelated work and create deadlocks. Lock only the state that must be protected, and use a consistent lock order when more than one lock is required.

Trace Database And List Updates

SQLite access should generally happen away from the main thread, especially when a query scans many rows or runs during app startup. The result should be treated as a snapshot: once loaded, pass it to the UI in a controlled update rather than allowing a worker to modify the adapter’s collection directly.

A practical example is the RecyclerView SQLite tutorial, where database results feed a list-based interface. In a production version, make sure an older query cannot replace newer data after the user has added or deleted an item.

A Useful Database Debugging Checklist

  • Log the query start and completion time
  • Attach a generation number to each load request
  • Update adapter data on one known thread
  • Close cursors and database resources reliably

Lifecycle changes make this more complicated. A user in Sydney might switch apps while travelling through a tunnel, then return after the query finishes. The screen should either ignore obsolete results or receive them through a lifecycle-aware state holder.

Keep UI Work On The Main Thread

Views belong to the UI thread, so background code should return values rather than reaching into an Activity or Fragment. Posting work with runOnUiThread, a Handler, LiveData, Flow or a lifecycle-aware coroutine can make the hand-off explicit.

A common failure occurs when a callback updates a view after the Activity has been destroyed. Rotation, split-screen mode and navigation can all expose this issue. Check lifecycle state where appropriate, cancel work when the owner is destroyed and avoid storing view references inside long-lived workers.

Log Details That Reveal The Bug

  • Thread name and thread ID
  • Screen or lifecycle owner
  • Request ID and data version
  • Start, callback and cancellation timestamps

StrictMode can expose accidental disk or network work on the main thread during development. Android Studio’s profiler and system trace tools can then show whether the app is blocked by CPU work, I/O, lock contention or excessive garbage collection.

Use Tools To Inspect Concurrency

A debugger is useful for checking thread stacks, but logs often provide better evidence for intermittent failures. Filter Logcat by request ID and include structured fields rather than vague messages such as “finished” or “error”. Preserve the original exception and its complete cause chain.

Thread dumps can reveal deadlocks: one thread waits for a monitor held by another, while the second waits for the first. Look for repeated BLOCKED, WAITING or long-running RUNNABLE states. If a worker pool is exhausted, queued tasks may look like missing callbacks even though they have not started.

For automated testing, run the same operation under load and introduce controlled delays. A short delay before a callback, database write or cancellation can expose ordering assumptions. Android Studio’s CPU profiler, StrictMode and tests that repeat lifecycle changes work together to narrow the cause.

Design Cancellation And Ownership Clearly

Every asynchronous operation needs an owner and a cancellation policy. A screen-scoped request should stop when that screen is gone; an app-wide synchronisation job may belong to WorkManager instead. Cancellation should be checked between meaningful steps, especially during loops that process many records.

Third-party libraries deserve the same scrutiny as your own code. An SDK that performs analytics, advertising or payment work may create threads and callbacks outside your immediate control. For example, when evaluating a library related to a crypto casino guide, inspect its Android permissions, background behaviour and callback threading before adding it to an app.

Use one clear data flow: input creates work, work produces an immutable result, and the lifecycle-aware UI consumes that result. This reduces hidden communication between workers and makes cancellation easier to verify.

Build A Thread-Safe Testing Routine

Concurrency defects are easier to prevent when thread rules are written down. Document which component owns each mutable value, which operations require the main thread and what happens when a request is cancelled. Code reviews can then check a concrete contract instead of relying on intuition.

Run tests on different Android versions, device speeds and screen sizes. Include older phones commonly retained by families, tablets used for point-of-sale tasks and devices with aggressive battery management. In Australia, background restrictions and intermittent coverage can make a perfectly timed demo behave differently from a real app used on a long regional drive.

A stable application does not require every operation to run in parallel. It requires deliberate ownership, observable scheduling and safe hand-offs. Once each task has a defined thread, lifecycle and cancellation path, intermittent failures become traceable engineering problems rather than mysterious user reports.