How it works
How the plugin rewrites bytecode (ASM) during the build to capture bridge calls
Library vs plugin
A library (implementation, api) is code that ships in your APK and runs on the device. dari is a library. It can record messages, but it cannot change your code, so your bridge has to call it.
A Gradle plugin (plugins { id(...) }) is code that runs inside Gradle on your machine or CI while the app builds. It is not packaged into the APK. AGP (com.android.application), KSP and the Sentry Android Gradle Plugin are all plugins.
| Step | Runs on | What happens |
|---|---|---|
1. plugins { ... } | Your machine or CI (Gradle) | Plugins load and configure the build. |
| 2. Compile | Your machine or CI (Gradle) | Your code becomes .class files. |
| 3. Transform | Your machine or CI (Gradle) | The Dari plugin rewrites the compiled classes. |
| 4. Dex + package | Your machine or CI (Gradle) | Classes and libraries become the APK. |
| 5. Run the app | Device | dari records what the inserted calls report. |
Inserting calls into your code needs a plugin. Recording and showing messages is the library's job, so the plugin needs dari too.
What is a DSL?
A DSL (domain-specific language) is a small language for one job. Gradle build scripts use Kotlin (or Groovy) to build one: android { }, dependencies { } and plugins { } are all ordinary Kotlin function calls that take a block.
The plugin adds one more block, dari { }. Inside it, webToAppRequest(...) { handlerName = arg(0) } is plain Kotlin as well:
webToAppRequestis a function on thedariextension;handlerNameis a property;arg(0)returns a value that describes "the first parameter".
Nothing runs on the device at this point. The block only records a description that the plugin bakes into your classes.
What is ASM?
Kotlin and Java compile to JVM bytecode: .class files that hold a list of instructions per method. Android turns them into DEX afterwards.
ASM is the standard library for reading and rewriting that bytecode. AGP exposes it through its official instrumentation API: a plugin registers an AsmClassVisitorFactory, and AGP passes every class through it before dexing. This is also how Firebase Performance, Sentry and Hilt modify compiled code.
For a mapped method such as:
fun onBridgeRequest(handlerName: String, requestId: String, data: String?) {
when (handlerName) { /* ... */ }
}the plugin adds one call at the very start of the method body in the compiled class. In source terms the result looks like this (your .kt file is not changed):
fun onBridgeRequest(handlerName: String, requestId: String, data: String?) {
DariHooks.onMappedCall("""{"kind":"WEB_TO_APP_REQUEST","fields":{...}}""", this, arrayOf(handlerName, requestId, data))
when (handlerName) { /* ... */ }
}On the device, DariHooks (inside dari) reads the mapping, picks the values out of the arguments and calls onWebToAppRequest(...) on an interceptor. Because the plugin works on bytecode, it handles Java and Kotlin the same way.
Why not a Kotlin compiler plugin? It runs inside kotlinc, so it only sees Kotlin sources. It can't touch bridges written in Java, which is common for WebView code, or bridges inside third-party AARs. Its IR API also isn't stable, so it usually needs a build per Kotlin version. AGP's instrumentation API works on the compiled classes of every language and AAR, at the cost of reading less source-level detail (parameter names, suspend) back from bytecode.
The plugin also rewrites two more things:
webView.addJavascriptInterface(bridge, name)calls now go throughDariHooks. Dari can then tell which WebView a bridge object belongs to.WebViewClient.doUpdateVisitedHistoryoverrides now report the page URL.
Built on official APIs
Every step uses a public, documented API.
| What | Official API |
|---|---|
| Rewrite compiled classes | AGP Instrumentation.transformClassesWith with an AsmClassVisitorFactory (Extend the Android Gradle plugin) |
| Choose which classes | AGP InstrumentationScope |
| Debug builds only | AGP Component.debuggable |
| Read and write bytecode | ASM, the copy AGP already ships |
| Bridge methods | @JavascriptInterface |
| Bridge → WebView | WebView.addJavascriptInterface |
| Page URL | WebViewClient.doUpdateVisitedHistory |
What else is captured
- Tag per WebView. Each WebView gets its own interceptor. The tag is the name passed to
addJavascriptInterface(Android), unless you settag.- Requests use the WebView their bridge object was added to.
- Responses use the interceptor that recorded the matching request.
- Mapped methods that are not bridge objects use a
WebViewparameter if there is one, otherwise the WebView seen most recently.
- Page URL. Taken from
doUpdateVisitedHistory, so redirects, link clicks and SPA history changes count too. This needs aWebViewClientsubclass that overridesdoUpdateVisitedHistory; an empty override is enough.