El desarorllo de aplicaciones híbridas en Android frecuentemente utiliza WebView para acelerar la iteración de la interfaz de usuario. Aunque la carga de recursos (HTML, CSS, JS, imágenes) es generalmente rápida, el renderizado solo comienza cuando todos los recursos están listos. Para optimizar el rendimiento y la experiencia del usuario, se recomienda minimizar el uso de frameworks pesados y preferir JavaScript nativo.
En escenarios donde se requiere acceso a funcionalidades nativas del sistema operativo (como la cámara, galería o micrófono), es esencial establecer un puente de comunicación bidireccional. Dado que WebView está basado en el motor WebKit, proporciona mecanismos integrados para permitir que JavaScript invoque métodos de Java y viceversa.
Definición del Layout XML
A continuación se presenta una estructura de interfaz simplificada que contiene el contenedor web y un botón para desencadenar acciones desde el lado nativo.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/hybrid_web_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/trigger_js_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ejecutar método JS desde Java" />
</LinearLayout>
Implementación de la Lógica en Java
La actividad principal configura el WebView, habilita JavaScript, enyecta la interfaz puente y maneja la navegación. Es crucial prestar atención a la gestión de hilos al comunicarse desde Java hacia JavaScript.
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.lang.ref.WeakReference;
public class HybridActivity extends AppCompatActivity {
private WebView hybridWebView;
private Button executeJsButton;
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hybrid);
hybridWebView = findViewById(R.id.hybrid_web_container);
executeJsButton = findViewById(R.id.trigger_js_btn);
configureWebView();
setupButtonListener();
}
@SuppressLint("SetJavaScriptEnabled")
private void configureWebView() {
hybridWebView.getSettings().setJavaScriptEnabled(true);
hybridWebView.addJavascriptInterface(new NativeBridge(this), "AndroidNative");
// Necesario para manejar alertas, confirmaciones y otros diálogos de JS
hybridWebView.setWebChromeClient(new WebChromeClient());
hybridWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
hybridWebView.loadUrl("file:///android_asset/hybrid_page.html");
}
private void setupButtonListener() {
executeJsButton.setOnClickListener(v -> {
String payload = "datos_desde_nativo";
hybridWebView.loadUrl("javascript:displayNativeData('" + payload + "')");
});
}
private static class NativeBridge {
private final WeakReference<HybridActivity> activityRef;
private final Handler mainHandler;
public NativeBridge(HybridActivity activity) {
this.activityRef = new WeakReference<>(activity);
this.mainHandler = new Handler(Looper.getMainLooper());
}
@JavascriptInterface
public void showNativeToast() {
HybridActivity activity = activityRef.get();
if (activity != null) {
activity.runOnUiThread(() ->
Toast.makeText(activity, "Método nativo invocado desde JS", Toast.LENGTH_SHORT).show()
);
}
}
@JavascriptInterface
public void requestJsExecution() {
mainHandler.post(() -> {
HybridActivity activity = activityRef.get();
if (activity != null) {
Toast.makeText(activity, "Invocando JS desde el hilo nativo", Toast.LENGTH_SHORT).show();
String message = "mensaje_nativo";
activity.hybridWebView.loadUrl("javascript:displayNativeData('" + message + "')");
}
});
}
}
}
Consideraciones Técnicas Críticas
- Anotaciones de Seguridad: A partir de Android 4.2 (API 17), cualquier método Java que deba ser accesible desde JavaScript requiere la anotación
@JavascriptInterfacepara prevenir vulnerabilidades de seguridad. - Manejo de Diálogos: Es imperativo configurar un
WebChromeClient. Sin él, las funciones de JavaScript comoalert(),confirm()oprompt()no tendrán efecto visual. - Seguridad de Hilos (Thread Safety): Cuando JavaScript invoca un método Java, la ejecución ocurre en un hilo secundario llamado
JavaBridge. Si se necesita invocar JavaScript desde Java (o actualizar la UI), es obligatorio cambiar al hilo principal (UI Thread) utilizandorunOnUiThread()o unHandlerasociado al Looper principal. Intentar llamar a métodos deWebViewdirectamente desde el hiloJavaBridgeprovocará un fallo en la aplicación.
Estructura HTML y JavaScript
El siguiente archivo debe almacenarse en el directorio assets del proyecto. El objeto inyectado (AndroidNative) actúa como el espacio de nombres global en el contexto de JS para acceder a los métodos de Java.
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interfaz Híbrida</title>
<script type="text/javascript">
function invokeNativeFeature() {
if (window.AndroidNative) {
window.AndroidNative.showNativeToast();
} else {
alert("Entorno nativo no disponible");
}
}
function displayNativeData(payload) {
alert("Datos recibidos del nativo: " + payload);
const headerElement = document.getElementById("main_header");
if (headerElement) {
headerElement.innerText = "Actualizado: " + payload;
}
}
function triggerNativeCallback() {
if (window.AndroidNative) {
window.AndroidNative.requestJsExecution();
}
}
</script>
</head>
<body>
<h3 id="main_header">Comunicación Java-JavaScript</h3>
<button onclick="invokeNativeFeature()">Ejecutar Función Nativa 1</button>
<button onclick="displayNativeData('datos_locales')">Probar Interfaz Local</button>
<button onclick="triggerNativeCallback()">Ejecutar Función Nativa 2</button>
</body>
</html>
Nota técnica: Al cargar páginas desde el protocolo local file://, los diálogos nativos como alert() mostrarán la ruta del archivo en el título en lugar de un nombre de dominio. Para personalizar estos diálogos y mejorar la experiencia de usuario, es necesario sobrescribir los métodos correspondientes en WebChromeClient, como onJsAlert().