The hook lands in the wrong process image
On a normal Android app, TLS work runs in Java framework classes and in a Java HTTP client. A standard procedure targets those classes and the library that implements them.
A Flutter app builds its interface with the Flutter engine and runs its application logic, including most of its networking, as Dart code compiled ahead of time into a shared library. A client written with the Dart http or dio package performs its TLS through the Dart runtime, which carries its own TLS implementation in that same library. The Java classes a Java-focused procedure waits for are still present in the process because the engine is hosted by an Android activity, but the app's requests do not pass through them. A hook placed there observes nothing.
Two consequences follow. A procedure that reports success is not evidence of anything, because the classes it replaced are not on the path. And an app that prints no traffic while the procedure claims to be active is behaving exactly as expected.
Where pinning can be configured in a Flutter app
The decision about which certificate to accept can sit in several places, and which one is used decides where you have to look.
- Dart-level pinning. A callback that inspects the peer certificate and aborts the connection is the most common form. The logic is Dart code and is compiled into the application snapshot.
- Native pinning through a plugin. A package can hand the request to the platform HTTP client or to a native library. When that happens, Java classes are back on the path and the shape becomes closer to a native Android app.
- Platform channel delegation. The Dart layer can call into Java through a method channel for the actual transfer, which splits the work across two runtimes.
- Certificate pinning supplied in configuration. Some setups pass a digest or a certificate into the client constructor, so the pin exists as data as well as code.
Establish which of these the app uses before planning anything else. The evidence is available statically. A Flutter app declares its Dart dependencies in a generated plugin registrant and in the package metadata that survives into the build, so the packages that handle networking and pinning are visible in the artifacts.
Reading a failure whose origin is not clear
Because two runtimes are involved, a handshake refusal can be reported in two different vocabularies. A Dart-level rejection appears as an exception raised in Dart code, often wrapped by the networking package into a generic connection error. A native rejection appears as a lower-level failure and may reach the Dart layer with the original detail stripped.
The useful test is placement rather than error text. Determine whether the connection reaches the server at all. A request that never arrives at the endpoint, combined with an error raised inside the Dart networking stack, places the refusal in the Dart client. A connection that completes and then receives an HTTP refusal places the problem in authentication or in the request itself.
Adding to the difficulty, a Flutter release build strips the information that would normally identify the failing frame. Dart code compiled ahead of time carries no readable names for most functions, so a stack trace from a release build is close to useless. The article on Flutter API logic describes how the compiled output is organised and why the usual Java reading techniques do not apply.
What the reconstruction has to account for
Flutter changes the extraction work rather than preventing it. The API is ordinary HTTP, so what has to be recovered is the same set of facts as in any other app: base URLs, routes, headers, body encoding, signing, and the lifetime of tokens. What changes is where those facts are stored and how they can be observed.
Three parts of the work depend on the Flutter specifics. The first is locating the request construction in compiled Dart, which happens through string and data recovery rather than through reading named methods. The second is deciding whether the app's TLS expectations have to be recorded or can be left to the caller, which depends on whether pinning was configured at the Dart layer or delegated to the platform. The third is any signing that runs in the Dart runtime, because it has to be reproduced from the algorithm rather than invoked through a Java hook.
Where the request logic runs in native code rather than in Dart, the analysis moves to the shared library and native Android reverse engineering applies. Flutter API reverse engineering describes how the two layers are handled together.
Related work
Reviewed 28 September 2026 · SReverse research desk