RizTech Academy logo
RizTech Academy
NetworkingLesson 3 of 635 min

The main thread, ANRs and StrictMode

Every frame Mandi Bhav draws, every tap it answers and every scroll it follows happens on one thread, the main thread. At 60 frames a second it has about 16 ms per frame for all of it. Anything else you put on that thread comes out of those 16 ms, and if it takes long enough, Android stops waiting and tells the user your app is not responding. That is an ANR, and Google Play counts them against you. Module 8 used StrictMode for a moment to catch a disk read in SharedPreferences; this lesson uses it properly, alongside the ANR, on the thing most likely to cause one: a network call. Then it turns StrictMode on in Mandi Bhav for good, and it finds something.

A network call on the main thread

Before suspend functions, Retrofit's interface returned a Call<T>, and execute() sent it and waited for the answer, on whatever thread called it. You will meet it in older code. A scratch build adds one to PricesApi:

@GET("prices.json")
fun latestPricesCall(): retrofit2.Call<NetworkPrices>

and calls it in MainActivity.onCreate, after injecting the API with @Inject lateinit var api: PricesApi:

// Experiment: a blocking network call on the main thread.
val response = api.latestPricesCall().execute()

The app dies before its first frame:

E/AndroidRuntime(23542): FATAL EXCEPTION: main
E/AndroidRuntime(23542): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.riztechacademy.mandibhav.verify/com.riztechacademy.mandibhav.MainActivity}: android.os.NetworkOnMainThreadException
E/AndroidRuntime(23542): Caused by: android.os.NetworkOnMainThreadException
E/AndroidRuntime(23542): 	at okhttp3.internal.http1.Http1ExchangeCodec.finishRequest(Http1ExchangeCodec.kt:183)
E/AndroidRuntime(23542): 	at okhttp3.internal.connection.Exchange.finishRequest(Exchange.kt:102)
E/AndroidRuntime(23542): 	at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:83)

Android has refused network access on the main thread for years, and it does it with a crash on purpose: a call that took 30 ms on the course's office Wi-Fi can take many seconds on one bar of signal, and a crash in testing is cheaper than a frozen phone in a mandi.

Getting around the crash, and getting an ANR instead

The tempting fix is to make the call a suspend call and wait for it with runBlocking, which compiles anywhere and does not crash:

// Experiment: waiting for a suspend call on the main thread.
window.decorView.postDelayed({
    val started = android.os.SystemClock.uptimeMillis()
    android.util.Log.d("M9", "runBlocking on ${Thread.currentThread().name}")
    val prices = kotlinx.coroutines.runBlocking { api.latestPrices() }
    android.util.Log.d("M9", "runBlocking returned ${prices.prices.size} prices after ${android.os.SystemClock.uptimeMillis() - started} ms")
}, 3_000)

The network work itself now happens on an OkHttp thread, so NetworkOnMainThreadException is not thrown. But runBlocking holds the main thread until the answer comes. With the course's test server told to wait 8 seconds, the app open, and one tap on a card a second after the call started:

11-08 22:55:04.073 D/M9      (23811): runBlocking on main
11-08 22:55:04.107 D/M9      (23811): request /api/v1/prices.json on OkHttp http://10.0.2.2:8765/...
11-08 22:55:09.616 I/WindowManager(  662): ANR in Window{14421ee u0 com.riztechacademy.mandibhav.verify/com.riztechacademy.mandibhav.MainActivity}. Reason:Input dispatching timed out (14421ee com.riztechacademy.mandibhav.verify/com.riztechacademy.mandibhav.MainActivity is not responding. Waited 5004ms for MotionEvent).
11-08 22:55:10.140 E/ActivityManager(  662): ANR in com.riztechacademy.mandibhav.verify (com.riztechacademy.mandibhav.verify/com.riztechacademy.mandibhav.MainActivity)
11-08 22:55:12.161 D/M9      (23811): runBlocking returned 9 prices after 8088 ms

8,088 ms with no frame drawn, and an ANR five seconds after the tap the app never answered. The tap is what made it an ANR: Android measured how long its touch waited, "Waited 5004ms for MotionEvent". A frozen app nobody touches is still frozen; the farmer only finds out when she taps. The system process wrote "ANR in" and collected the main thread's stack; on a phone with Mandi Bhav from Google Play, the same event reaches the Play Console's Android vitals, which module 14 reads.

A second run, with the server told to wait 15 seconds, did not even get the prices. OkHttp's 10-second read timeout fired first, and the SocketTimeoutException came out of runBlocking uncaught: an ANR, then a crash. runBlocking belongs in main functions and tests, never on the main thread of an app.

What the same slow server does to the real code

The real refresh is viewModelScope.launch { repository.refresh() }, and the waiting is a suspension, not a blocked thread. Same server, same 8-second wait, Refresh tapped, and the list scrolled up and down four times during the wait, with adb shell dumpsys gfxinfo reset beforehand:

Total frames rendered: 373
Janky frames: 3 (0.80%)
50th percentile: 21ms
90th percentile: 23ms
99th percentile: 40ms

373 frames in 6.3 seconds of scrolling, three of them late, while the download was running. The progress bar moved, the list scrolled, and the prices changed when the answer came. Same server, same eight seconds: the only difference is whether the main thread waited or was free. A slow network is a problem for the user's patience; a blocked main thread is a problem for the app's life.

On the main thread What happens
call.execute(), URL.openStream(), any socket NetworkOnMainThreadException, at once
runBlocking { } around network or disk No exception; frozen frames, then an ANR if the user touches
Disk: SharedPreferences.getString, a file read, a database query on Room 2 with main-thread queries allowed Frozen frames; StrictMode reports it
Heavy CPU: sorting, parsing, decoding a large image Frozen frames; StrictMode does not see it, the profiler does (module 13)
suspend calls to main-safe code: Retrofit, Room 3, DataStore Nothing: the thread is free while they wait

StrictMode, on for good

StrictMode is a debugging tool: you give it a policy for the main thread, and it reports every violation, with the stack that made it. Mandi Bhav now turns it on in debug builds, in MandiBhavApplication:

override fun onCreate() {
    if (BuildConfig.DEBUG) {
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().penaltyDeathOnNetwork().build())
    }
    super.onCreate()
}
Policy part Means
detectAll() Disk reads and writes, network, slow calls libraries mark, unbuffered I/O, and more each Android version
penaltyLog() Write each violation to logcat, tag StrictMode, with a stack
penaltyDeathOnNetwork() Crash on network access, as Android does by default
penaltyDialog(), penaltyDeath() Louder: a dialog, or a crash, for every violation

penaltyDeathOnNetwork() is there because of something the scratch build showed. With only detectAll().penaltyLog(), the execute() call above did not crash. It logged:

D/StrictMode(23426): StrictMode policy violation; ~duration=58 ms: android.os.strictmode.NetworkViolation

and the app carried on. A policy you set replaces the one Android set, and Android's default is the one that crashes on network. Setting a "stricter" policy had quietly made the app more forgiving.

Only debug builds: StrictMode costs time on every check, and a log line in a release build helps nobody.

What StrictMode found in Mandi Bhav

With the policy in place, a cold start of the real app, no experiments, logged one violation:

D/StrictMode(23197): StrictMode policy violation; ~duration=270 ms: android.os.strictmode.CustomViolation: newSSLContext
D/StrictMode(23197): 	at com.riztechacademy.mandibhav.di.NetworkModuleKt.pricesHttpClient(NetworkModule.kt:87)
D/StrictMode(23197): 	at com.riztechacademy.mandibhav.di.NetworkModule.provideOkHttpClient(NetworkModule.kt:46)
D/StrictMode(23197): 	at com.riztechacademy.mandibhav.ui.PricesScreenKt.PricesRoute(PricesScreen.kt:451)

Read it from the bottom. PricesRoute asked Hilt for its ViewModel; the ViewModel needs the repository, which needs the network data source, which needs PricesApi, which needs Retrofit, which needs the OkHttpClient; and building an OkHttpClient sets up TLS, which OkHttp itself marks as a slow call. 270 ms on the main thread, during the app's first frame, before a single request. No crash, no ANR, no visible bug: the cold start was slower than it needed to be, on every launch.

The fix is to build the client the first time a request needs it. Retrofit accepts a Call.Factory instead of a client, and Hilt can inject a Lazy<OkHttpClient> that builds nothing until get():

@Provides
@Singleton
fun provideRetrofit(baseUrl: HttpUrl, client: Lazy<OkHttpClient>, json: Json): Retrofit =
    Retrofit.Builder()
        .baseUrl(baseUrl)
        .callFactory { request -> client.get().newCall(request) }
        .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
        .build()

Lazy is dagger.Lazy, not Kotlin's. The first request runs in the application scope from lesson 2, off the main thread, so that is where the 270 ms moves. After the change, a cold start and a refresh logged no StrictMode violations at all. MandiBhavApplication, which lesson 5 gives an image loader over the same client, takes it as Lazy<OkHttpClient> too, so nothing builds the client before a request needs it. Once the source of a violation is something you cannot change, such as a library's start-up, you can permit it narrowly with StrictMode.allowThreadDiskReads() around that one call, and say why in a comment; Mandi Bhav has not needed to.

Check your work

Why execute() on the main thread crashed. Android's default policy throws NetworkOnMainThreadException for any network access on the main thread.

Why runBlocking did not crash and was worse. The network ran on an OkHttp thread, but the main thread waited 8,088 ms, drew nothing, and the tap made it an ANR.

What triggered the ANR. An input event that waited more than five seconds: "Waited 5004ms for MotionEvent".

What the real refresh did during the same eight seconds. Drew 373 frames in 6.3 seconds of scrolling, 0.8% janky.

Why penaltyDeathOnNetwork() is in the policy. A custom policy replaces Android's default, and without it network on main only logs.

What StrictMode found, and the fix. Building OkHttpClient (TLS setup) took 270 ms on the main thread while the ViewModel was created; Retrofit now builds the client lazily on the first request.

Practice 5. The newSSLContext violation comes back on the first cold start. Bottom to top: PricesRoute asked for PricesViewModel, which needed PriceRepository, then PriceNetworkDataSource, PricesApi, Retrofit and finally OkHttpClient, whose constructor set up TLS. Each one was reasonable; together they put 270 ms of TLS setup inside the first frame.

Practice

  1. Add the debug StrictMode policy to MandiBhavApplication, cold-start the app, and read the StrictMode tag in logcat. Then make the Retrofit change and check that the violation has gone.
  2. In a scratch copy, add the Call<T> function and call execute() in onCreate. Read the crash. Then remove penaltyDeathOnNetwork() from the policy and run it again: what does StrictMode do now?
  3. Serve the API with a delay (a five-line change to a Python http.server handler, time.sleep(8) before answering), wrap the call in runBlocking, tap the screen, and find "ANR in" in logcat. Then try a 15-second delay and explain the crash.
  4. Measure your own refresh with adb shell dumpsys gfxinfo com.riztechacademy.mandibhav reset, a slow server and some scrolling, then dumpsys gfxinfo again. Compare the frame count with the runBlocking version.
  5. Break it on purpose: put .client(client) back in provideRetrofit, with a plain OkHttpClient parameter. Cold-start, find the violation, and read its stack from the bottom, naming each class that asked Hilt for the next.
  6. Harder: add detectAll().penaltyLog() to a StrictMode.VmPolicy as well, which watches things like leaked Closeables and activities. Run the app through rotation, process death and a refresh, and decide for each violation it reports whether it is Mandi Bhav's to fix.

Official documentation

Next: loading, empty, error and retry, and what Mandi Bhav shows while a slow first download runs.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship