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.
- Alchemy App Demo is interesting because it makes native extensibility look declarative, with annotations and module contracts doing the wiring that teams usually hard-code by hand.
- The repo’s real trick is discovery, not just plugins, because the host can find lifecycle hooks, shortcuts, QR flows, and tab surfaces without a giant central registry.
- Its plugin bridge shows how a JS call can become permissioned native work and then return cleanly to the web layer, which is the difference between a demo and an integration pattern.
- The architecture favors first-class UX and tighter session control, but it also binds extensions more closely to the host platform than a WebView-first model would.
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.
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.
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.
| Dimension | WebView-first extension | Annotation-driven native extension |
|---|---|---|
| Extension surface | HTML or embedded web app | Native modules and fragments |
| Registration model | Manual, often centralized | Declared through annotations and contracts |
| UX feel | Separate from the host | First-class inside the host app |
| Performance | Bound by web runtime overhead | Tighter access to native APIs |
| Session control | Indirect and often brittle | Explicit lifecycle and login hooks |
| Maintenance | Simple at first, messy at scale | More 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.





