← 返回文章列表

把 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 即可,密钥文件务必备份好——丢了它,老用户只能卸载重装。