alchemy_app_demo: Alchemy App Demo: How Lark Turns Native Code Into a Discoverable Extension Layer

A reference implementation for native plugins, lifecycle hooks, and tab injection inside a super-app, with annotations doing much of the wiring.

8 min read • View on GitHub • More from larksuite

A central host system sits in the middle like a sealed engine, with multiple native modules connected around it through clean lines and annotation tags. The scene explains the repo’s core idea: a super-app can discover capabilities as modules instead of forcing every feature through a manual registration pile.
Alchemy treats native features as discoverable modules, not scattered app code.
Key Takeaways

The most revealing thing in larksuite/alchemy_app_demo is not a single plugin. It is the way the repo turns native capabilities into something the host can discover, bind, and launch as if they were built in from the start.

The real trick is not the plugin. It is the discovery layer.

This repo reads like a reference implementation for native injection inside a super-app. Instead of forcing every capability through one central registration file, it spreads functionality across annotated modules and lets Lark assemble the pieces. That matters because large enterprise surfaces do not stay small for long.

The pattern shows up across lifecycle hooks, QR code handling, shortcuts, tabs, and plugin factories. The architecture is less about a clever API and more about removing friction from integration.

Annotations feed host discovery, which then fans out into runtime behavior.

What Alchemy is for

The repo is a multi-platform demo, but the Android implementation makes the underlying model easiest to see. It is built to show how Lark can host native code on Android, iOS, and HarmonyOS without treating every extension like a one-off special case.

That is the key distinction from a web embedding story. A WebView can display content. A native extension can participate in the host app’s lifecycle, tab structure, permissions, and session state.

Annotations replace hand wiring

The annotation set is the architectural headline: @NativeAppExtensionImpl, @Lifecycle, @ShortcutImpl, @QRCodeImpl, and @NativeAppPluginFactory. The point is not syntax. The point is that a module can advertise what it is, and the host can discover it without every integration step being manually glued together.

That reduces boilerplate, but more importantly it reduces coordination cost. In a large platform, central registries become merge-conflict magnets. Discovery by contract scales better because each module declares itself.

@Lifecycle
class LifecycleImpl : LifecycleCallback {
    override fun onAppStateChanged(state: AppState) {
        if (state == AppState.BACKGROUND) {
            // pause sync, trim caches, release resources
        }
    }

    override fun onLoginSuccess() {
        // refresh user-scoped data
    }

    override fun onLogout() {
        // clear session data
    }
}

The same pattern is visible in the plugin factory. A factory object creates the bridge implementation, and the host can discover it as a native app plugin rather than as ad hoc app code.

A plugin bridge that turns JS calls into native work

The cleanest proof is BDMapPlugin and BDMapPluginFactory. A web-side event enters the bridge, the native layer checks permissions, calls into Baidu Maps, and returns a callback back to the JS side. That is the whole promise of the architecture in one path.

A tight bridge carries a JS request from one side into native Android work on the other. The request splits into permission handling, map lookup, and callback return, showing the exact path from web action to native response.
A JS event becomes native work, then returns as a callback.
public void handleEvent(Event event, IApiCallback callback) {
    if (event.getType() == EventType.GET_LOCATION) {
        requestLocationPermission(() -> {
            BDMapService service = new BDMapService();
            service.getCurrentLocation(result -> callback.onResult(result));
        });
    }
}

That flow matters because it crosses layers without collapsing them. The JS layer stays declarative. The native layer keeps control over permission and device APIs. The callback closes the loop cleanly.

Lifecycle hooks make the host app feel continuous

LifecycleImpl.kt shows the other half of the platform story. Enterprise apps care about foreground and background transitions, login and logout boundaries, cache hygiene, and session-aware behavior. A host that knows when the user changes state can behave much more safely.

This is where native integration beats a loose web container. When login changes, you do not want to guess. You want explicit hooks that can sync data, reset state, or stop sensitive work immediately.

@Lifecycle
class LifecycleImpl {
    fun onAppStateChanged(isForeground: Boolean) {
        if (!isForeground) {
            // cleanup, pause polling, persist state
        }
    }

    fun onLoginSuccess() {
        // fetch fresh user context
    }

    fun onLogout() {
        // clear app-scoped and user-scoped data
    }
}

Tabs are injected, not merely linked

The user-facing payoff is tab injection. In KAMailTab.kt and KATabFragment.kt, the demo places a native fragment into the main navigation surface instead of sending users into a browser wrapper. That makes the extension feel like part of the product, not a sidecar.

That difference is subtle in code and obvious in UX. A tab can inherit the host’s navigation logic, styling, and session context while still being owned by an extension module.

DimensionWebView-first extensionAnnotation-driven native extension
Extension surfaceHTML or embedded web appNative modules and fragments
Registration modelManual, often centralizedDeclared through annotations and contracts
UX feelSeparate from the hostFirst-class inside the host app
PerformanceBound by web runtime overheadTighter access to native APIs
Session controlIndirect and often brittleExplicit lifecycle and login hooks
MaintenanceSimple at first, messy at scaleMore structured, less registry churn

Why this beats a WebView-first model

The honest comparison is not that native is always better. WebView extensions are simpler and more portable. But when the job is tight platform integration, lower latency, and consistent session-aware behavior, the native path is stronger.

The tradeoff is coupling. Native injection asks more of the host platform and the extension team. In exchange, it gives you a capability layer that feels built in instead of bolted on.

What the repo says about enterprise platform design

Alchemy App Demo points at a broader shift. Super-app extensibility is moving from “embed a page” toward “expose a native capability graph.” That is a bigger idea than plugins alone. It is a way to scale features without turning integration into a manual wiring project.

That is why the annotation system matters so much. It is a small mechanism with a large consequence: native code becomes discoverable, composable, and easier to host inside a platform that never stops growing.