Acessar APIs nativas com a ponte JavaScript

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> de allowedOriginRules durante 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 sourceOrigin com 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 sourceOrigin confiá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.com permite https://example.com/login e https://example.com/home.
    • A WebView limita estritamente os caracteres curinga ao início do host para subdomínios. Por exemplo, https://*.example.com corresponde a https://foo.example.com, mas não a https://example.com. Se você precisar corresponder a https://example.com e 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 JavaScriptReplyProxy que pode ser usado para enviar mensagens de volta a esse frame específico. Você pode reter esse objeto replyProxy e 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 usando postMessage() 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) ou evaluateJavaScript() (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:

  1. Iniciação (app): o app nativo registra o listener com addWebMessageListener e carrega a página da Web com loadUrl().
  2. Envio de mensagens (Web): as chamadas JavaScript da página da Web myObject.postMessage(message) para iniciar a comunicação.
  3. Recebimento e resposta de mensagens (app): o app recebe a mensagem no callback do listener e responde usando o replyProxy.postMessage() fornecido.
  4. 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 targetOrigin para 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: ou loadData(), 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, addJavascriptInterface está 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 como WebView.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:

  1. 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.
  2. O app Android intercepta essa solicitação em WebViewClient.shouldInterceptRequest.
  3. 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 allowedOriginRules e verificar o sourceOrigin fornecido 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 usando WebViewFeature.isFeatureSupported() antes de ligar para eles.