Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Documentation

Android

Les applications NSH s’exécutent sur Android avec des vues Android ordinaires et du texte StaticLayout, à partir d’Android 9 (API 28), sur arm64-v8a, armeabi-v7a et x86_64. Des applications entières écrites en Rust et en TSX s’exécutent et se packagent comme sur les autres plateformes, et une application existante peut embarquer des modules dans un NshView et charger les packages signés qu’elle télécharge, dans les limites et avec les capabilities qu’elle définit. Sur Android, os() vaut Os::Android (platform.os vaut "android" en TSX) et les props android={{…}} de TSX s’appliquent.

#Prérequis

  • Le SDK Android (avec platforms;android-35, les build tools et platform-tools) et un JDK, sur macOS, Windows ou Linux. Voir Installation.
  • Un émulateur, ou un appareil connecté via adb.

#Exécuter une application

Une application Rust s’exécute depuis son répertoire, une application TSX indique son point d’entrée :

nsh run --target android               # a Rust app
nsh run App.tsx --target android       # a TSX app

Démarrez d’abord un émulateur ou connectez un appareil (adb devices les liste ; NSH utilise le premier). Pour une application Rust, nsh compile le libnsh.so de l’application avec votre crate, installe l’APK et le démarre ; une modification du code Rust de la crate le recompile et le réinstalle. Pour une application TSX, il installe un APK hôte de développement (recompilé uniquement lorsque le runtime change) et redirige le port du serveur de développement avec adb reverse, ce qui fonctionne par USB sans réseau partagé ; l’application se met à jour dès que vous enregistrez, avec les logs dans le terminal.

#Packaging

nsh package --target android --run             # a Rust app: .nsh/out/<Name>.apk, installed and launched
nsh package App.tsx --target android --run     # a TSX app

L’APK d’une application Rust contient son propre libnsh.so, compilé avec votre crate, dont l’activité monte la racine.

L’APK est construit avec les build tools du SDK, sans Gradle, et signé avec la clé de debug sauf si vous fournissez un keystore de release. Signature de release et icônes : Packaging.

NSH compile libnsh.so avec un alignement ELF de 16 Ko et package les bibliothèques natives non compressées sur des frontières ZIP de 16 Ko. Un APK de release embarque libnsh.so sans ses tables de symboles ni ses informations de debug. La bibliothèque complète se trouve à côté du runtime dans symbols/<abi>/libnsh.so (celle d’une application Rust dans .nsh/rust/android-rust-release/symbols/<abi>/) : donnez-la à ndk-stack ou envoyez-la à Play Console comme symboles de debug natifs pour lire les stacks de crash natifs.

#Embarquer dans une application Android

Une application Android existante peut afficher des applications NSH dans sa propre interface. Le runtime est fourni sous forme de bibliothèque Android, nsh.aar : vous créez un com.longbridge.nsh.NshView (un FrameLayout), l’ajoutez à n’importe quel layout et y chargez une application TSX ou un package signé. Une application écrite en Rust est livrée sur Android sous la forme de l’APK que nsh package --target android construit. L’application NSH s’affiche avec des vues Android ordinaires, à côté des vôtres.

Chaque NshView est un moteur indépendant avec sa propre VM JS ; il n’y a aucun singleton global, donc plusieurs peuvent s’exécuter en même temps.

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.

#Obtenir la bibliothèque

La distribution contient les deux variantes :

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

L’AAR contient le runtime Java et libnsh.so pour arm64-v8a, armeabi-v7a et x86_64, avec leurs symboles : le build Gradle de l’application les retire lors du packaging et conserve les symboles pour ses rapports de crash. Il déclare la permission INTERNET et requiert l’API 28 (Android 9) ou plus récente. Il embarque ses règles R8 (proguard.txt), qu’un build minifié applique automatiquement : la bibliothèque native retrouve les classes du runtime par leur nom, donc com.longbridge.nsh reste inchangé.

Ajoutez-le au module de l’application, par exemple depuis un dossier libs :

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

#Livrer un bundle avec l’application

Compilez l’application TSX dans les assets de l’application :

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

Cela écrit bundle.nshc (bytecode), bundle.js (source), bundle.js.map, capabilities.json (lorsque nsh.json déclare des capabilities) et assets/ (les images et les polices que l’application importe).

#Afficher l’application

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.nshc
    }

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

En 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 peut aussi se placer dans n’importe quel layout, par exemple sous votre propre barre d’outils :

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);

La taille de la vue est celle de la fenêtre de l’application : NSH met en page l’application à la taille de la vue, puis à nouveau à chaque changement. Charger de nouveau remplace l’application en cours. L’exemple complet se trouve dans examples/embed-android/src/com/longbridge/nsh/embeddemo/MainActivity.java.

#NshView

MéthodeDescription
NshView(Context context), NshView(Context context, boolean trace)Crée une vue vide. trace journalise la durée de chaque commit.
setErrorListener(ErrorListener listener)Reçoit les erreurs fatales de l’application (échecs de chargement, exceptions JS non interceptées, VM arrêtée par une limite) sur le thread UI. Elles sont toujours journalisées.
setLimits(int memoryMb, int stackKb, int loadMs, int callMs, int maxNodes)Limites de la VM des applications chargées ensuite ; 0 conserve la valeur par défaut.
setGrantedCapabilities(String json)Le maximum que les applications chargées ensuite peuvent faire, en JSON sous la forme du capabilities de nsh.json ; null accorde ce que chaque application déclare. Renvoie false (et est ignoré) pour une autorisation invalide.
setLocale(Locale locale)La langue et la région de l’application à la place de celles de l’utilisateur : Intl, locale() et les composants les suivent immédiatement, et les applications chargées ensuite démarrent avec elles. null suit celles du système.
setFontScale(float scale)La taille du texte à la place de la taille de police du système (1 est la valeur par défaut) : les textes et les champs de saisie sont remis en page à cette taille et fontScale() la lit, immédiatement et pour les applications chargées ensuite ; les longueurs gardent leur taille. 0 suit celle du système.
setThemeOverrides(String json)L’identité de marque de l’hôte par-dessus le thème de l’application : un fichier de thème au format du "theme" de nsh.json. Ses tokens l’emportent sur ceux de l’application portant les mêmes noms, en mode clair et en mode sombre, et les autres tokens de l’application sont conservés. S’applique immédiatement et aux applications chargées ensuite ; null rend à l’application son propre thème. Lève IllegalArgumentException pour un fichier qui ne peut pas être analysé.
setStorageDirectory(File dir)L’emplacement où les applications chargées ensuite conservent leur stockage ; null : files/nsh/<app id> dans le répertoire privé de l’application, selon l’identifiant propre de l’application.
setCacheOptions(NshCacheOptions options)L’emplacement où les applications chargées ensuite conservent leurs caches, les images qu’elles téléchargent, et la quantité conservée par chacune. Construisez-en un avec NshCacheOptions.builder() : location(File), systemLocation() (la valeur par défaut : nsh dans le répertoire de cache, chaque application dans <app id>) ou noDiskCache() ; imagesMaxMegabytes(int) (256 par défaut) ou imagesOff().
loadAssetBundle(String dir)Exécute dir/bundle.nshc depuis les assets de l’APK ; les assets de l’application proviennent de dir/assets/. Renvoie false s’il ne peut pas être lu (également signalé à l’error listener).
loadBundleFile(File file)Exécute un fichier bundle.js ; les assets proviennent de assets/ à côté. Refuse les fichiers .nshc.
runPackage(NshPackage pkg)Exécute un package signé vérifié, en remplaçant l’application en cours. Un package ne s’exécute qu’une fois.
registerComponent(String name, NshComponent.Factory factory)Enregistre un composant natif.
setParams(Object params)Les paramètres de lancement des applications chargées ensuite (un Map ou JSONObject) : l’application les lit au démarrage sous la forme host.params (host). Renvoie false lorsqu’ils ne forment pas un objet JSON.
send(String name, Object data)Envoie un événement à l’application (host.on(name, …)) ; conservé jusqu’à ce qu’elle l’écoute, ou pour la prochaine application tant qu’aucune ne s’exécute. Depuis n’importe quel thread ; renvoie false après destroy().
handle(String name, HostHandler handler)Gère les appels (host.call) et les posts (host.post) de l’application pour name sur le thread UI ; null supprime le gestionnaire. Le gestionnaire répond via son NshHostReply, une seule fois, depuis n’importe quel thread.
connectDevServer(String url)Développement : charge depuis un serveur nsh dev et recharge à chaque modification. Renvoie false avec l’AAR de release.
handleBack()Laisse l’application gérer un appui sur le bouton retour (ferme les boîtes de dialogue, dépile les pages, exécute les listeners BackHandler) ; indique si elle l’a fait.
destroy()Arrête l’application et appelle destroy() sur chacun de ses composants natifs. La vue ne peut plus être utilisée.

setLimits, setGrantedCapabilities, setStorageDirectory et setCacheOptions s’appliquent à partir du prochain chargement : appelez-les avant loadAssetBundle et les méthodes similaires.

Chaque application conserve son stockage sous son propre identifiant sans aucun réglage : un package signé sous l’identifiant que le certificat de sa clé de release garantit, un bundle sous le id de son nsh.json (nsh/app.json parmi les assets de l’APK, écrit par nsh package). Appelez setStorageDirectory pour conserver les données d’une application à un emplacement choisi par l’hôte :

nsh.setStorageDirectory(new File(getFilesDir(), "nsh/com.example.orders"));
nsh.loadAssetBundle("orders");

#Dialoguer avec l’application

La vue et l’application qu’elle exécute communiquent par un canal qui leur est propre (host) : des paramètres de lancement que l’application lit au démarrage, des événements de l’application hôte, et des appels et des posts de l’application, avec des données JSON (maps, listes, chaînes, nombres et booléens de ce côté). Il ne nécessite aucune vue dans l’application ni aucun composant natif.

// Before loading: what the app opens first (`host.params` in TSX).
nsh.setParams(Map.of("conversation", intent.getStringExtra("conversation")));
nsh.loadAssetBundle("nsh");

// The app's `await host.call("user.profile", { id })`, on the UI thread.
nsh.handle("user.profile", (data, reply) -> {
    Object id = ((Map<?, ?>) data).get("id");
    api.profile(id, profile -> reply.resolve(Map.of("id", id, "name", profile.name)),
            error -> reply.reject(error.getMessage()));
});

// The app's `host.post("analytics", …)`: no answer.
nsh.handle("analytics", (data, reply) -> analytics.log(data));

// Later: the app hears `host.on("conversation.open", …)`.
nsh.send("conversation.open", Map.of("id", tappedConversation));

En Kotlin :

nsh.handle("user.profile") { data, reply ->
    val id = (data as Map<*, *>)["id"]
    lifecycleScope.launch {
        runCatching { api.profile(id) }
            .onSuccess { reply.resolve(mapOf("id" to id, "name" to it.name)) }
            .onFailure { reply.reject(it.message ?: "failed") }
    }
}

Les gestionnaires s’exécutent sur le thread UI ; un appel attend sa réponse dans l’application sans bloquer aucun des deux côtés. Une réponse qui n’est jamais donnée fait échouer l’appel une fois qu’elle est collectée, et un appel dont le nom n’a aucun gestionnaire échoue immédiatement. Les événements dont les noms ne sont pas encore écoutés par l’application sont conservés jusqu’à ce qu’elle les écoute, et les événements envoyés alors qu’aucune application ne s’exécute (avant loadAssetBundle, pendant un rechargement) vont à la suivante.

#Surcharges de thème

Une application embarquée dans plusieurs applications hôtes de marques différentes adopte la marque de chaque hôte : l’hôte fournit les tokens que sa marque modifie, sous forme de fichier de thème au format du "theme" de nsh.json (le cssVars de shadcn/ui, avec des valeurs écrites comme en CSS). Ils l’emportent sur les tokens de l’application portant les mêmes noms ; les autres tokens de l’application sont conservés. Donnez à un token sa valeur en mode sombre sous dark (une seule valeur définie sous light ou theme correspond à cette couleur dans les deux modes) :

nsh.setThemeOverrides("{ \"cssVars\": {"
        + " \"theme\": { \"radius\": \"0.75rem\" },"
        + " \"light\": { \"primary\": \"#e11d48\", \"primary-foreground\": \"#ffffff\" },"
        + " \"dark\": { \"primary\": \"#fb7185\", \"primary-foreground\": \"#1c1917\" } } }");
nsh.loadAssetBundle("orders");

Appelez-le avant ou après le chargement : les classes comme bg-primary et rounded-(--radius) changent de style immédiatement, et chaque application chargée ensuite démarre avec les surcharges dès sa première frame. setThemeOverrides(null) rend à l’application son propre thème.

#Liens ouverts par l’application

Les URL que l’application ouvre (openUrl, les liens d’un TextView avec links="open") parviennent d’abord au OpenUrlHandler de la vue, sur le thread UI : renvoyez true lorsque l’application hôte a géré l’URL, par exemple en la faisant passer par son propre routeur, et false pour que le système l’ouvre avec un intent ACTION_VIEW. Sans gestionnaire, le système ouvre chaque URL. Les URL lui parviennent après que la capability openUrl de l’application les a autorisées.

nsh.setOpenUrlHandler(url -> {
    Uri uri = Uri.parse(url);
    // The host app's own links open in the host app.
    if ("lb".equals(uri.getScheme()) || "longbridge.com".equals(uri.getHost())) {
        router.open(uri);
        return true;
    }
    return false;
});

#Limites

// 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);

Les valeurs par défaut sont un heap de 256 Mo, une stack de 4096 Ko, 10 s pour le chargement, 2 s par appel et 200 000 nœuds. Ce que fait chaque limite lorsqu’elle est atteinte : Sandbox et capabilities.

#Capabilities

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

Une demande doit être autorisée à la fois par ce que l’application déclare dans son nsh.json et par l’autorisation accordée. Voir Sandbox et capabilities.

#Packages signés

Vérifiez les packages téléchargés en dehors du thread UI, puis exécutez-les sur celui-ci :

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)Ouvre ou crée le trust store. Son état (la liste de révocation la plus récente, le build le plus élevé accepté par application) y persiste et les assets des packages y sont extraits. Ne fait confiance à aucune clé avant addRoot. Utilisable depuis n’importe quel thread ; close() une fois terminé.
boolean addRoot(String publicKey)Fait confiance à une clé publique racine (en base64, le contenu d’un fichier .pub). false pour une clé invalide.
boolean updateRevocations(String json)Applique une liste de révocation depuis nsh revoke, signée par une racine de confiance et plus récente que celle du store.
NshPackage openPackage(File file) throws NshPackage.RefusedExceptionVérifie, extrait les assets et accepte le package (les builds plus anciens de l’application sont alors refusés). Lent pour les gros packages : appelez-le en dehors du thread UI.
NshPackageappId, appName, version, build ; close() s’il ne peut pas s’exécuter.

Fonctionnement des clés, des certificats et de la révocation : Packages signés.

#Développement

Avec nsh-dev.aar :

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

Exécutez nsh dev App.tsx sur votre ordinateur. Pour un émulateur ou un appareil USB, redirigez le port afin que le 127.0.0.1 propre à l’appareil atteigne le serveur :

adb reverse tcp:8790 tcp:8790

Sinon, utilisez l’adresse LAN qu’affiche nsh dev. Les assets proviennent du serveur de développement en HTTP simple, donc nsh-dev.aar déclare android:usesCleartextTraffic="true", que le manifest merger applique à l’application (une configuration de sécurité réseau de l’application peut le remplacer). Le nsh.aar de release ne le fait pas.

#Configuration de l’activity

  • Bouton retour. NshView reçoit lui-même les appuis sur retour tant que l’application les souhaite (une boîte de dialogue ouverte, une page empilée, un listener BackHandler), et les laisse à l’activity dans le cas contraire. Avec android:enableOnBackInvokedCallback="true" sur Android 13 et versions ultérieures, il enregistre un OnBackInvokedCallback ; ailleurs (Android 12 et versions antérieures, ou attribut désactivé), il prend la touche retour avant le onBackPressed de l’activity, donc avant le OnBackPressedDispatcher d’AndroidX et les callbacks des fragments. Aucune redirection n’est nécessaire, dans une activity comme dans un fragment. Un hôte qui redirige déjà les appuis vers handleBack() continue de fonctionner : un appui n’atteint l’application qu’une seule fois.

    Pour le retour prédictif sur Android 13+, définissez android:enableOnBackInvokedCallback="true" sur le <application>, comme le font les applications NSH packagées.

  • Clavier. NshView fait défiler lui-même les champs de saisie ayant le focus pour les dégager du clavier. Laissez android:windowSoftInputMode non défini ou utilisez adjustResize ; avec adjustPan, le système déplace plutôt toute la fenêtre.

  • Safe area. La vue ne signale comme insets que les parties des barres système et du clavier qui la recouvrent, donc un NshView placé sous une barre d’outils n’obtient aucun inset supérieur. Pour un contenu bord à bord, dessinez derrière les barres système et laissez l’application utiliser le padding env(safe-area-inset-*) / safeAreaInsets() (safe_area() en Rust).

  • Changements de configuration. Une activity recréée lors d’une rotation ou d’un changement de thème détruit la vue, et l’application repart de zéro. Pour la garder en cours d’exécution, déclarez les changements que l’activity gère elle-même ; les applications NSH packagées utilisent :

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

    NshView suit lui-même la nouvelle taille, le schéma de couleurs, l’échelle de police, la densité d’affichage et le sens de mise en page ; l’application conserve son état. Les positions de défilement sont restaurées en coordonnées logiques dans les nouvelles limites du contenu. Les vues sticky et les fenêtres modales ouvertes suivent aussi la nouvelle densité.

  • Cycle de vie. Appelez destroy() lorsque l’activity ou le fragment hôte disparaît.

#Voir aussi