一、前言
在移动开发中,我们有时候会遇到这样的需求:
- 有一部分功能需要网页实现(比如登录页、主页,已经有网页端了,不希望在 app 中再写一遍)
- 另一部分功能需要原生实现(比如硬件访问、获取系统权限、或者一些注重性能的逻辑)
这时候 Hybrid App(原生 + WebView 混合应用) 就派上用场了。
本文带你全面了解 Android 原生 App 和 WebView 的交互方式,并附上实战示例。
二、交互
WebView 与原生 App 的交互也就两种:
- 网页调用 App 原生方法(JS → Native)
- 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 事件 |
| 常用回调 | shouldOverrideUrlLoading、onPageStarted、onPageFinished、onReceivedError |
onProgressChanged、onReceivedTitle、onConsoleMessage、onJsAlert |
| 场景举例 | 拦截跳转、处理自定义 URL Scheme、控制加载动画 | 显示网页标题、监控加载进度、拦截 JS 弹窗、打印调试信息 |
| 比喻 | 浏览器"司机" | 浏览器"仪表盘" |
| 建议 | 必须设置一个(否则无法处理跳转) | 可选(但调试与交互建议加) |
👉 总结一句话:
WebViewClient负责"页面去哪",WebChromeClient负责"页面看起来怎样"。
其他关键配置:
| 配置 | 作用 |
|---|---|
setLayerType(View.LAYER_TYPE_HARDWARE, null) |
启用硬件加速,提升渲染性能(尤其是视频或动画)。 |
setOnLongClickListener { true } + isLongClickable = false |
禁用长按(防止复制或保存图片)。 |
WebView.setWebContentsDebuggingEnabled(true) |
允许通过 Chrome 调试网页内容(chrome://inspect)。 |
CookieManager.getInstance().setAcceptThirdPartyCookies(...) |
以上就是本文的全部内容了,如果你喜欢本文的内容,欢迎点赞、投币、收藏、转发,更重要的是点一个大大的关注,这对我们有非常大的帮助......