把 Next.js PWA 装进 Android WebView:一个零第三方依赖的纯 Java 壳工程
Web 应用做成 PWA 之后「添加到主屏幕」已经很好用了,但对老人孩子来说,
一个正经的桌面图标、点开全屏无地址栏的「App 感」依然是刚需。市面上的方案多是基于
Capacitor / Cordova 套壳,可我只想加载一个网址,不想为一个 WebView 引入一整个工程生态。
于是手写了一个纯 Java、零第三方依赖的壳工程,总共一个 MainActivity。
核心配置
WebView 要「像个 App」,第一件事是把该开的开关全开对:
WebSettings s = webView.getSettings();
s.setJavaScriptEnabled(true);
s.setDomStorageEnabled(true); // PWA 的 localStorage 全靠它
s.setMediaPlaybackRequiresUserGesture(false); // 语音报词要能自动出声
s.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
webView.setWebChromeClient(new WebChromeClient() { ... });
webView.setWebViewClient(new WebViewClient() { ... });
setMediaPlaybackRequiresUserGesture(false) 这条容易被漏:PWA 里
「自动报听写」的语音合成如果受手势限制,孩子每写一个词都得先点一下屏幕。
返回键:交给 WebView 历史,而不是退出 App
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed(); // 只在无历史时才真正退出
}
}
看似基础,但少了它用户体验会非常割裂:页面内跳转了几层之后一按返回直接闪退到桌面。 WebView 内部的路由历史和系统的返回手势,要在这一行里合流。
文件上传:触发系统选择器
PWA 里的 <input type="file"> 在 WebView 默认没反应,需要
WebChromeClient.onShowFileChooser 接管,转发给系统的文件选择 Intent:
@Override
public boolean onShowFileChooser(WebView view,
ValueCallback<Uri[]> callback, FileChooserParams params) {
// 记住 callback,startActivityForResult 拿到结果后回传
pendingCallback = callback;
startActivityForResult(params.createIntent(), REQ_FILE);
return true;
}
拍照上传作业单、上传批改照片,走的全是这条路。注意
onActivityResult 里无论成功失败都要给 callback 一个结果,
否则 WebView 里的文件选择器会「卡死」,第二次点开没反应。
下载拦截
DownloadListener 默认什么也不做。Web 应用一般不需要下载,
但至少要 Toast 提示一下,别让用户的点击无声无息地消失。
构建的小坑:JDK 版本
构建时必须显式指定 JDK 21:
./gradlew assembleRelease \
-Dorg.gradle.java.home="C:/Program Files/Java/jdk-21"
因为全局 ~/.gradle/gradle.properties 里固定了旧的 JDK 8,
而新版 Android Gradle Plugin 要求 17+。这种「全局配置压过项目」的问题,
命令行参数是唯一稳定的覆盖方式。
总结
- 只加载一个网址的壳,不需要 Capacitor:一个 Activity + 三个 Client 回调足够;
- 必做清单:
DomStorage、免手势播放、返回键接管、文件选择回调、下载提示; - 发布自用 APK 用自签名 keystore 即可,密钥文件务必备份好——丢了它,老用户只能卸载重装。