Augment实战-开发一个终端屏Android H5容器
这个案例比较简单,就是想用webview 封装下H5页面(背景是没有Android开发人员、客户端后面也不方便升级,但要安装在10寸屏上),自己开发也能做,但是自己一个个文件建和修改调试,估计也要一两小时,使用了Augment 也就花了10分钟写提示词,后面让它来写代码,基本上是3分钟一次搞定的,连项目规则都没写。
第一步,提示词
第一步主要目标是想做一个简单的技术方案,由是写了技术方案的提示词,让Gemini帮写了下技术方案,其实我感觉直接这提示词给Augment写代码估计也差不多,如果不行中间让他修改下,也差不多能完成。
提示词无非就背景、任务、要求三部分,不了解提示词怎么写的,可以去学习下提示词工程。
提示词
# 背景
- 现在有一个物联网项目,包括客户端10寸屏幕(安卓系统),和服务端两部分
- 考虑到团队资源不太熟悉安卓开发以及客户端不方便经常升级, 想在10寸屏开发一个webview包装H5页面的应用,业务只要开发H5页面内容就行了,同时因业务需求变化,只要开发H5页面就行了,不需要客户端升级应用
- 10寸屏硬件装的是安卓系统,版本不会很新,也不会太旧,硬件资源不是很丰富,但也不是很差
# 任务
- 基于以上背景,我需要开发一个客户端框架应用,提供给前端团队封装H5页面,请帮我做一个这样的技术方案,方便我让AI开发
- 技术方案包括一个wifi网络信息变更回调 或者 JS主动获取wifi网络信息的jsbridge
# 要求
- 因10寸屏安卓系统系统目前版本未知,建议选择不会太新也不会太旧的相关软件环境;
- 框架应用里面,后续应该还要加入其他少量功能开发,比如开发ASR(语音识别)相关的功能,提供jsBridge给 H5调用/回调
- 采用尽量简单的方案,不要太复杂的实现,但要考虑后续业务变化基本不需要升级客户端
- 安卓开发使用java,不使用kotlin,可以有少量的伪代码,但不要写太多代码,开发代码后面我会让AI开发
第二步 技术方案产出
考虑到Gemini对推理、代码能力比较强,技术方案应该也有优势,就让它给生成一份,用第一步的提示词喂给它,直接写就行了,返回的结果基本上99%是可以了,中间稍有两次对话,修改了一点东西,比如它给出方案有kotlin/java的实现,我就直接让他用java了。
以下是他最终的输出:
好的,这是一个为你量身定制的技术方案,旨在创建一个简单、可扩展的安卓 WebView 框架应用,用于封装 H5 页面,并能与原生功能(如 WiFi 信息、ASR)交互。
## 客户端框架应用技术方案 (安卓 WebView + H5)
### 1. 核心目标 🎯
- **快速迭代**:业务逻辑主要在 H5 端实现,实现快速更新,无需频繁升级客户端。
- **原生能力**:通过 JsBridge,H5 页面可以调用安卓原生功能。
- **扩展性**:框架设计应便于后续添加新的原生功能模块。
- **兼容性与简洁性**:选用稳定且广泛支持的技术,保持实现方案简单。
### 2. 技术选型 ⚙️
- **安卓开发语言**:**Java**
- **最低安卓版本 (minSdkVersion)**:API 21 (Android 5.0 Lollipop)。这个版本开始 WebView 基于 Chromium,性能和标准支持较好,且覆盖绝大多数设备。
- **目标安卓版本 (targetSdkVersion)**:API 30 (Android 11) 或 API 31 (Android 12)。选择一个相对较新的目标版本以符合 Google Play 政策,同时注意处理新版本带来的权限和行为变更。
- **开发工具**:Android Studio
- **JsBridge 实现**:使用安卓 WebView 自带的 `addJavascriptInterface` 方法,这是最简单直接的方式。
### 3. 框架应用结构 🏗️
```
app/
├── java/
│ └── com/inxvision/
│ ├── MainActivity.java // 主活动,承载WebView
│ ├── WebViewConfig.java // WebView的通用配置
│ ├── JsBridge.java // JsBridge接口类,H5调用的Java方法在此定义
│ ├── modules/ // 原生功能模块
│ │ ├── WifiModule.java // WiFi信息处理模块
│ │ └── AsrModule.java // (未来) ASR功能模块
│ └── receivers/
│ └── NetworkChangeReceiver.java // 网络状态变更广播接收器
│
├── res/
│ ├── layout/
│ │ └── activity_main.xml // 主活动布局,包含WebView
│ └── values/
│ └── strings.xml // 字符串资源,可配置H5加载URL
│
└── AndroidManifest.xml // 应用清单文件,声明权限等
```
### 4.核心组件实现要点
#### 4.1. WebView 配置 (`WebViewConfig.java` & `MainActivity.java`)
- **启用 JavaScript**:`webView.getSettings().setJavaScriptEnabled(true);`
- **DOM Storage 支持**:`webView.getSettings().setDomStorageEnabled(true);`
- **缓存策略**:考虑到 H5 经常更新,可以设置为 `webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);` 或 `LOAD_DEFAULT` 配合 H5 自身的缓存控制。
- **加载 H5 页面**:`webView.loadUrl("YOUR_H5_APP_URL");` URL 可以配置在 `strings.xml` 中。
- **处理内部跳转**:设置 `WebViewClient`,确保 H5 页面内部链接仍在 WebView 中打开。
- **JS 弹窗等支持**:设置 `WebChromeClient`。
**伪代码 (`MainActivity.java` onCreate):**
Java
```
// WebView webView = findViewById(R.id.my_webview);
// WebSettings webSettings = webView.getSettings();
// webSettings.setJavaScriptEnabled(true);
// webSettings.setDomStorageEnabled(true);
// webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); // 或根据需求调整
// // 设置WebViewClient以在应用内打开链接
// webView.setWebViewClient(new WebViewClient() {
// @Override
// public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// view.loadUrl(request.getUrl().toString());
// return true; // 表示我们处理了这个URL加载
// }
// });
// // JsBridge 注入
// JsBridge jsBridge = new JsBridge(this, webView);
// webView.addJavascriptInterface(jsBridge, "AndroidNative"); // "AndroidNative" 是H5调用时的对象名
// // 加载H5页面
// webView.loadUrl(getString(R.string.h5_url));
```
#### 4.2. JsBridge 实现 (`JsBridge.java`)
此类将包含所有暴露给 JavaScript 调用的 Java 方法。
**伪代码 (`JsBridge.java`):**
Java
```
// public class JsBridge {
// private Context context;
// private WebView webView;
// private WifiModule wifiModule;
// // private AsrModule asrModule; // 未来
// public JsBridge(Context context, WebView webView) {
// this.context = context;
// this.webView = webView;
// this.wifiModule = new WifiModule(context, webView);
// // this.asrModule = new AsrModule(context, webView); // 未来
// }
// // ---------------- WiFi 相关 -----------------
// @JavascriptInterface
// public void getWifiInfo(String jsCallbackFunction) {
// wifiModule.getCurrentWifiInfo(jsCallbackFunction);
// }
// // H5 主动注册 WiFi 变更监听的回调函数名
// @JavascriptInterface
// public void registerWifiChangeCallback(String jsCallbackFunction) {
// wifiModule.setJsWifiChangeCallback(jsCallbackFunction);
// }
// // ---------------- ASR 相关 (未来) -----------------
// // @JavascriptInterface
// // public void startAsr(String jsCallbackFunction) {
// // asrModule.startRecognition(jsCallbackFunction);
// // }
// // @JavascriptInterface
// // public void stopAsr() {
// // asrModule.stopRecognition();
// // }
// // 通用:Java 主动调用 JavaScript 方法
// public void callJsMethod(String jsMethodName, String jsonData) {
// if (webView != null) {
// webView.post(() -> { // 确保在UI线程执行
// String script = String.format("javascript:%s(%s)", jsMethodName, jsonData == null ? "" : "'" + jsonData + "'");
// webView.evaluateJavascript(script, null);
// });
// }
// }
// }
```
#### 4.3. WiFi 网络信息 (`WifiModule.java` & `NetworkChangeReceiver.java`)
**目标**:
1. JS 主动获取当前 WiFi 信息。
2. WiFi 网络变更时,主动回调通知 H5。
**`WifiModule.java` 伪代码:**
Java
```
// public class WifiModule {
// private Context context;
// private WebView webView; // 用于回调JS
// private String jsWifiChangeCallback; // H5注册的回调函数名
// public WifiModule(Context context, WebView webView) {
// this.context = context;
// this.webView = webView;
// }
// public void setJsWifiChangeCallback(String callbackName) {
// this.jsWifiChangeCallback = callbackName;
// }
// // JS 主动获取
// public void getCurrentWifiInfo(String jsCallbackFunction) {
// WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
// String resultJson;
// if (wifiManager != null && wifiManager.isWifiEnabled()) {
// WifiInfo wifiInfo = wifiManager.getConnectionInfo();
// if (wifiInfo != null && wifiInfo.getSSID() != null) {
// // 构建包含SSID, BSSID, IP地址等信息的JSON字符串
// // e.g., {"ssid": "MyWifi", "bssid": "xx:xx:xx:xx:xx:xx", "ipAddress": "192.168.1.100"}
// resultJson = "{\"ssid\": \"" + wifiInfo.getSSID().replace("\"", "") + "\", \"ipAddress\": \"" + formatIpAddress(wifiInfo.getIpAddress()) + "\"}";
// } else {
// resultJson = "{\"error\": \"Not connected to WiFi\"}";
// }
// } else {
// resultJson = "{\"error\": \"WiFi not enabled or no permission\"}";
// }
// callJsMethod(jsCallbackFunction, resultJson);
// }
// // 由 NetworkChangeReceiver 调用,当网络变化时
// public void onNetworkChanged() {
// if (jsWifiChangeCallback != null && !jsWifiChangeCallback.isEmpty()) {
// getCurrentWifiInfo(jsWifiChangeCallback); // 主动将最新信息推给JS
// }
// }
// private void callJsMethod(String jsFunctionName, String data) {
// if (webView != null && jsFunctionName != null && !jsFunctionName.isEmpty()) {
// webView.post(() -> {
// String script = String.format("javascript:typeof %s === 'function' && %s(%s)", jsFunctionName, jsFunctionName, "'" + data + "'");
// webView.evaluateJavascript(script, null);
// });
// }
// }
// private String formatIpAddress(int ipAddress) {
// // 辅助函数:将int格式的IP转为字符串
// return (ipAddress & 0xFF) + "." +
// ((ipAddress >> 8) & 0xFF) + "." +
// ((ipAddress >> 16) & 0xFF) + "." +
// (ipAddress >> 24 & 0xFF);
// }
// }
```
**`NetworkChangeReceiver.java` 伪代码:**
Java
```
// public class NetworkChangeReceiver extends BroadcastReceiver {
// private WifiModule wifiModule; // 通过某种方式注入或获取 WifiModule 实例
// // 构造函数或其他方式传递 WifiModule 实例
// public NetworkChangeReceiver(WifiModule wifiModule) {
// this.wifiModule = wifiModule;
// }
// // 空构造函数,用于 Manifest 注册,之后再关联 WifiModule
// public NetworkChangeReceiver() {}
// public void setWifiModule(WifiModule wifiModule) { this.wifiModule = wifiModule; }
// @Override
// public void onReceive(Context context, Intent intent) {
// if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()) ||
// WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) {
// if (wifiModule != null) {
// wifiModule.onNetworkChanged();
// }
// }
// }
// }
```
**在 `MainActivity.java` 中注册和取消注册 `NetworkChangeReceiver`:**
Java
```
// private NetworkChangeReceiver networkChangeReceiver;
// private WifiModule wifiModuleInstance; // 从JsBridge或直接创建
// @Override
// protected void onCreate(Bundle savedInstanceState) {
// // ... 其他初始化 ...
// // jsBridge = new JsBridge(this, webView);
// // wifiModuleInstance = jsBridge.getWifiModule(); // 假设JsBridge提供getter
// // networkChangeReceiver = new NetworkChangeReceiver(wifiModuleInstance);
// // 或者
// // wifiModuleInstance = new WifiModule(this, webView);
// // networkChangeReceiver = new NetworkChangeReceiver();
// // networkChangeReceiver.setWifiModule(wifiModuleInstance); // 关联实例
// // webView.addJavascriptInterface(new JsBridge(this, webView, wifiModuleInstance /*, asrModule*/), "AndroidNative");
// // ...
// // registerReceiver();
// }
// private void registerNetworkReceiver() {
// networkChangeReceiver = new NetworkChangeReceiver();
// // 假设 JsBridge 实例为 jsBridge, 并且它持有了 WifiModule 实例
// // 你需要确保 NetworkChangeReceiver 能访问到 WifiModule 的 onNetworkChanged 方法
// // 一种方式是让 JsBridge (或 WifiModule) 提供一个静态方法或单例来获取回调接口/实例
// // 这里简化处理,假设 JsBridge 实例可被访问,并且它内部有 wifiModule
// // JsBridge jsBridge = ((YourApplicationClass) getApplication()).getJsBridge(); // 通过Application共享
// // if (jsBridge != null && jsBridge.getWifiModule() != null) {
// // networkChangeReceiver.setWifiModule(jsBridge.getWifiModule());
// // }
// // 或者,如果 JsBridge 在 Activity 中创建,直接传递
// networkChangeReceiver.setWifiModule(this.jsBridge.getWifiModuleInstance()); // 假设 JsBridge 有此方法
// IntentFilter filter = new IntentFilter();
// filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
// filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
// registerReceiver(networkChangeReceiver, filter);
// }
// @Override
// protected void onResume() {
// super.onResume();
// // 确保 JsBridge 和 WifiModule 已初始化
// if (jsBridge != null && jsBridge.getWifiModuleInstance() != null && networkChangeReceiver == null) {
// networkChangeReceiver = new NetworkChangeReceiver(jsBridge.getWifiModuleInstance());
// IntentFilter filter = new IntentFilter();
// filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
// filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
// registerReceiver(networkChangeReceiver, filter);
// }
// }
// @Override
// protected void onPause() {
// super.onPause();
// if (networkChangeReceiver != null) {
// unregisterReceiver(networkChangeReceiver);
// networkChangeReceiver = null; // 防止内存泄漏
// }
// }
```
*Note: `NetworkChangeReceiver` 与 `WifiModule` 的交互需要妥善处理。可以在 `MainActivity` 中创建 `WifiModule` 实例,并将其传递给 `JsBridge` 和 `NetworkChangeReceiver`。*
#### 4.4. ASR (语音识别) 功能 (未来 - `AsrModule.java`)
- **接口**:提供 `startRecognition(jsCallbackFunction)` 和 `stopRecognition()`。
- **实现**:使用安卓内置的 `SpeechRecognizer` API。
- **回调**:识别结果(成功文本、错误信息)通过 `jsCallbackFunction` 回调给 H5。
- **权限**:需要在 `AndroidManifest.xml` 中声明 `RECORD_AUDIO` 权限,并进行运行时权限请求。
**伪代码 (`AsrModule.java`):**
Java
```
// public class AsrModule {
// private Context context;
// private WebView webView;
// private SpeechRecognizer speechRecognizer;
// private String currentAsrCallback;
// public AsrModule(Context context, WebView webView) {
// this.context = context;
// this.webView = webView;
// // 初始化 SpeechRecognizer
// }
// public void startRecognition(String jsCallbackFunction) {
// this.currentAsrCallback = jsCallbackFunction;
// // 检查权限
// // 配置 Intent, 设置 RecognitionListener
// // speechRecognizer.startListening(intent);
// // RecognitionListener 的 onResults, onError 等方法中调用 callJsMethod 通知H5
// // e.g., callJsMethod(currentAsrCallback, "{\"text\": \"识别结果\"}");
// // e.g., callJsMethod(currentAsrCallback, "{\"error\": \"识别错误码\"}");
// }
// public void stopRecognition() {
// // if (speechRecognizer != null) speechRecognizer.stopListening();
// }
// private void callJsMethod(String jsFunctionName, String data) {
// // 与 WifiModule 中的 callJsMethod 类似
// }
// }
```
### 5. AndroidManifest.xml 配置 📜
XML
```
<?xml version="1.0" encoding="utf-f"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yourpackage">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"> <activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
```
*动态注册 `BroadcastReceiver` (如 `NetworkChangeReceiver`) 通常更灵活,因为它允许你在 Activity 的生命周期内控制其行为,并且可以方便地传递 Activity/WebView 相关的引用。*
### 6. H5 端交互示例 (JavaScript) 💻
HTML
```
<!DOCTYPE html>
<html>
<head>
<title>H5 App</title>
<script type="text/javascript">
// --- WiFi 信息 ---
function handleWifiInfo(jsonString) { console.log("WiFi Info received:", jsonString); const wifiData = JSON.parse(jsonString); document.getElementById('wifiStatus').innerText = wifiData.ssid ? `Connected to: ${wifiData.ssid}` : (wifiData.error || "WiFi status unknown"); } function requestWifiInfo() { if (window.AndroidNative && typeof window.AndroidNative.getWifiInfo === 'function') { // "handleWifiInfo" 是在JS中定义的全局函数名,Java端会回调这个函数
window.AndroidNative.getWifiInfo("handleWifiInfo"); } else { console.log("AndroidNative bridge not available for getWifiInfo."); document.getElementById('wifiStatus').innerText = "Native bridge not available."; } } // 用于WiFi变更自动回调
function onWifiNetworkChanged(jsonString) { console.log("WiFi Info CHANGED:", jsonString); const wifiData = JSON.parse(jsonString); document.getElementById('wifiStatus').innerText = `WiFi Updated: ${wifiData.ssid || wifiData.error || "WiFi status unknown"}`; // 可以在这里做更多UI更新
} function registerForWifiChanges() { if (window.AndroidNative && typeof window.AndroidNative.registerWifiChangeCallback === 'function') { // "onWifiNetworkChanged" 是JS中定义的全局函数名,Java端网络变化时会回调它
window.AndroidNative.registerWifiChangeCallback("onWifiNetworkChanged"); console.log("Registered for WiFi changes."); } else { console.log("AndroidNative bridge not available for registerWifiChangeCallback."); } } // --- ASR (未来) ---
function handleAsrResult(jsonString) { console.log("ASR Result:", jsonString); const asrData = JSON.parse(jsonString); if (asrData.text) { document.getElementById('asrResult').innerText = `Recognized: ${asrData.text}`; } else if (asrData.error) { document.getElementById('asrResult').innerText = `ASR Error: ${asrData.error}`; } } function startListening() { if (window.AndroidNative && typeof window.AndroidNative.startAsr === 'function') { // "handleAsrResult" 是JS中定义的全局函数名
window.AndroidNative.startAsr("handleAsrResult"); document.getElementById('asrResult').innerText = "Listening..."; } else { console.log("AndroidNative bridge not available for startAsr."); document.getElementById('asrResult').innerText = "ASR bridge not available."; } } // 页面加载完成后自动注册WiFi变更监听
window.onload = function() { registerForWifiChanges(); requestWifiInfo(); // 页面加载时获取一次当前状态
}; </script>
</head>
<body>
<h1>My H5 IoT App</h1>
<div>
<h2>WiFi Status</h2>
<p id="wifiStatus">Loading WiFi status...</p>
<button onclick="requestWifiInfo()">Refresh WiFi Info</button>
</div>
</body>
</html>
```
### 7. 后续扩展考虑 🧩
- **模块化**:每个原生功能(如 WiFi, ASR, 蓝牙等)封装在独立的 `Module` 类中,`JsBridge` 类负责路由和调用。
- **异步处理**:原生耗时操作应在子线程执行,结果通过 `webView.post()` 或 `runOnUiThread()` 回到主线程,再调用 `evaluateJavascript` 通知 H5。
- **错误处理与日志**:在 JsBridge 和原生模块中加入充分的错误处理和日志记录。
- **安全性**:警惕 `addJavascriptInterface` 可能带来的安全风险 (尤其在加载不受信任的 H5 内容时)。确保 H5 内容来源可靠。对于 API 17 以下,`@JavascriptInterface` 注解不是必需的,所有 public 方法都会暴露,需特别注意。由于你选用的 `minSdkVersion` 是 21,此问题已缓解。
- **H5 URL 配置**:提供本地配置 H5 入口 URL 的功能,通过长按屏幕右下角区域 3 秒,弹出密码验证对话框,验证通过后显示 URL 配置界面,可以修改 H5 页面地址或重置为默认地址。配置信息保存在 SharedPreferences 中,应用启动时自动读取最新配置的 URL。这种方式既方便现场调试和环境切换,又不会被普通用户误操作。
这个方案提供了一个基础但可扩展的框架。AI 可以基于此方案的伪代码和结构来生成具体的 Java 和 XML 代码。
第三步 让Augment写代码
很简单,新建一个工程文件夹,把上面的写技术方案提示词和技术方案 分别存到工程根目录下为文件,“需求说明.md”、“技术方案.md”。
打开IDEA,然后打开Augment对话框,开到Agent Auto模式。
直接一句话,附上两个文件,它就可以做事了:
@需求说明.md @技术方案.md 这是一个新的项目,请按项目说胆和技术方案文档,帮我完成开发
然后打开Android Studio软件,配置好环境,连上手机,打包验证,打包出了错,提示两个图标找不到,这个是Augment写完代码唯一有问题的地方,丢给Augment,它理解问题,马上就开始画图了,包括各尺寸的图标,画出来的png结果是打不开的,网上也看别人介绍说Augment画不了图,我直接让Gemini画了个标图放进去,打包运行,一切OK,后面还让它增加了项功能,就是H5地址不写死,可以随时设置,这样一次打包就不分环境可用了,