SReverseby Simpa Labs

Flutter packaging

Why Flutter apps do not decompile like Java apps

You open a Flutter APK in a Java decompiler and the classes belong to the framework. The application's own logic was compiled to machine code before packaging, and no bytecode form of it is in the file.

What the build produces instead of bytecode

A Flutter application for Android ships two large native libraries and a small amount of Java. The engine library holds the Dart runtime, the rendering pipeline and the networking stack. The application library holds the Dart code of the app itself, compiled ahead of time into machine instructions for each supported processor architecture. The Java and Kotlin in the package is the embedding that starts the engine and registers plugins.

That is why a Java decompiler disappoints. It opens the DEX file, lists the activity, the generated plugin registrant and the framework glue, and finds none of the code that builds a request. The request logic was never in that format.

Machine code and bytecode are different reading problems

Java and Kotlin compile to bytecode, which is a description of operations with names attached: classes, methods, fields and their signatures. A decompiler reconstructs source from that description because the description sits close to the source. Dart in a release build compiles to instructions for the device's processor, and the names are discarded. The result still computes the request, and it no longer carries an index of what each part was called.

The task changes with the format. Reading Java means following names and call graphs. Reading compiled Dart means finding a region of the library by its behaviour, its constants or its position relative to the networking calls, then working out what that region does.

What survives, and what is worth searching for

  • String literals survive into the snapshot data. Hostnames, paths, header names and parameter names are stored as data, so a search of the application library for a URL fragment or a header name still returns hits.
  • The plugin boundary survives. A Flutter app registers its plugins through generated Java, and each registration names a plugin, which tells you which capabilities exist and where a request could leave the Dart code.
  • The shape of the networking calls survives. The Dart runtime performs HTTP through its own stack, and the code around a request has a recognisable structure even when every function is unnamed.
  • A symbol file survives when the build produced one. A build configured to keep debug symbols writes a separate symbols file, and where that file is available, the addresses in the library map back to Dart function names.

What the DEX still tells you

The embedding is worth reading even though it holds none of the request code. The activity that starts the app, the generated plugin registrant and the manifest entries together list the plugins the app uses, and a plugin name identifies a capability: secure storage, device information, certificate handling. Each name is a candidate location for a value the request depends on. The DEX also carries any channel names the developer defined, which appear as string constants inside the classes that call across them.

Why the usual dynamic tools also miss

Tools that hook the JVM expect a request to pass through OkHttp, Retrofit or a similar Java client. In a Flutter release build the request is assembled inside the Dart runtime and reaches the network through the engine, so a JVM interceptor sits in a layer the request never visits. Runtime observation still works, and it has to target the Dart side or the socket rather than the Java HTTP classes.

A sequence that reaches a working client

  • Confirm the build type before choosing tools. The presence of the application library and the absence of Dart source in the package identify a release build, and a debug build behaves differently because it carries a kernel file that is far more legible.
  • Harvest strings from the application library first. A list of hostnames, paths and header names is cheap to produce and gives the capture something to confirm.
  • Capture the traffic to learn the shape. The method, the field names and the ordering come from the capture, and the static reading then explains how the values were produced.
  • Locate the request construction in the library. Work backwards from the string references and the networking calls to the region that assembles the body and the headers.
  • Reproduce and verify against the server. The only proof that the reading was correct is a request the backend accepts.

Deliverables for this work are a Python, JavaScript or TypeScript client, an importable Postman collection and documentation, starting at $120 with most projects finished in 24 to 72 hours.

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?