SReverseby Simpa Labs

Native Android

Finding network logic in a Jetpack Compose app

A Compose screen describes state, and the request that fills that state sits one or two layers below it. The route from a screen to the call it triggers runs through a state holder and a repository.

Why the UI layer holds no request logic

Compose renders by calling composable functions again whenever the state they read changes. A composable can run many times for one screen and may run close to a frame deadline, so work with effects outside the composition does not belong in its body. A request started there would repeat on recomposition and could outlive the composition that started it. The framework provides effect handlers for one-off work, and applications place sustained request logic above the composition instead.

That design decision helps during analysis. The composable that draws a list usually receives the list and a callback, and the callback leads to the code that builds the request. When you find the composable, you are one edge away from the state holder.

Where the state holder and repository sit

The usual arrangement places a ViewModel between the composable and the data layer. The composable obtains it with a view model helper or an injection call and invokes a method that changes state. The ViewModel owns a coroutine scope tied to its lifetime, launches the call, and writes the result into a state flow or live data object that the composable observes.

Below the ViewModel sit a repository and one or more data sources. The repository decides whether a result comes from a local database, a cache or the network. The network source holds the client: a Retrofit service interface, a Ktor client, or a generated client from a schema. The client itself is built once, often in a dependency injection module, and the base URL, interceptors and serialisation converters are configured at that point.

The class that builds the client is usually the single place where the host appears. Search the dex for a scheme or a domain to find it, then read the builder for the base URL, the converter factory that names the serialisation library, and the interceptor list that names the authentication, header and logging code. That one class answers most of the questions a client reconstruction asks, and it is worth reading before any individual screen.

Injection frameworks leave readable traces after obfuscation. Hilt and Dagger keep the injection annotations, module classes and provider methods, and they generate factory and component classes whose names contain the original type names. Koin keeps a module block with single and factory declarations. In each case the binding from an interface to its implementation is the edge you follow.

Moving from a screen to the call it triggers

  • Find the composable first, because a composable function in decompiled output carries a Composer parameter and integer changed flags, and the composable annotation usually survives.
  • Read the lambda arguments it passes, since click handlers and load handlers appear as classes that implement a function type and whose invoke method calls one method on the state holder.
  • Follow that method into the launched coroutine, then into the repository and the network source in turn.
  • Read the interface annotation for the route, because Retrofit keeps the HTTP method and path template in the annotation and Ktor keeps the URL in the request builder call.

A screen can also reach the network without an obvious callback. Paging libraries and work schedulers start their own flows, and a WebView loads a URL taken from a string resource. Check string resources and navigation route definitions when no call appears under the composable.

Reading Compose output without the original source

Compose compilation adds parameters and generates classes, and it leaves string constants alone. Endpoint paths, query parameter names and JSON field names remain in the dex, so a search for a host or a path segment leads back to the client even when every class name has been renamed. Header names and signing salts survive in the same way.

When the chain runs through a use case class, expect one extra edge. The use case usually holds a single method with a clear name, and its constructor argument names the repository it depends on, which confirms the order of the layers. Compose also keeps state across configuration changes in a saveable holder, and a screen can restore a value written during an earlier session. When a request depends on a stored identifier, find the code that reads that value, because the call that produced it often runs at startup rather than on the screen that uses it. Reconstructing the client from that point is the work described under Retrofit API extraction, and the wider service is Android API reverse engineering.

Reviewed 28 September 2026 · SReverse research desk

Start your full APK reconstruction

Send the full APK

Projects start at $120. Choose WhatsApp or email, then attach the APK in the app that opens. We reply within one hour with the next step and send the fixed quote after review.

Want us to contact you?