
flutterdec is a static Flutter AOT decompiler research tool for Android ARM64 binaries.
It takes an APK or libapp.so and emits readable pseudo-Dart plus optional IR, asm, diff, startup, and symbol-reporting artifacts.
flutterdec is meant for people reversing Flutter apps who want a practical first pass that is more readable than raw disassembly, while still being easy to verify against lower-level artifacts.
If you are new to the project, the fastest way to think about it is:
info tells you what the target looks likedecompile gives you pseudocode plus reportsdiff compares two buildsmap-symbols improves naming when you have matched engine binarieslibapp.soIf you just want to try the tool, start with one of these paths:
nix runnix profile installRun directly from GitHub:
nix run github:caverav/flutterdec -- --help
nix run github:caverav/flutterdec -- info ./sample.apk --json
nix run github:caverav/flutterdec -- decompile ./sample.apk -o ./out
Run from a local checkout:
nix run . -- --help
nix run . -- info ./sample.apk --json
nix run . -- decompile ./sample.apk -o ./out
Install from GitHub:
nix profile install github:caverav/flutterdec
flutterdec --help
Install from a local checkout:
nix profile install .
flutterdec --help
Update later:
nix profile upgrade flutterdec
Current prerelease: v0.1.0-alpha.2
Linux x64:
curl -fLO https://github.com/caverav/flutterdec/releases/download/v0.1.0-alpha.2/flutterdec-v0.1.0-alpha.2-Linux-X64.tar.gz
tar -xzf flutterdec-v0.1.0-alpha.2-Linux-X64.tar.gz
sudo install -m 0755 flutterdec /usr/local/bin/flutterdec
flutterdec --help
macOS arm64:
curl -fLO https://github.com/caverav/flutterdec/releases/download/v0.1.0-alpha.2/flutterdec-v0.1.0-alpha.2-macOS-ARM64.tar.gz
tar -xzf flutterdec-v0.1.0-alpha.2-macOS-ARM64.tar.gz
sudo install -m 0755 flutterdec /usr/local/bin/flutterdec
flutterdec --help
Other platforms and future tags:
Install into the user Cargo bin:
nix develop -c cargo install --path crates/flutterdec-cli
~/.cargo/bin/flutterdec --help
Run from source without installing:
nix develop -c cargo run -p flutterdec-cli -- --help
nix develop -c cargo run -p flutterdec-cli -- info ./sample.apk --json
nix develop -c cargo run -p flutterdec-cli -- decompile ./sample.apk -o ./out
Build a local release binary:
nix develop -c cargo build -p flutterdec-cli --release
./target/release/flutterdec --help
If this is your first run, this is the shortest useful path.
flutterdec info ./sample.apk --json
For APK inputs, info reports Android startup summary fields such as:
android_startup_presentandroid_startup_confidenceandroid_startup_entrypoint_countandroid_startup_flutter_activity_countIf adapter metadata is available, info also reports package and compatibility signals such as:
app_package_counts_topadapter_kindadapter_snapshot_hash_matchcompatibility_warnings
Install the adapter for the detected Dart hash:
flutterdec adapter install --dart-hash <HASH>
flutterdec decompile ./sample.apk -o ./out
Open the main outputs first:
out/pseudocode/*.dartpseudo
out/report.jsonout/quality.jsonThat is enough to start working on most APKs.
Inspect target metadata:
flutterdec info ./sample.apk --json
Install and list adapters:
flutterdec adapter install --dart-hash <HASH>
flutterdec adapter list
Decompile with the default app-focused scope:
flutterdec decompile ./sample.apk -o ./out
Emit asm and IR too:
flutterdec decompile ./sample.apk -o ./out --emit-asm --emit-ir
Compare two builds:
flutterdec diff --old ./old.apk --new ./new.apk -o ./out-diff --json
Generate Ghidra and IDA import scripts:
flutterdec decompile ./sample.apk -o ./out \
--emit-ghidra-script \
--emit-ida-script
By default, decompile focuses app reversing with --function-scope app-unknown and excludes known Flutter/Dart framework internals.
Available scopes:
app-unknown (default): app (package:*) plus unknown ownership functionsapp: only app (package:*) functionsall: app plus Flutter, Dart runtime, and framework internalsInclude everything:
flutterdec decompile ./sample.apk -o ./out --function-scope all
Focus only specific Dart packages (repeatable):
flutterdec decompile ./sample.apk -o ./out \
--function-scope app-unknown \
--app-package my_app
If package names are unknown, inspect report.json under function_scope.app_package_counts_top.
When --app-package is not provided, capped prioritization also uses manifest-derived package hints under function_scope.priority_package_hints to favor app-owned code.
Target a specific function by id:
flutterdec decompile ./sample.apk -o ./out \
--target id:42 \
--emit-asm
Target by entry address:
flutterdec decompile ./sample.apk -o ./out \
--target va:0x613468 \
--emit-asm
--target accepts:
id:<N>va:0x<ADDR>0x<ADDR><N>If <N> is ambiguous, flutterdec requires explicit id: or va:. Selection details are emitted in report.json.target_selection.
If you have a stripped/unstripped libflutter.so pair, generate a symbol map:
flutterdec map-symbols \
--stripped ./libflutter.stripped.so \
--unstripped ./libflutter.unstripped.so \
-o ./out/symbol-map \
--register-local-cache
Then use that in later decompile runs:
flutterdec decompile ./sample.apk -o ./out \
--extra-symbol-elf ./libflutter.unstripped.so
If the cached engine build id matches the APK's embedded libflutter.so, decompile auto-loads the cached symbol_target_summary.json and reports it under report.json.engine_symbol_ingestion.
flutterdec diff --old ./old.apk --new ./new.apk -o ./out-diff --json
diff_report.json includes added, removed, and common function summaries plus added_packages_top and removed_packages_top churn summaries.
| If you want to... | Start with... | Why |
|---|---|---|
| Read recovered logic | pseudocode/*.dartpseudo |
Best first pass for branches, loops, returns, and named callsites |
| Validate the decompiler | asm/*.s and ir/*.json |
Lets you confirm control flow and pool-backed calls |
| Understand startup | report.json.android_startup |
Shows manifest anchors, startup stages, and DartEntrypoint evidence |
| Check analysis health | quality.json and report.json |
Shows coverage, compatibility, target selection, and symbol-ingestion diagnostics |
| Review version-to-version changes | diff_report.json |
Shows recovered function churn and package-level change summaries |
decompile exposes analysis-engine profiles so you can trade detail for speed.
Default profile:
balanced (recommended)Available profiles:
balanced: full semantic naming, hints, and reportinglight: lower-overhead analysis for faster large-scale runsExample:
flutterdec decompile ./sample.apk -o ./out --analysis-profile light
Adapter backend selection:
--adapter-backend auto (default): try Blutter backend if configured, otherwise fall back to the internal adapter--adapter-backend internal: force the internal adapter only--adapter-backend blutter: require the Blutter backend and fail if unavailable--require-snapshot-hash-match: fail when the adapter-reported snapshot hash does not match the loader snapshot hashBlutter backend environment knobs:
FLUTTERDEC_BLUTTER_CMD: full command to launch Blutter, for example python3 /path/to/blutter.pyFLUTTERDEC_BLUTTER_PY: path to blutter.py when you want the current Python interpreter to run itNix integration:
nix develop provides flutterdec-blutter and auto-exports FLUTTERDEC_BLUTTER_CMD to that wrappernix run .#blutter-bridge -- --helpPer-feature engine toggles:
--with-canonical-model-symbols / --no-canonical-model-symbols--with-pool-value-hints / --no-pool-value-hints--with-pool-semantic-hints / --no-pool-semantic-hints--with-semantic-reporting / --no-semantic-reporting--with-bootflow-category-seeds / --no-bootflow-category-seeds--with-apk-startup-analysis / --no-apk-startup-analysisMain outputs under -o <OUT_DIR>:
pseudocode/*.dartpseudoquality.jsonreport.jsondiff_report.json (if flutterdec diff)asm/*.s (if --emit-asm)--emit-asm --emit-asm-opcodes)ghidra_apply_symbols.py (if --emit-ghidra-script)ida_apply_symbols.py (if --emit-ida-script)ir/*.json (if --emit-ir)report.json also includes:
compatibility for schema, hash, and manifest alignment diagnosticsandroid_manifest for manifest-derived launcher, deeplink, and activity signalsandroid_startup for APK bytecode startup evidence such as embedding calls, JNI bootstrap stages, and recovered DartEntrypoint callsites when presentandroid_startup.dart_entrypoints entries can carry function_name, library_uri, and app_bundle_path when those values are directly recoverable from APK bytecode or simple helper return propagationandroid_startup.bootstrap_chain summarizes observed Android embedder startup stages per source method, including ownership, stage ordering, completeness, and missing stepsengine_symbol_ingestion for auto-loaded local engine symbol cache matches keyed by libflutter.so build idbootflow_discovery entries tagged by source (adapter, manifest, apk_startup)The goal of these examples is simple: show original public source first, then show what flutterdec recovers from the shipped APK.
Original 1: Android Startup Surface
Original 2: App-Side Flutter / Dart Surface
Compare 1: Startup Source -> Recovered Startup Path
Source app: hiVPN v1.0.0 released on October 29, 2025. MainActivity and the manifest launcher are public in the repository:
- Source repo: https://github.com/Mr-Dark-debug/hivpn
- Release APK: https://github.com/Mr-Dark-debug/hivpn/releases/tag/release
Original
The app enters Flutter from MainActivity.onCreate. The second source card shows the app-side Flutter bridge that exposes MethodChannel('com.example.vpn/VpnChannel') to Dart code.
Recovered 1: APK Startup Report
Recovered
flutterdec parsed the APK manifest, recovered com.example.hivpn.MainActivity as the launcher, and correlated the startup chain from MainActivity.onCreate into Flutter JNI bootstrap calls such as attachToNative and nativeAttach.
Compare 2: App Source Using Flutter APIs -> Recovered Named Selectors
Source app: ZedSecure v1.2.0
- Source repo: https://github.com/CluvexStudio/ZedSecure
- Release APK: https://github.com/CluvexStudio/ZedSecure/releases/tag/v1.2.0
This is the comparison you asked for: public app source that uses Flutter APIs, then recovered APK artifacts where flutterdec keeps recognizable Dart and Flutter selector names instead of only anonymous control flow.
Original Source
This source is ordinary app UI code. It builds a ping badge with BoxConstraints(minWidth: 50) and other Flutter widget APIs inside the shipped app.
Recovered 2: From The ZedSecure APK With flutterdec
↓ ARM64
At the machine-code layer, the APK still looks like indirect selector dispatch through pool-loaded metadata and call targets.
↓ Function IR
<img src="https://github.com/caverav/flutterdec/raw/main/docs/assets/readme/zedsecure-minwidth-ir.svg" alt="IR summary
$ claude mcp add flutterdec \
-- python -m otcore.mcp_server <graph>