Guide

Embedding in Android Apps

An existing Android app can show nsh apps inside its own UI. The runtime ships as an Android library, nsh.aar: you create a com.longbridge.nsh.NshView (a FrameLayout), add it to any layout and load an app into it. The nsh app renders with ordinary Android views next to your own.

Each NshView is one independent engine with its own JS VM; there are no global singletons, so several can run at once.

One host app with two nsh views (NshAppView on iOS, NshView on Android): each is its own engine with its own thread, VM, limits and capabilities; the host grants capabilities, sets limits and registers native components such as a map. One host app with two nsh views (NshAppView on iOS, NshView on Android): each is its own engine with its own thread, VM, limits and capabilities; the host grants capabilities, sets limits and registers native components such as a map.

#Get the library

The distribution contains both variants:

dist/nsh/embed/android/nsh.aar       release: bundles and signed packages
dist/nsh/embed/android/nsh-dev.aar   also connects to a `nsh dev` server

The AAR holds the Java runtime and libnsh.so for arm64-v8a and x86_64 (64-bit only). It declares the INTERNET permission and needs API 28 (Android 9) or newer.

Add it to the app module, for example from a libs folder:

// app/build.gradle.kts
dependencies {
    debugImplementation(files("libs/nsh-dev.aar"))
    releaseImplementation(files("libs/nsh.aar"))
}

#Ship a bundle with the app

Build the nsh app into the app’s assets:

nsh build App.tsx --release --out app/src/main/assets/nsh

This writes bundle.qbc (bytecode), bundle.js (source), bundle.js.map, capabilities.json (when nsh.json declares capabilities) and assets/ (the images and fonts the app imports).

#Show the app

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import com.longbridge.nsh.NshView;

public final class MainActivity extends Activity {
    private NshView nsh;

    @Override
    protected void onCreate(Bundle state) {
        super.onCreate(state);
        nsh = new NshView(this);
        nsh.setErrorListener(message -> Log.e("nsh", message));
        setContentView(nsh);
        nsh.loadAssetBundle("nsh");   // runs assets/nsh/bundle.qbc
    }

    @Override
    protected void onDestroy() {
        nsh.destroy();
        super.onDestroy();
    }
}

In Kotlin:

class MainActivity : Activity() {
    private lateinit var nsh: NshView

    override fun onCreate(state: Bundle?) {
        super.onCreate(state)
        nsh = NshView(this)
        nsh.setErrorListener { message -> Log.e("nsh", message) }
        setContentView(nsh)
        nsh.loadAssetBundle("nsh")
    }

    override fun onDestroy() {
        nsh.destroy()
        super.onDestroy()
    }
}

NshView can also sit inside any layout, below your own toolbar for example:

LinearLayout column = new LinearLayout(this);
column.setOrientation(LinearLayout.VERTICAL);
column.addView(toolbar);
column.addView(nsh, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
setContentView(column);

The view’s size is the app’s window: nsh lays the app out at the view’s size and again whenever it changes. Loading again replaces the running app. The complete sample is examples/embed-android/src/com/longbridge/nsh/embeddemo/MainActivity.java.

#NshView

MethodDescription
NshView(Context context), NshView(Context context, boolean trace)Creates an empty view. trace logs the time of every commit.
setErrorListener(ErrorListener listener)Receives fatal app errors (load failures, uncaught JS exceptions, a VM stopped by a limit) on the UI thread. They are always logged.
setLimits(int memoryMb, int stackKb, int loadMs, int callMs, int maxNodes)VM limits of the apps loaded next; 0 keeps a default.
setGrantedCapabilities(String json)The most the apps loaded next may do, as JSON in the form of nsh.json’s capabilities; null grants what each app declares. Returns false (and is ignored) for an invalid grant.
loadAssetBundle(String dir)Runs dir/bundle.qbc from the APK’s assets; app assets come from dir/assets/. Returns false if it cannot be read (also reported to the error listener).
loadBundleFile(File file)Runs a bundle.js file; assets come from assets/ next to it. Refuses .qbc files.
runPackage(NshPackage pkg)Runs a verified signed package, replacing the running app. A package runs once.
registerComponent(String name, NshComponent.Factory factory)Registers a native component.
connectDevServer(String url)Development: loads from a nsh dev server and reloads on every change. Returns false with the release AAR.
handleBack()Lets the app handle a back press (closes modals, pops screens, runs BackHandler listeners); returns whether it did.
destroy()Stops the app. The view can no longer be used.

setLimits and setGrantedCapabilities apply from the next load: call them before loadAssetBundle and friends.

#Limits

// 128 MB heap, default stack, 10 s to load, 1 s per call, 50 000 nodes
nsh.setLimits(128, 0, 10_000, 1_000, 50_000);

The defaults are a 256 MB heap, a 4096 KB stack, 10 s to load, 2 s per call and 200 000 nodes. What each limit does when it is hit: Sandbox and capabilities.

#Capabilities

nsh.setGrantedCapabilities(
    "{\"network\": {\"hosts\": [\"api.example.com\"]}, \"webview\": {\"hosts\": [\"help.example.com\"]}}");
nsh.loadAssetBundle("nsh");

A request must be allowed both by what the app declares in its nsh.json and by the grant. See Sandbox and capabilities.

#Signed packages

Verify downloaded packages off the UI thread, then run them on it:

NshTrust trust = new NshTrust(new File(getFilesDir(), "nsh"));
trust.addRoot(rootPublicKey);   // the contents of root.pub from `nsh keygen`

executor.execute(() -> {
    try {
        NshPackage pkg = trust.openPackage(downloadedFile);
        runOnUiThread(() -> nsh.runPackage(pkg));
    } catch (NshPackage.RefusedException e) {
        Log.w("nsh", "refused: " + e.getMessage());   // the running app stays
    }
});
APIDescription
new NshTrust(File stateDir)Opens or creates the trust store. Its state (newest revocation list, highest build accepted per app) persists there and packages’ assets are extracted under it. Trusts no key until addRoot. Usable from any thread; close() when done.
boolean addRoot(String publicKey)Trusts a root public key (base64, the contents of a .pub file). false for an invalid key.
boolean updateRevocations(String json)Applies a revocation list from nsh revoke, signed by a trusted root and newer than the store’s.
NshPackage openPackage(File file) throws NshPackage.RefusedExceptionVerifies, extracts the assets and accepts the package (older builds of the app are refused from then on). Slow for large packages: call it off the UI thread.
NshPackageappId, appName, version, build; close() if it will not run.

How keys, certificates and revocation work: Signed packages.

#Development

With nsh-dev.aar:

if (BuildConfig.DEBUG) {
    nsh.connectDevServer("ws://127.0.0.1:8790/ws");
} else {
    nsh.loadAssetBundle("nsh");
}

Run nsh dev App.tsx on your computer. For an emulator or a USB device, forward the port so the device’s own 127.0.0.1 reaches the server:

adb reverse tcp:8790 tcp:8790

Otherwise use the LAN address nsh dev prints. Assets come from the dev server over plain HTTP, so nsh-dev.aar declares android:usesCleartextTraffic="true", which the manifest merger applies to the app (a network security config of the app may override it). The release nsh.aar does not.

#Activity setup

  • Back button. On Android 13 and newer, NshView registers its own back callback while the app wants back presses (an open modal, a pushed screen, a BackHandler listener). On Android 12 and older, forward the press:

    @Override
    public void onBackPressed() {
        if (!nsh.handleBack()) super.onBackPressed();
    }

    For predictive back on Android 13+, set android:enableOnBackInvokedCallback="true" on the <application>, as packaged nsh apps do.

  • Keyboard. NshView scrolls focused inputs clear of the keyboard itself. Leave android:windowSoftInputMode unset or use adjustResize; with adjustPan the system moves the whole window instead.

  • Safe area. The view reports as insets only the parts of the system bars and the keyboard that overlap it, so an NshView below a toolbar gets no top inset. For edge-to-edge content, draw behind the system bars and let the app use SafeAreaView / useSafeAreaInsets().

  • Configuration changes. An activity recreated on rotation or a theme change destroys the view, and the app starts again from scratch. To keep it running, declare the changes the activity handles itself; packaged nsh apps use:

    <activity android:name=".MainActivity"
        android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density|fontScale|locale|layoutDirection"
        android:windowSoftInputMode="adjustResize" />

    NshView follows the new size, color scheme, font scale and layout direction itself.

  • Lifecycle. Call destroy() when the hosting activity or fragment goes away.

#See also