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 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> de allowedOriginRules durante 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 sourceOrigin com 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 sourceOrigin confiá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.com permite https://example.com/login e https://example.com/home.
    • O 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 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 JavaScriptReplyProxy que pode ser usado para enviar mensagens de volta a esse frame específico. Você pode manter 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 pela página. Se o frame de origem sair ou for destruído, as mensagens enviadas usando postMessage() 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) ou evaluateJavaScript() (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:

  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): o JavaScript da página da Web chama 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 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 targetOrigin para 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 ou loadData(), 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, addJavascriptInterface está 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 como WebView.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:

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

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 allowedOriginRules e verificar a sourceOrigin fornecida 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 usando WebViewFeature.isFeatureSupported() antes de chamá-las.