Documentação
Android
Os apps NSH rodam no Android com views Android comuns e texto StaticLayout, a partir do Android 9 (API 28), em arm64-v8a, armeabi-v7a e x86_64. Apps inteiros escritos em Rust e em TSX rodam e são empacotados como nas outras plataformas, e um app existente pode incorporar módulos em um NshView e carregar pacotes assinados que ele baixa, sob limites e capabilities que ele define. No Android, os() é Os::Android (platform.os é "android" em TSX) e as props android={{…}} do TSX se aplicam.
#Requisitos
- O Android SDK (com
platforms;android-35, build tools eplatform-tools) e um JDK, em macOS, Windows ou Linux. Veja Instalação. - Um emulador, ou um dispositivo conectado via
adb.
#Executando um app
Um app Rust roda a partir do seu diretório; um app TSX informa o seu ponto de entrada:
nsh run --target android # a Rust app
nsh run App.tsx --target android # a TSX app
Inicie um emulador ou conecte um dispositivo antes (adb devices lista os disponíveis; o NSH usa o primeiro). Para um app Rust, nsh compila o libnsh.so do app com o seu crate, instala o APK e o inicia; uma alteração no código Rust do crate compila e instala novamente. Para um app TSX, ele instala um APK host de desenvolvimento (recompilado apenas quando o runtime muda) e encaminha a porta do servidor de desenvolvimento com adb reverse, de modo que funciona por USB sem uma rede compartilhada; o app é atualizado conforme você salva, com os logs no terminal.
#Empacotamento
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
O APK de um app Rust contém o seu próprio libnsh.so, compilado com o seu crate, cuja activity monta a raiz.
O APK é compilado com as build tools do SDK, sem Gradle, e assinado com a chave de debug, a menos que você informe um keystore de release. Assinatura de release e ícones: Empacotamento.
O NSH compila libnsh.so com alinhamento ELF de 16 KB e empacota as bibliotecas nativas sem compressão em limites ZIP de 16 KB. Um APK de release inclui libnsh.so sem as tabelas de símbolos e as informações de debug. A biblioteca completa fica ao lado do runtime em symbols/<abi>/libnsh.so (a de um app Rust em .nsh/rust/android-rust-release/symbols/<abi>/): entregue-a ao ndk-stack ou envie-a ao Play Console como símbolos de debug nativos para ler os stacks de crashes nativos.
#Incorporando em um app Android
Um app Android existente pode exibir apps NSH dentro da sua própria UI. O runtime é distribuído como uma biblioteca Android, nsh.aar: você cria uma com.longbridge.nsh.NshView (uma FrameLayout), a adiciona a qualquer layout e carrega nela um app TSX ou um pacote assinado. Um app escrito em Rust é distribuído no Android como o APK que nsh package --target android compila. O app NSH é renderizado com views Android comuns ao lado das suas.
Cada NshView é um engine independente, com a sua própria VM JS; não há singletons globais, então vários podem rodar ao mesmo tempo.
#Obtendo a biblioteca
A distribuição contém as duas 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
O AAR contém o runtime Java e libnsh.so para arm64-v8a, armeabi-v7a e x86_64, com seus símbolos: o build Gradle do app os remove ao empacotar o app e mantém os símbolos para os seus relatórios de crash. Ele declara a permissão INTERNET e exige API 28 (Android 9) ou mais recente. Ele traz as suas regras do R8 (proguard.txt), que um build minificado aplica por conta própria: a biblioteca nativa encontra as classes do runtime pelo nome, então com.longbridge.nsh permanece como está.
Adicione-o ao módulo do app, por exemplo a partir de uma pasta libs:
// app/build.gradle.kts
dependencies {
debugImplementation(files("libs/nsh-dev.aar"))
releaseImplementation(files("libs/nsh.aar"))
}
#Distribuindo um bundle com o app
Compile o app TSX para dentro dos assets do app:
nsh build App.tsx --release --out app/src/main/assets/nsh
Isso gera bundle.nshc (bytecode), bundle.js (código-fonte), bundle.js.map, capabilities.json (quando nsh.json declara capabilities) e assets/ (as imagens e fontes que o app importa).
#Exibindo o 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.nshc
}
@Override
protected void onDestroy() {
nsh.destroy();
super.onDestroy();
}
}
Em 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 também pode ficar dentro de qualquer layout, abaixo da sua própria toolbar, por exemplo:
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);
O tamanho da view é a janela do app: o NSH faz o layout do app no tamanho da view e de novo sempre que ele muda. Carregar novamente substitui o app em execução. O exemplo completo está em examples/embed-android/src/com/longbridge/nsh/embeddemo/MainActivity.java.
#NshView
| Método | Descrição |
|---|---|
NshView(Context context), NshView(Context context, boolean trace) |
Cria uma view vazia. trace registra no log o tempo de cada commit. |
setErrorListener(ErrorListener listener) |
Recebe os erros fatais do app (falhas de carregamento, exceções JS não tratadas, uma VM interrompida por um limite) na thread de UI. Eles são sempre registrados no log. |
setLimits(int memoryMb, int stackKb, int loadMs, int callMs, int maxNodes) |
Limites de VM dos apps carregados a seguir; 0 mantém o padrão. |
setGrantedCapabilities(String json) |
O máximo que os apps carregados a seguir podem fazer, como JSON no formato de nsh.json em capabilities; null concede o que cada app declara. Retorna false (e é ignorado) para uma concessão inválida. |
setLocale(Locale locale) |
O idioma e a região do app no lugar dos do usuário: Intl, locale() e os componentes o seguem imediatamente, e os apps carregados depois já começam com ele. null segue o do sistema. |
setFontScale(float scale) |
O tamanho do texto no lugar do tamanho de fonte do sistema (1 é o padrão): textos e inputs têm o layout refeito com ele e fontScale() o lê, imediatamente e para os apps carregados depois; os comprimentos mantêm o seu tamanho. 0 segue o do sistema. |
setThemeOverrides(String json) |
A marca do host sobre o tema do app: um arquivo de tema no formato de "theme" em nsh.json. Os seus tokens prevalecem sobre os do app com os mesmos nomes, nos modos claro e escuro, e os demais tokens do app permanecem. Aplica-se imediatamente e aos apps carregados depois; null devolve ao app o seu próprio tema. Lança IllegalArgumentException para um arquivo que não pode ser interpretado. |
setStorageDirectory(File dir) |
Onde os apps carregados a seguir guardam o seu storage; null: files/nsh/<app id> no diretório privado do app, pelo id do próprio app. |
setCacheOptions(NshCacheOptions options) |
Onde os apps carregados a seguir guardam os seus caches, as imagens que baixam, e quanto cada um mantém. Crie um com NshCacheOptions.builder(): location(File), systemLocation() (o padrão: nsh no diretório de cache, cada app em <app id>) ou noDiskCache(); imagesMaxMegabytes(int) (256 por padrão) ou imagesOff(). |
loadAssetBundle(String dir) |
Executa dir/bundle.nshc a partir dos assets do APK; os assets do app vêm de dir/assets/. Retorna false se não puder ser lido (também informado ao error listener). |
loadBundleFile(File file) |
Executa um arquivo bundle.js; os assets vêm de assets/ ao lado dele. Recusa arquivos .nshc. |
runPackage(NshPackage pkg) |
Executa um pacote assinado verificado, substituindo o app em execução. Um pacote roda uma única vez. |
registerComponent(String name, NshComponent.Factory factory) |
Registra um componente nativo. |
setParams(Object params) |
Os parâmetros de inicialização dos apps carregados a seguir (um Map ou JSONObject): o app os lê ao iniciar como host.params (host). Retorna false quando não são um objeto JSON. |
send(String name, Object data) |
Envia um evento ao app (host.on(name, …)); é mantido até que ele o escute, ou para o próximo app enquanto nenhum estiver rodando. Qualquer thread; retorna false após destroy(). |
handle(String name, HostHandler handler) |
Trata as chamadas (host.call) e os posts (host.post) do app para name na thread de UI; null remove o handler. O handler responde por meio do seu NshHostReply, uma única vez, a partir de qualquer thread. |
connectDevServer(String url) |
Desenvolvimento: carrega de um servidor nsh dev e recarrega a cada alteração. Retorna false com o AAR de release. |
handleBack() |
Permite que o app trate um pressionar de voltar (fecha diálogos, remove páginas, executa listeners de BackHandler); retorna se ele o tratou. |
destroy() |
Para o app e chama destroy() em cada um dos seus componentes nativos. A view não pode mais ser usada. |
setLimits, setGrantedCapabilities, setStorageDirectory e setCacheOptions valem a partir do próximo carregamento: chame-os antes de loadAssetBundle e afins.
Cada app mantém o seu storage sob o seu próprio id, sem nenhuma configuração: um pacote assinado sob o id que o certificado da sua chave de release garante, um bundle sob o id do seu nsh.json (nsh/app.json entre os assets do APK, escrito por nsh package). Chame setStorageDirectory para manter os dados de um app em um local escolhido pelo host:
nsh.setStorageDirectory(new File(getFilesDir(), "nsh/com.example.orders"));
nsh.loadAssetBundle("orders");
#Conversando com o app
A view e o app que ela executa conversam por um canal próprio (host): parâmetros de inicialização que o app lê ao iniciar, eventos do app host, e chamadas e posts do app, com dados JSON (maps, listas, strings, números e booleanos deste lado). Não precisa de nenhuma view no app nem de nenhum componente nativo.
// 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));
Em 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") }
}
}
Os handlers rodam na thread de UI; uma chamada espera pela sua resposta no app sem bloquear nenhum dos lados. Uma resposta que nunca é dada faz a chamada falhar quando for coletada, e uma chamada de um nome sem handler falha imediatamente. Eventos de nomes que o app ainda não escuta são mantidos até que ele os escute, e eventos enviados enquanto nenhum app está rodando (antes de loadAssetBundle, durante um reload) vão para o próximo.
#Sobrescrita de tema
Um app incorporado em vários apps host com marcas diferentes assume a marca de cada host: o host informa os tokens que a sua marca altera, como um arquivo de tema no formato de "theme" em nsh.json (cssVars do shadcn/ui, com valores como o CSS os escreve). Eles prevalecem sobre os tokens do app com os mesmos nomes; os demais tokens do app permanecem. Dê a um token o seu valor para o modo escuro em dark (um token definido apenas em light ou theme tem aquela cor nos dois modos):
nsh.setThemeOverrides("{ \"cssVars\": {"
+ " \"theme\": { \"radius\": \"0.75rem\" },"
+ " \"light\": { \"primary\": \"#e11d48\", \"primary-foreground\": \"#ffffff\" },"
+ " \"dark\": { \"primary\": \"#fb7185\", \"primary-foreground\": \"#1c1917\" } } }");
nsh.loadAssetBundle("orders");
Chame antes ou depois de carregar: classes como bg-primary e rounded-(--radius) mudam de estilo imediatamente, e todo app carregado depois já começa com as sobrescritas no primeiro frame. setThemeOverrides(null) devolve ao app o seu próprio tema.
#Links que o app abre
As URLs que o app abre (openUrl, os links de um TextView com links="open") chegam primeiro ao OpenUrlHandler da view, na thread de UI: retorne true quando o app host tratou a URL, por exemplo roteando-a pelo seu próprio router, e false para que o sistema a abra com um intent ACTION_VIEW. Sem um handler, o sistema abre todas as URLs. As URLs chegam a ele depois que a capability openUrl do app as permitiu.
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);
Os padrões são 256 MB de heap, 4096 KB de stack, 10 s para carregar, 2 s por chamada e 200 000 nós. O que cada limite faz quando é atingido: Sandbox e capabilities.
#Capabilities
nsh.setGrantedCapabilities(
"{\"network\": {\"hosts\": [\"api.example.com\"]}, \"webview\": {\"hosts\": [\"help.example.com\"]}}");
nsh.loadAssetBundle("nsh");
Uma requisição precisa ser permitida tanto pelo que o app declara em seu nsh.json quanto pela concessão. Veja Sandbox e capabilities.
#Pacotes assinados
Verifique os pacotes baixados fora da thread de UI e execute-os nela:
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
}
});
| API | Descrição |
|---|---|
new NshTrust(File stateDir) |
Abre ou cria o trust store. O seu estado (a lista de revogação mais recente, o maior build aceito por app) persiste ali, e os assets dos pacotes são extraídos sob ele. Não confia em nenhuma chave até addRoot. Pode ser usado de qualquer thread; close() quando concluído. |
boolean addRoot(String publicKey) |
Confia em uma chave pública raiz (base64, o conteúdo de um arquivo .pub). false para uma chave inválida. |
boolean updateRevocations(String json) |
Aplica uma lista de revogação de nsh revoke, assinada por uma raiz confiável e mais nova que a do store. |
NshPackage openPackage(File file) throws NshPackage.RefusedException |
Verifica, extrai os assets e aceita o pacote (builds mais antigos do app são recusados a partir de então). Lento para pacotes grandes: chame fora da thread de UI. |
NshPackage |
appId, appName, version, build; close() se ele não puder ser executado. |
Como funcionam chaves, certificados e revogação: Pacotes assinados.
#Desenvolvimento
Com nsh-dev.aar:
if (BuildConfig.DEBUG) {
nsh.connectDevServer("ws://127.0.0.1:8790/ws");
} else {
nsh.loadAssetBundle("nsh");
}
Execute nsh dev App.tsx no seu computador. Para um emulador ou um dispositivo USB, encaminhe a porta para que o 127.0.0.1 do próprio dispositivo alcance o servidor:
adb reverse tcp:8790 tcp:8790
Caso contrário, use o endereço de LAN que nsh dev imprime. Os assets vêm do servidor de desenvolvimento por HTTP simples, então nsh-dev.aar declara android:usesCleartextTraffic="true", que o manifest merger aplica ao app (uma network security config do app pode sobrescrevê-lo). O nsh.aar de release não faz isso.
#Configuração da activity
-
Botão voltar.
NshViewrecebe os pressionamentos de voltar por conta própria enquanto o app os quer (um diálogo aberto, uma página empilhada, um listener deBackHandler), e os deixa para a activity caso contrário. Comandroid:enableOnBackInvokedCallback="true"no Android 13 e mais recentes, ele registra umOnBackInvokedCallback; nas demais versões (Android 12 e anteriores, ou com o atributo desligado), ele captura a tecla voltar antes doonBackPressedda activity, ou seja, antes doOnBackPressedDispatcherdo AndroidX e dos callbacks dos fragments. Nenhum encaminhamento é necessário, em uma activity ou em um fragment. Um host que já encaminha os pressionamentos parahandleBack()continua funcionando: cada pressionamento chega ao app uma única vez.Para o predictive back no Android 13+, defina
android:enableOnBackInvokedCallback="true"no<application>, como fazem os apps NSH empacotados. -
Teclado.
NshViewrola os inputs com foco para fora do alcance do teclado por conta própria. Deixeandroid:windowSoftInputModesem definir ou useadjustResize; comadjustPan, o sistema move a janela inteira. -
Safe area. A view informa como insets apenas as partes das barras do sistema e do teclado que a sobrepõem, então um
NshViewabaixo de uma toolbar não recebe inset superior. Para conteúdo edge-to-edge, desenhe atrás das barras do sistema e deixe o app usar o paddingenv(safe-area-inset-*)/safeAreaInsets()(safe_area()em Rust). -
Mudanças de configuração. Uma activity recriada em uma rotação ou mudança de tema destrói a view, e o app começa do zero novamente. Para mantê-lo em execução, declare as mudanças que a activity trata por conta própria; os apps NSH empacotados usam:
<activity android:name=".MainActivity" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density|fontScale|locale|layoutDirection" android:windowSoftInputMode="adjustResize" />NshViewacompanha por conta própria o novo tamanho, esquema de cores, escala de fonte, densidade de tela e direção de layout; o app mantém o seu estado. As posições de rolagem são restauradas em coordenadas lógicas dentro dos novos limites do conteúdo. As views sticky e as janelas modais abertas também acompanham a nova densidade. -
Ciclo de vida. Chame
destroy()quando a activity ou o fragment host for encerrado.
#Veja também
- Building with Rust
- Embedding in iOS apps
- Native components
- Signed packages
- Packaging: um APK standalone sem escrever nenhum código Java