Esta página discute os vários métodos e práticas recomendadas para estabelecer uma ponte nativa, também conhecida como ponte JavaScript, para facilitar a comunicação entre o conteúdo da Web em um WebView e um app Android host.
Isso permite que os desenvolvedores da Web usem JavaScript para acessar recursos da plataforma nativa, como a câmera, o sistema de arquivos ou sensores de hardware avançados, que as APIs da Web padrão normalmente não oferecem.
Casos de uso
Uma implementação de ponte JavaScript permite vários cenários de integração em que o conteúdo da Web exige acesso mais profundo ao sistema operacional Android. Confira alguns exemplos:
- Integração de plataforma: acionamento de componentes de interface nativa do Android (por
exemplo, comandos biométricos,
BottomSheetDialog) de uma página da Web. - Performance: descarregamento de tarefas computacionais pesadas para código Java ou Kotlin nativo.
- Persistência de dados: acesso a bancos de dados criptografados locais ou preferências compartilhadas.
- Transferências de dados grandes: transmissão de arquivos de mídia ou estruturas de dados complexas entre o app e o renderizador da Web.
Mecanismos de comunicação
O Android oferece três gerações principais de APIs para estabelecer uma ponte nativa. Embora todas ainda estejam disponíveis, elas diferem significativamente em segurança, usabilidade e performance.
Usar addWebMessageListener (recomendado)
addWebMessageListener é a abordagem mais moderna e recomendada para comunicação entre o conteúdo da Web e o código do app nativo. Ela combina a facilidade de uso da interface JavaScript com a segurança do sistema de mensagens.
Como funciona: o app adiciona um listener com um nome específico e um conjunto de
regras de origem permitidas. O WebView garante que o objeto JavaScript esteja presente no escopo global (window.objectName) desde o momento em que a página começa a carregar.
Inicialização: para garantir que o WebView injete o objeto JavaScript antes da execução de
qualquer script, chame addWebMessageListener antes de
loadUrl().
Principais recursos:
Segurança e confiança: ao contrário das APIs legadas, esse método exige um
Set<String>deallowedOriginRulesdurante a inicialização. Esse é o mecanismo principal para estabelecer confiança.Ao especificar uma origem confiável, como
https://example.com, o WebView garante que ele só exponha os objetos JavaScript injetados a páginas da Web carregadas dessa origem exata.O callback do listener nativo recebe um parâmetro
sourceOrigincom cada mensagem. Você pode usar isso para verificar a origem exata do remetente se a ponte oferece suporte a várias origens permitidas.Como o WebView aplica estritamente essas verificações de origem no nível da plataforma, seu app geralmente pode confiar em mensagens recebidas de uma
sourceOriginconfiável como verdadeiras, eliminando a necessidade de validação rigorosa de payload na maioria das implementações padrão.- O WebView corresponde a regras em relação ao esquema (HTTP/HTTPS), host e porta.
- O WebView ignora caminhos. Por exemplo,
https://example.compermitehttps://example.com/loginehttps://example.com/home. - O WebView limita estritamente os caracteres curinga ao início do host para subdomínios. Por exemplo,
https://*.example.comcorresponde ahttps://foo.example.com, mas não ahttps://example.com. Se você precisar corresponder ahttps://example.come aos subdomínios dele, adicione cada regra de origem separadamente à lista de permissões (por exemplo,"https://example.com", "https://*.example.com"). Não é possível usar caracteres curinga para o esquema ou no meio de um domínio.
Isso restringe a ponte a domínios verificados, impedindo que conteúdo de terceiros não autorizado ou iframes injetados executem código nativo.
Suporte a vários frames: funciona em todos os frames que correspondem às regras de origem
Linhas de execução: o callback do listener é executado na linha de execução principal (interface) do aplicativo. Se a ponte precisar processar dados complexos, analisar JSON ou fazer pesquisas de banco de dados, descarregue esse trabalho para uma linha de execução em segundo plano para evitar o congelamento da interface do aplicativo com um erro "O app não está respondendo" (ANR).
Bidirecional: quando a página da Web envia uma mensagem, o app recebe um
JavaScriptReplyProxyque pode ser usado para enviar mensagens de volta a esse frame específico. Você pode manter esse objetoreplyProxye usá-lo a qualquer momento para enviar qualquer número de mensagens à página, não apenas para responder a cada mensagem individual enviada pela página. Se o frame de origem sair ou for destruído, as mensagens enviadas usandopostMessage()no proxy serão ignoradas silenciosamente.Iniciação do lado do app: embora a página da Web sempre precise iniciar o canal de comunicação com o app, o app nativo pode solicitar unilateralmente que a página da Web inicie esse processo. O app nativo pode se comunicar com a página da Web usando
addDocumentStartJavaScript()(para avaliar o JavaScript antes do carregamento da página) ouevaluateJavaScript()(para avaliar o JavaScript depois que a página for carregada).
Limitação: essa API envia dados como strings ou matrizes byte[]. Para estruturas de dados mais complicadas, como objetos JSON, é necessário serializar isso em um desses formatos e, em seguida, desserializar do outro lado para reconstruir a estrutura de dados.
Exemplo de uso:
Para entender a sequência completa de uma troca de mensagens bidirecional, os eventos seguem esta ordem:
- Iniciação (app): o app nativo registra o listener com
addWebMessageListenere carrega a página da Web comloadUrl(). - Envio de mensagens (Web): o JavaScript da página da Web chama
myObject.postMessage(message)para iniciar a comunicação. - Recebimento e resposta de mensagens (app): o app recebe a mensagem no
callback do listener e responde usando o
replyProxy.postMessage()fornecido. - Recebimento de respostas (Web): a página da Web recebe a resposta assíncrona na
myObject.onmessage()função de callback.
Kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
// Handle the message from JS
replyProxy.postMessage("Acknowledged!")
}
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val allowedOrigins = setOf("https://www.example.com")
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
Java
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Handle the message from JS
replyProxy.postMessage("Acknowledged!");
};
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
Set<String> allowedOrigins = Set.of("https://www.example.com");
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
O JavaScript a seguir demonstra a implementação do lado do cliente de addWebMessageListener, permitindo que o conteúdo da Web receba mensagens do app nativo e envie as próprias mensagens pelo proxy myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Usar postWebMessage (alternativa)
O Android introduziu isso para fornecer uma alternativa assíncrona baseada em mensagens semelhante a window.postMessage da Web.
Como funciona: o app usa WebViewCompat.postWebMessage para enviar um payload
ao frame principal da página da Web. Para estabelecer um canal de comunicação bidirecional, crie um WebMessageChannel e transmita uma das portas dele com a mensagem para o conteúdo da Web.
Características:
- Assíncrono: como
addWebMessageListener, esse método usa mensagens assíncronas, o que garante que a página da Web permaneça responsiva às interações do usuário enquanto o app processa dados em segundo plano. - Reconhecimento de origem: é possível especificar um
targetOriginpara garantir que o WebView entregue dados apenas a um site confiável.
Limitações:
- Escopo: essa API limita a comunicação ao frame principal. Ela não oferece suporte ao endereçamento direto ou ao envio de mensagens para iframes.
- Restrições de URI: não é possível usar esse método para conteúdo carregado usando
data:URIs,file:URIs ouloadData(), a menos que você especifique "*" como a origem de destino. Isso permite que qualquer página receba a mensagem. - Risco de identidade: não há uma maneira clara de o conteúdo da Web verificar a identidade do remetente. Uma mensagem recebida pela página da Web pode ter sido originada do seu app nativo ou de outro iframe.
Use esse método quando precisar de um canal assíncrono simples para dados baseados em string em versões anteriores do Android que não oferecem suporte a addWebMessageListener.
Usar addJavascriptInterface (legado)
O método mais antigo envolve a injeção de uma instância de objeto nativo diretamente no WebView.
Como funciona: você define uma classe Kotlin ou Java, anota os
métodos permitidos com @JavascriptInterface e adiciona uma instância da classe ao
WebView usando addJavascriptInterface(Object, String).
Características:
- Síncrono: o ambiente de execução do JavaScript é bloqueado até que o método no código do Android seja retornado.
- Segurança de linhas de execução: o sistema chama métodos em uma linha de execução em segundo plano, exigindo uma sincronização cuidadosa no lado do Kotlin ou Java.
- Risco de segurança: por padrão,
addJavascriptInterfaceestá disponível para todos os frames no WebView, incluindo iframes. Ele não tem controle de acesso baseado em origem. Devido ao comportamento assíncrono do WebView, não é possível determinar com segurança o URL do frame que está chamando sua interface. Não confie em métodos comoWebView.getUrl()para verificação de segurança, porque eles não são garantidos como precisos e não indicam qual frame específico fez a solicitação.
Resumo dos mecanismos
A tabela a seguir oferece uma comparação rápida dos três principais mecanismos de implementação de ponte nativa:
| Método | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implementação | Assíncrona (listener na linha de execução principal) | Assíncrona | Síncrona |
| Segurança | Maior (baseada em lista de permissões) | Alta (reconhecimento de origem) | Baixa (sem verificações de origem) |
| Complexidade | Moderada | Moderada | Simples |
| Direção | Bidirecional | Bidirecional | Web para app |
| Versão mínima do WebView | Versão 82 (e Jetpack Webkit 1.3.0) | Versão 45 (e Jetpack Webkit 1.1.0) | Todas as versões |
| Recomendado | Sim | Não | Não |
Processar transferências de dados grandes
É necessário gerenciar a memória com cuidado ao transferir payloads grandes, como strings de vários megabytes ou arquivos binários, para evitar erros ou falhas de "O app não está respondendo" (ANR) em dispositivos de 32 bits. Esta seção discute as várias técnicas e limitações associadas à transferência de quantidades significativas de dados entre o aplicativo host e o conteúdo da Web.
Transferir dados binários com matrizes de bytes
Com a classe WebMessageCompat, é possível enviar matrizes byte[] diretamente
em vez de serializar dados binários em strings Base64. Como o Base64 adiciona aproximadamente 33% de overhead ao tamanho dos dados, isso é significativamente mais eficiente em termos de memória e mais rápido.
- Vantagem binária: transfira dados binários, como arquivos de imagem ou áudio, entre o app nativo e o conteúdo da Web.
- Limitação: mesmo com matrizes de bytes, o sistema copia dados no limite de comunicação entre processos (IPC) entre o app e o processo isolado que o WebView usa para renderizar o conteúdo da Web. Isso ainda consome uma quantidade significativa de memória para arquivos muito grandes.
Os exemplos de código a seguir demonstram como configurar addWebMessageListener no lado do app nativo para receber mensagens marcadas com WebMessageCompat.TYPE_ARRAY_BUFFER e, opcionalmente, responder com dados binários verificando WebViewFeature.MESSAGE_ARRAY_BUFFER.
Kotlin
fun setupWebView(webView: WebView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->
// Check if the received message is an ArrayBuffer
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val binaryData: ByteArray = message.arrayBuffer
// Process your binary data (image, audio, etc.)
println("Received bytes: ${binaryData.size}")
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
replyProxy.postMessage(replyBytes)
}
}
}
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
setOf("https://example.com"), // Security: restrict origins
listener
)
}
}
Java
public void setupWebView(WebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Check if the received message is an ArrayBuffer
if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
byte[] binaryData = message.getArrayBuffer();
// Process your binary data (image, audio, etc.)
System.out.println("Received bytes: " + binaryData.length);
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
replyProxy.postMessage(replyBytes);
}
}
};
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
Set.of("https://example.com"), // Security: restrict origins
listener
);
}
}
O código JavaScript a seguir demonstra a implementação do lado do cliente de addWebMessageListener, permitindo que o conteúdo da Web envie e receba dados binários (ArrayBuffer) para e do app nativo usando o proxy window.myBridge injetado no exemplo anterior.
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
const response = await fetch('image.jpg');
const buffer = await response.arrayBuffer();
// Check if the injected bridge object exists
if (window.myBridge) {
// You can send the ArrayBuffer directly
window.myBridge.postMessage(buffer);
}
}
// Receiving binary data from the app
if (window.myBridge) {
window.myBridge.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
console.log('Received binary data from App, length:', event.data.byteLength);
// Process the binary data (for example, as a Uint8Array)
const bytes = new Uint8Array(event.data);
console.log('First byte:', bytes[0]);
}
};
}
Carregamento de dados eficiente em grande escala
Para arquivos muito grandes (>10 MB), use o shouldInterceptRequest método para
transmitir dados:
- A página da Web inicia uma chamada
fetch()para um URL personalizado de marcador de posição. Por exemplo,https://app.local/large-file. - O app Android intercepta essa solicitação em
WebViewClient.shouldInterceptRequest. - O app retorna os dados como um
InputStream.
Isso permite transmitir dados em partes em vez de carregar todo o payload na memória de uma só vez.
A função JavaScript a seguir demonstra o código do lado do cliente para carregar com eficiência um arquivo binário grande do app nativo usando uma chamada fetch() padrão para um URL personalizado de marcador de posição.
async function fetchBinaryFromApp() {
try {
// This URL doesn't need to exist on the internet
const response = await fetch('https://app.local/data/large-file.bin');
if (!response.ok) throw new Error('Network response was not okay');
// For raw binary data:
const arrayBuffer = await response.arrayBuffer();
console.log('Received binary data, size:', arrayBuffer.byteLength);
// Process buffer (for example, new Uint8Array(arrayBuffer))
/*
// OR for an image:
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
*/
} catch (error) {
console.error('Fetch error:', error);
}
}
Os exemplos de código a seguir demonstram o lado do app nativo, usando o método WebViewClient.shouldInterceptRequest em Kotlin e Java, para transmitir um arquivo binário grande interceptando um URL de marcador de posição personalizado solicitado pelo conteúdo da Web.
Kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url ?: return null
// Check if this is our custom placeholder URL
if (url.host == "app.local" && url.path == "/data/large-file.bin") {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
val inputStream: InputStream = context.assets.open("my_data.pb")
// 2. Define Response Headers (Crucial for CORS/Fetch)
val headers = mutableMapOf<String, String>()
headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin
// 3. Return the response
return WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
)
} catch (e: Exception) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request)
}
}
Java
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String urlPath = request.getUrl().getPath();
String host = request.getUrl().getHost();
// Check if this is our custom placeholder URL
if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
InputStream inputStream = getContext().getAssets().open("my_data.pb");
// 2. Define Response Headers (Crucial for CORS/Fetch)
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin
// 3. Return the response
return new WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
);
} catch (Exception e) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request);
}
});
Seguir recomendações de segurança
Para proteger seu aplicativo e os dados do usuário, siga estas diretrizes ao implementar uma ponte:
Aplicar HTTPS: para garantir que conteúdo de terceiros mal-intencionado não possa invocar a lógica nativa do aplicativo, permita apenas a comunicação com origens seguras.
Confie nas regras de origem: a melhor maneira de lidar com a confiança é definir estritamente suas
allowedOriginRulese verificar asourceOriginfornecida no callback da mensagem. Evite usar o caractere curinga completo (*), que corresponde a todas as origens, como sua única regra de origem, a menos que seja absolutamente necessário. O uso de caracteres curinga para subdomínios (por exemplo,*.example.com) continua válido e seguro para correspondência de vários subdomínios (por exemplo,foo.example.com,bar.example.com).Observação: embora as regras de origem protejam contra sites maliciosos de terceiros e iframes ocultos, elas não podem proteger contra vulnerabilidades de scripting em vários sites (XSS) no seu próprio domínio confiável. Por exemplo, se a página da Web mostrar conteúdo gerado pelo usuário e for vulnerável a XSS armazenado, um invasor poderá executar um script que atue como sua origem confiável. Considere aplicar a validação aos payloads de mensagens antes de executar operações sensíveis da plataforma nativa.
Minimizar a área de superfície: exponha apenas os métodos ou dados específicos que a página da Web exige.
Verificar recursos no tempo de execução: as APIs de ponte recentes, incluindo
addWebMessageListener, fazem parte da biblioteca Jetpack Webkit. Portanto, sempre verifique o suporte usandoWebViewFeature.isFeatureSupported()antes de chamá-las.