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(...)

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

相关推荐
独自破碎E1 小时前
【BISHI9】田忌赛马
android·java·开发语言
代码s贝多芬的音符2 小时前
android 两个人脸对比 mlkit
android
darkb1rd4 小时前
五、PHP类型转换与类型安全
android·安全·php
gjxDaniel4 小时前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj504 小时前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
峥嵘life5 小时前
Android16 【CTS】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·学习
stevenzqzq6 小时前
Compose 中的状态可变性体系
android·compose
似霰6 小时前
Linux timerfd 的基本使用
android·linux·c++
darling3318 小时前
mysql 自动备份以及远程传输脚本,异地备份
android·数据库·mysql·adb
你刷碗8 小时前
基于S32K144 CESc生成随机数
android·java·数据库