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 desenvolvedores da Web usem JavaScript para acessar recursos nativos da plataforma, como câmera, 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 nativos da interface do Android (por exemplo, solicitações biométricas,
BottomSheetDialog) em uma página da Web. - Performance: descarregar tarefas computacionais pesadas para código nativo Java ou Kotlin.
- Persistência de dados: acesso a bancos de dados criptografados locais ou preferências compartilhadas.
- Transferências de dados grandes: passagem 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 todos ainda estejam disponíveis, eles diferem significativamente em segurança, usabilidade e desempenho.
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. Ele 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 ser carregada.
Inicialização: para garantir que o WebView injete o objeto JavaScript antes da execução de
qualquer script, chame addWebMessageListener antes de chamar
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.Quando você especifica uma origem confiável, como
https://example.com, o WebView garante que só expõe os objetos JavaScript injetados para páginas da Web carregadas dessa origem exata.O callback do listener nativo recebe um parâmetro
sourceOrigincom cada mensagem. Isso pode ser usado para verificar a origem exata do remetente se a sua ponte for compatível com várias origens permitidas.Como a WebView aplica estritamente essas verificações de origem no nível da plataforma, seu app geralmente pode confiar nas mensagens recebidas de um
sourceOriginconfiável como verdadeiras, eliminando a necessidade de validação rigorosa de payload na maioria das implementações padrão.- A WebView compara as regras com o esquema (HTTP/HTTPS), o host e a porta.
- O WebView ignora caminhos. Por exemplo,
https://example.compermitehttps://example.com/loginehttps://example.com/home. - A 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 no 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 (de interface) do aplicativo. Se a ponte precisar processar dados complexos, analisar JSON ou fazer pesquisas no 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 reter 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 por ela. Se o frame de origem sair da navegação ou for destruído, as mensagens enviadas usandopostMessage()no proxy serão ignoradas silenciosamente.Início no 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 comece 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 é carregada).
Limitação: essa API envia dados como strings ou matrizes byte[]. Para estruturas de dados mais complicadas, como objetos JSON, é necessário serializar para um desses formatos e 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): as chamadas JavaScript da página da Web
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 resposta (Web): a página da Web recebe a resposta assíncrona na função de callback
myObject.onmessage().
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!");
Use postWebMessage (alternativa)
O Android introduziu isso para oferecer uma alternativa assíncrona baseada em mensagens
semelhante ao window.postMessage da Web.
Como funciona: o app usa WebViewCompat.postWebMessage para enviar uma carga útil
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 ao conteúdo da Web.
Características:
- Assíncrono: assim 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. - Com reconhecimento de origem: é possível especificar um
targetOriginpara garantir que a WebView forneça dados apenas a um site confiável.
Limitações:
- Escopo: essa API limita a comunicação ao frame principal. Ele não é compatível com o envio de mensagens ou o direcionamento direto a iframes.
- Restrições de URI: não é possível usar esse método para conteúdo carregado com URIs
data:,file:ouloadData(), a menos que você especifique "*" como a origem de destino. Assim, qualquer página pode receber 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 strings em
versões anteriores do Android que não oferecem suporte a addWebMessageListener.
Usar addJavascriptInterface (legado)
O método mais antigo envolve injetar uma instância de objeto nativo diretamente na WebView.
Como funciona: você define uma classe Kotlin ou Java, anota os métodos permitidos com @JavascriptInterface e adiciona uma instância da classe à WebView usando addJavascriptInterface(Object, String).
Características:
- Síncrono: o ambiente de execução do JavaScript é bloqueado até que o método no seu código Android seja retornado.
- Concorrência segura: o sistema chama métodos em uma linha de execução em segundo plano, exigindo sincronização cuidadosa no lado do Kotlin ou do Java.
- Risco de segurança: por padrão,
addJavascriptInterfaceestá disponível para todos os frames na WebView, incluindo iframes. Ele não tem controle de acesso baseado na 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 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íncrono (listener na linha de execução principal) | Assíncrono | Síncrona |
| Segurança | Mais alta (com base na lista de permissões) | Alta (com 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 grandes transferências de dados
Gerencie a memória com cuidado ao transferir payloads grandes, como strings de vários megabytes ou arquivos binários, para evitar erros do tipo "O app não está respondendo" (ANR) ou falhas em dispositivos de 32 bits. Esta seção discute as várias técnicas e limitações associadas à transferência de grandes quantidades 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 sobrecarga ao tamanho dos dados, isso é muito 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 seu app nativo e o conteúdo da Web.
- Limitação: mesmo com matrizes de bytes, o sistema copia dados na fronteira de comunicação entre processos (IPC, na sigla em inglês) entre o app e o processo isolado que o WebView usa para renderizar o conteúdo da Web. Isso ainda consome memória significativa para arquivos muito grandes.
Os exemplos de código a seguir demonstram como configurar addWebMessageListener no
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
inserido 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 eficiente de dados em grande escala
Para arquivos muito grandes (mais de 10 MB), use o método shouldInterceptRequest 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);
}
});
Siga as 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 seu 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 seu
allowedOriginRulese verificar osourceOriginfornecido 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 corresponder a vários subdomínios (por exemplo,foo.example.com,bar.example.com).Observação: embora as regras de origem protejam contra sites mal-intencionados 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 estiver vulnerável a XSS armazenado, um invasor poderá executar um script agindo 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 ambiente de execução: as APIs de ponte recentes, incluindo
addWebMessageListener, fazem parte da biblioteca Jetpack Webkit. Portanto, sempre verifique se há suporte usandoWebViewFeature.isFeatureSupported()antes de ligar para eles.