1. 为什么我要用 Trae Android Studio 写一个 WebView 壳如果你手头有一个已经跑通的 H5 页面想把它快速塞进安卓应用里最省事的路线就是 Kotlin WebView 做一个壳。这个场景特别适合内部工具、活动页、后台管理面板、需要调用少量原生能力的混合应用。你不需要从零学 Compose 全套只要把 WebView 初始化、网络权限、JS 注入、按键事件这几件事理顺一个能用的壳半天就能跑起来。我这次的实际需求是用 Trae 作为 AI 辅助编码工具配合 Android Studio 建一个 Empty Activity 项目在 Compose 里嵌入 WebView加载本地 assets 里的 HTML同时让原生层捕获方向键通过evaluateJavascript往页面里注入 JS 改内容。过程中踩的坑不少网络权限加错位置、WebView 抢走按键事件、AndroidView异步导致 WebView 实例为 null、evaluateJavascript不生效。这篇笔记把可复制的配置和排障过程完整写出来你照着做能少走弯路。另外AI 辅助编码这块我会用 TaoToken 统一管理 Key 和 API 通道这样 Trae 里切换模型、换项目时不用到处翻 Key接入文档和模型对话入口都在一个地方后面会给出具体配置方式。2. 前置准备TaoToken 统一 Key 与 API 通道在开始写代码之前先把 AI 辅助编码的通道理顺。Trae 这类工具在调用模型时需要一个可用的 API 端点和 Key。我的做法是统一走 TaoToken好处是 Key 集中管理换工具、换项目时只改一处配置不用在每个 IDE 里重复填。具体操作路径打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建 API KeyKey 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI 基础地址是https://taotoken.net/api注意这个地址不带 UTM 参数直接填到工具的 Base URL 里即可。如果你用的是 Claude Code 这类命令行编码工具可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentClaudeCodeAnthropicutm_campaignrewrite 的接入说明如果是长期编码或 Agent 场景Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。注意Key 只创建一次就够后续 Trae、Android Studio 插件、命令行工具都复用同一个 Key。不要把 Key 硬编码进build.gradle或提交到 Git建议放在本地环境变量或工具的配置面板里。配置完成后你可以在模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodelutm_campaignrewrite 先发一条测试消息确认通道可用再回到 Android Studio 里让 Trae 生成代码。3. 项目骨架与 Gradle 配置3.1 创建 Empty Activity 项目打开 Android Studio选择 New Project模板选 Empty Activity语言选 KotlinMinimum SDK 建议 API 24 以上。项目创建后等待右下角 Gradle 同步完成第一次会下载依赖比较吃网络和内存。3.2 build.gradle 关键配置WebView 本身不需要额外依赖但 Compose 项目要确保androidx.compose.ui:ui-viewinterop可用AndroidView就在这个包里。模块级build.gradle.kts参考配置android { namespace com.example.webviewdemo compileSdk 34 defaultConfig { applicationId com.example.webviewdemo minSdk 24 targetSdk 34 versionCode 1 versionName 1.0 } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion 1.5.8 } } dependencies { implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.activity:activity-compose:1.8.2) implementation(androidx.compose.ui:ui:1.6.0) implementation(androidx.compose.ui:ui-viewinterop:1.6.0) implementation(androidx.compose.material3:material3:1.2.0) }版本号按你本地 SDK 实际情况调整重点是ui-viewinterop必须在否则AndroidView无法导入。3.3 AndroidManifest.xml 权限配置网络权限是 WebView 加载在线页面的前提但加的位置很关键。正确做法是在manifest下、application之前加?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:toolshttp://schemas.android.com/tools uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:themestyle/Theme.WebViewDemo tools:targetApi31 activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest我踩过的坑让 AI 加权限时它一度把整个AndroidManifest.xml清空只留一行权限或者把权限加到了application内部导致 XML 解析失败。遇到这种情况直接拒绝重新用明确提示词要求「在原有配置基础上追加权限不要改动其他节点」。4. WebView 初始化与 JS 交互代码4.1 MainActivity.kt 完整实现核心思路用AndroidView创建 WebView通过回调把实例传给 Activity 层Activity 在onKeyDown里捕获方向键再用evaluateJavascript注入 JS。关键点是AndroidView的 factory 是异步执行的不能直接在 Composable 返回时拿 WebView 实例必须用回调。package com.example.webviewdemo import android.os.Bundle import android.util.Log import android.view.KeyEvent import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import java.io.IOException var webViewInstance: WebView? null class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Surface(modifier Modifier.fillMaxSize()) { Greeting(Android) { webView - webViewInstance webView } } } } } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { KeyEvent.KEYCODE_DPAD_UP - { Log.d(KeyEvent, 上方向键被按下, webView$webViewInstance) webViewInstance?.evaluateJavascript( document.body.innerText new Date().toString();, null ) return true } } return super.onKeyDown(keyCode, event) } } Composable fun Greeting(name: String, onWebViewCreated: (WebView) - Unit {}) { val context LocalContext.current AndroidView( factory { ctx - val webView WebView(ctx).apply { settings.javaScriptEnabled true isFocusable false isFocusableInTouchMode false webViewClient object : WebViewClient() { override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val url request.url.toString() if (url https://www.example.com/) { try { val inputStream context.assets.open(example.html) return WebResourceResponse(text/html, UTF-8, inputStream) } catch (e: IOException) { e.printStackTrace() } } Log.d(WebViewRequests, 请求链接: $url) return super.shouldInterceptRequest(view, request) } } loadUrl(https://www.example.com) } onWebViewCreated(webView) webView }, modifier Modifier.fillMaxSize() ) }4.2 本地 assets 资源在src/main/assets/目录下新建example.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 title示例页面/title /head body h1这是一个本地资源文件示例/h1 p该文件从 Android 的 assets 目录加载。/p /body /htmlshouldInterceptRequest里拦截https://www.example.com/返回 assets 里的 HTML这样 WebView 显示的是本地内容但地址栏逻辑上还是在线地址方便后续替换成真实远程页面。4.3 按键事件为什么会被 WebView 吃掉WebView 默认会优先处理焦点和按键事件导致 Activity 的onKeyDown收不到。解决办法是给 WebView 设置isFocusable false和isFocusableInTouchMode false让按键事件冒泡到 Activity。我一开始没加这两行方向键日志死活打不出来加上之后立刻正常。5. 验证请求与真机调试5.1 编译运行与日志验证在 Android Studio 里点绿色运行按钮选择模拟器或真机。启动后点击应用白色区域按键盘上下键在 Logcat 里过滤KeyEvent标签应该能看到D/KeyEvent: 上方向键被按下, webViewandroid.webkit.WebView{...}同时 WebView 页面里的文字会被替换成当前时间说明evaluateJavascript注入成功。5.2 adb 验证步骤如果你用真机调试先确认设备连接adb devices输出应包含你的设备序列号。然后查看实时日志adb logcat -s KeyEvent WebViewRequests WebViewInit按方向键时观察KeyEvent输出页面加载时观察WebViewRequests输出请求链接。如果WebViewRequests里看到https://www.example.com/被拦截说明本地资源替换生效。5.3 远程调试 WebView 内容想看 WebView 里的 JS 执行情况可以用 Chrome 或 Edge 的远程调试。地址栏输入edge://inspect/#devices或chrome://inspect找到你的应用 WebView点 inspect 打开控制台。我实测 Edge 更稳Chrome 有时会超时。在控制台里手动粘贴document.body.innerText test能生效说明 JS 本身没问题问题一定出在原生调用侧。6. 本篇常见错误排查6.1 webpage not available最常见原因是网络权限没加对或者权限加到了错误位置。检查AndroidManifest.xml里uses-permission android:nameandroid.permission.INTERNET /是否在application之前。另一个坑是模拟器网络本身没通下拉通知栏重新开关 WiFi 再试。我遇到过一次换了百度也打不开最后是模拟器网络状态卡住重启 WiFi 解决。6.2 Modifier override is not applicable to local function这个报错通常出现在 AI 把override fun onKeyDown写进了局部函数里。override只能用于类成员函数不能用于 Composable 内部的局部函数。解决办法是把按键处理逻辑提到 Activity 的onKeyDown里或者用Modifier.onKeyEvent处理。6.3 webViewInstance 为 null根因是AndroidView的 factory 异步执行Composable 返回时 WebView 还没创建。不要试图在 Composable 外部直接拿实例用回调onWebViewCreated把实例传出来赋值给全局变量。我试过让 AI 加日志排查它一直加日志不解决最后明确提示「AndroidView 异步执行导致返回 null改用回调」才修好。6.4 evaluateJavascript 没效果先确认webViewInstance不为 null再确认 JS 字符串本身没问题。可以在远程调试控制台手动执行同样的 JS 验证。如果 JS 没问题但注入无效检查是不是在loadUrl完成之前就调用了evaluateJavascript可以放在onPageFinished回调里执行。6.5 按键事件仍然被 WebView 捕获除了设置isFocusable false还要确认没有在 WebView 内部重写onKeyDown返回 true。如果 WebView 里加载的页面有 JS 监听键盘事件也可能拦截。可以在shouldInterceptRequest或页面 JS 里加event.preventDefault()的相反逻辑让事件冒泡。7. 接入 AI 辅助编码的推荐路径整个开发过程中Trae 帮我生成了大部分骨架代码但排障环节需要你给出精确的上下文。我的经验是报错信息、日志输出、当前代码片段三样一起贴给 AI比只描述现象有效得多。比如「webViewInstance 是 null」这种问题直接贴日志和AndroidView代码AI 才能定位到异步执行。如果你也想用这套流程建议先把 TaoToken 的 Key 配好Trae 里 Base URL 填https://taotoken.net/apiKey 用控制台创建的。模型对话测试走 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodelutm_campaignrewrite长期编码或 Agent 场景看 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite接入细节查 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。Key 管理统一在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite换项目时只改一处省得每个工具里重复填。最后提醒一句WebView 壳应用上线前记得处理onBackPressed的页面回退逻辑以及WebViewClient里的shouldOverrideUrlLoading否则用户按返回键会直接退出应用而不是回退网页。这两个点我下次单独写一篇。