Android 原生 app 和 WebView 如何交互?

一、前言

在移动开发中,我们有时候会遇到这样的需求:

  • 有一部分功能需要网页实现(比如登录页、主页,已经有网页端了,不希望在 app 中再写一遍)
  • 另一部分功能需要原生实现(比如硬件访问、获取系统权限、或者一些注重性能的逻辑)

这时候 Hybrid App(原生 + WebView 混合应用) 就派上用场了。

本文带你全面了解 Android 原生 App 和 WebView 的交互方式,并附上实战示例。


二、交互

WebView 与原生 App 的交互也就两种:

  1. 网页调用 App 原生方法(JS → Native)
  2. App 调用网页 JS 方法(Native → JS)

双向通信的典型场景:

场景 方向 示例
网页点击按钮调用 app 功能 JS → Native window.myApp.nativeMethod('a')
App 收集设备信息反馈给网页 Native → JS webView.evaluateJavascript("jsMethod('a', 'b')")
登录状态同步 双向 网页通知 App 用户登录了,App 也可以主动查询网页是否已登录

2.1 编写本地 html

写一个本地的 html 文件 test_login.html,内容如下:

html 复制代码
<html>
<head><meta charset="utf-8"><title>Login Demo</title></head>
<body>
<h2>Hybrid Login Demo</h2>
<button onclick="login()">Login</button>
<button onclick="logout()">Logout</button>

<script>
    window.loginState = { isLoggedIn: false };

    window.isUserLoggedIn = function() {
      console.log("isUserLoggedIn = " + window.loginState.isLoggedIn);
      return window.loginState.isLoggedIn;
    }

    function login() {
      window.loginState.isLoggedIn = true;
      console.log("Login success!");
      if (window.myApp && window.myApp.onLoginStateChanged) {
        window.myApp.onLoginStateChanged(true);
      }
    }

    function logout() {
      window.loginState.isLoggedIn = false;
      console.log("Logout success!");
      if (window.myApp && window.myApp.onLoginStateChanged) {
        window.myApp.onLoginStateChanged(false);
      }
    }
</script>
</body>
</html>

运行效果:

可以看到,页面内容很简单,一个 title,两个按钮。一个用于登入,一个用于登出。

html 中维护了一个 loginState.isLoggedIn 属性,表示用户是否已登录。

提供了一个 isUserLoggedIn 函数,用于查询当前登录状态。

另外,还有一个 login 和一个 logout 方法,分别用于模拟登入登出,当状态改变后,通过 window.myApp.onLoginStateChanged 回调通知 app 登陆状态发生了改变。

2.2 编写 app

为了便于测试,我们将 test_login.html 文件,放在 assets 文件夹下,app 上的 WebView 直接加载本地 url 即可。

MainActivity 完整代码:

Kotlin 复制代码
package com.example.interaction

import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.example.interaction.ui.theme.WebViewJsInteractionDemoTheme

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            WebViewJsInteractionDemoTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    LoginWebView(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun LoginWebView(modifier: Modifier = Modifier) {
    var loginStatus by remember { mutableStateOf("Unknown") }
    val context = LocalContext.current
    val webViewRef = remember { mutableStateOf<WebView?>(null) }

    Column(modifier = modifier.fillMaxSize()) {
        AndroidView(
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth(),
            factory = { context ->
                WebView(context).apply {
                    settings.apply {
                        javaScriptEnabled = true
                        domStorageEnabled = true
                        allowFileAccess = true
                        allowContentAccess = true
                        cacheMode = WebSettings.LOAD_DEFAULT
                    }
                    webChromeClient = WebChromeClient()

                    webViewClient = object : WebViewClient() {
                        override fun onPageFinished(view: WebView?, url: String?) {
                            super.onPageFinished(view, url)
                            // Query login status when page is loaded
                            evaluateJavascript("isUserLoggedIn()") { result ->
                                val isLoggedIn = result?.contains("true") == true
                                loginStatus = if (isLoggedIn) "Logged In" else "Logged Out"
                            }
                        }
                    }

                    // Register the JavaScript interface
                    addJavascriptInterface(object {
                        @android.webkit.JavascriptInterface
                        fun onLoginStateChanged(isLoggedIn: Boolean) {
                            (context as ComponentActivity).runOnUiThread {
                                loginStatus = if (isLoggedIn) "Logged In" else "Logged Out"
                                Toast.makeText(context, "Login status changed: $loginStatus", Toast.LENGTH_SHORT).show()
                            }
                        }
                    }, "myApp")

                    WebView.setWebContentsDebuggingEnabled(true)
                    CookieManager.getInstance().setAcceptCookie(true)

                    loadUrl("file:///android_asset/test_login.html")
                    webViewRef.value = this
                }
            }
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Check login status button
        Button(
            onClick = {
                webViewRef.value?.evaluateJavascript("isUserLoggedIn()") { result ->
                    val isLoggedIn = result?.contains("true") == true
                    loginStatus = if (isLoggedIn) "Logged In" else "Logged Out"
                    Toast.makeText(context, "Login status: $loginStatus", Toast.LENGTH_SHORT).show()
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp)
        ) {
            Text("Check Login Status")
        }

        Spacer(modifier = Modifier.height(8.dp))

        Text(
            text = "Current Status: $loginStatus",
            modifier = Modifier.padding(horizontal = 16.dp)
        )
    }
}

运行效果:

可以看到,在 MainActivity 中,通过 addJavascriptInterface 函数添加了 onLoginStateChanged 接口供 Web 端调用,添加接口时,第二个参数是 name,Web 端将通过 name.接口名 来调用对应的接口,例如:window.myApp.onLoginStateChanged(true);

在点击 Check Login Status 按钮后,通过 WebView 的 evaluateJavascript 函数调用网页端的 isUserLoggedIn 函数,收到 result 后,更新 loginStatus 变量。

另外,还自定义了 WebViewClient,在 onPageFinished 调用后,主动调用一次 isUserLoggedIn 函数,完成 Current Status 的初始化。

三、后话

有一些需要注意的点:

  • 调用 js 方法时,结果是异步返回的,通过 listener 接收结果。
  • @JavascriptInterface 的方法在 非 UI 线程 执行,如果要更新 UI,需要使用 runOnUiThread
  • 设置了 WebView.setWebContentsDebuggingEnabled(true) 之后,通过 Chrome DevTools 可直接调试 WebView。方法是在 app 加载了网页后,在 Chrome 浏览器访问 chrome://inspect/#devices,在这里找到自己的设备,点击 inspect。我对这种方式不是很熟悉,就不过多介绍了。

附:一些常见的问题

注:这一节是 AI 总结的,不保真:

在 WebView 中,通过 webView.settings 可以获取到 WebSettings,它可以用来配置一系列网页渲染与访问能力。以下是关键属性解释:

属性 作用 是否常用 注意事项
javaScriptEnabled = true 启用网页中的 JavaScript 执行。没有这个,网页的交互和动态内容几乎全失效。 ✅ 必须 启用 JS 后要配合 addJavascriptInterface 谨慎使用,否则存在安全隐患。
domStorageEnabled = true 启用 HTML5 的 DOM Storage(localStorage / sessionStorage)。网页才能保存本地状态。 ✅ 常用 现代 Web 必备。
databaseEnabled = true 启用 Web SQL 数据库(旧标准)。 ⚠️ 较旧 新网页一般用 IndexedDB。
allowFileAccess = true 允许访问本地文件(file://)。 ✅ 常用 某些 WebView 资源加载或本地调试需要。
allowContentAccess = true 允许访问 content:// URI 内容(如系统媒体)。 ✅ 常用 安全风险低。
allowFileAccessFromFileURLs = true 允许网页 JS 从 file:// 页面访问其他本地文件。 ⚠️ 慎用 容易被恶意网页利用本地文件。
allowUniversalAccessFromFileURLs = true 允许 file:// 页面访问任意网络资源(http/https)。 ⚠️ 高风险 建议仅限调试环境启用。
useWideViewPort = true 启用自适应宽度,让网页以「网页比例」显示而非手机分辨率。 ✅ 常用 loadWithOverviewMode 一起使用更佳。
loadWithOverviewMode = true 缩放网页以适配屏幕宽度。 ✅ 常用 常配合 responsive 页面。
setSupportZoom(true) 支持缩放。 ✅ 常用 可搭配手势操作。
builtInZoomControls = true 启用内建缩放按钮。 ✅ 可选 通常在调试或旧网页中启用。
displayZoomControls = false 隐藏默认的缩放控件(仅保留手势缩放)。 ✅ 推荐 提升视觉体验。
cacheMode = WebSettings.LOAD_DEFAULT 启用缓存策略。 ✅ 常用 可选 LOAD_NO_CACHE 禁止缓存。

WebViewClient 和 WebChromeClient 的区别:

对比项 WebViewClient WebChromeClient
职责 控制页面导航与加载逻辑 控制网页中"浏览器行为"与 UI 事件
常用回调 shouldOverrideUrlLoadingonPageStartedonPageFinishedonReceivedError onProgressChangedonReceivedTitleonConsoleMessageonJsAlert
场景举例 拦截跳转、处理自定义 URL Scheme、控制加载动画 显示网页标题、监控加载进度、拦截 JS 弹窗、打印调试信息
比喻 浏览器"司机" 浏览器"仪表盘"
建议 必须设置一个(否则无法处理跳转) 可选(但调试与交互建议加)

👉 总结一句话

WebViewClient 负责"页面去哪",WebChromeClient 负责"页面看起来怎样"。

其他关键配置:

配置 作用
setLayerType(View.LAYER_TYPE_HARDWARE, null) 启用硬件加速,提升渲染性能(尤其是视频或动画)。
setOnLongClickListener { true } + isLongClickable = false 禁用长按(防止复制或保存图片)。
WebView.setWebContentsDebuggingEnabled(true) 允许通过 Chrome 调试网页内容(chrome://inspect)。
CookieManager.getInstance().setAcceptThirdPartyCookies(...)

以上就是本文的全部内容了,如果你喜欢本文的内容,欢迎点赞、投币、收藏、转发,更重要的是点一个大大的关注,这对我们有非常大的帮助......

相关推荐
小兔薯了5 小时前
7. LNMP-wordpress
android·运维·服务器·数据库·nginx·php
L***d6706 小时前
mysql的主从配置
android·mysql·adb
Sammyyyyy7 小时前
PHP 8.5 新特性:10 大核心改进
android·php·android studio
TO_ZRG8 小时前
Unity 通过 NativePlugin 接入Android SDK 指南
android·unity·游戏引擎
n***84078 小时前
Springboot-配置文件中敏感信息的加密:三种加密保护方法比较
android·前端·后端
方白羽9 小时前
一次由 by lazy 引发的“数据倒灌”,深入理解 `by`关键字、`lazy`函数的本质
android·kotlin·app
v***55349 小时前
MySQL 中如何进行 SQL 调优
android·sql·mysql
vx_vxbs6611 小时前
【SSM高校普法系统】(免费领源码+演示录像)|可做计算机毕设Java、Python、PHP、小程序APP、C#、爬虫大数据、单片机、文案
android·java·python·mysql·小程序·php·idea
j***827012 小时前
【MyBatisPlus】MyBatisPlus介绍与使用
android·前端·后端
ljt272496066112 小时前
Compose笔记(五十八)--LinearOutSlowInEasing
android·笔记·android jetpack