diff --git a/README.md b/README.md index f240f46..7e1021e 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,18 @@ ## ✨ 特性 - **🎯 智能平台适配**:Android 直下直装,iOS 跳转 App Store -- **🔄 两种更新体验**:非强制(可后台下载)与强制更新(阻塞式对话框) +- **🔄 灵活的更新策略**: + - **一键检查**:自动拉取、对比版本、弹窗提示 + - **静默检查**:后台获取更新信息,适合冷启动或用户主动点击前的预检查 +- **🎨 现代化 UI**: + - Material 风格对话框 + - **📝 富文本支持**:更新日志支持粗体、斜体、代码块、高亮等格式 + - 进度与状态可视化 - **🛡️ 权限适配完善**:针对不同 Android 版本的存储、安装、通知权限自动处理 -- **📱 现代化 UI**:Material 风格对话框,进度与状态可视化 - **🌐 网络可配置**:证书校验、超时、默认方法、Headers 等 - **🔧 安装策略灵活**:系统流程/预检查权限/智能策略可选 - **🏪 应用市场支持**:支持多应用市场白名单,智能检测设备已安装的市场 -- **📦 三种更新方式**:应用市场、浏览器下载、应用内下载 +- **📦 多种更新方式**:应用市场、浏览器下载、应用内下载 ## 📦 安装 @@ -58,9 +63,11 @@ void checkUpdate(BuildContext context) { appStoreUrl: data['appStoreUrl'], apkSize: data['apkSize'], apkMd5: data['apkMd5'], + // 应用市场配置 appMarkets: (data['appMarkets'] as List?) ?.map((e) => AppMarket.fromString(e)) .toList(), + // 支持的更新方式 supportedMethods: [ AppUpgradeMethod.market, AppUpgradeMethod.browser, @@ -74,27 +81,28 @@ void checkUpdate(BuildContext context) { ### 方式二:静默检查 + 由用户决定 -先静默检查是否有更新,然后由用户决定是否显示升级对话框: +此方式适用于: +1. **App 启动时**:在后台静默检查是否有更新,如果有则在合适的时机(如用户点击"版本更新"按钮时)展示,避免打断用户操作。 +2. **红点提示**:检查到有更新时仅显示红点,用户点击后才弹出对话框。 ```dart -// 注意:当前版本不提供独立的静默检查方法 -// 如果需要静默检查,请直接调用您的 API,然后手动判断是否需要显示对话框 +// 1. 静默检查更新(不显示任何 UI) +final upgradeInfo = await AppUpgradeSimple.instance.silentCheckUpdate( + future: () async { + // 调用您的 API + return AppUpgradeVersion(...); + }, +); -final response = await http.get('https://your-api.com/check-update'); -final data = json.decode(response.body); - -// 手动比较版本 -final serverVersion = data['versionBuildNumber']; -final currentVersion = await AppUpgradeSimple.instance.getAppInfo(); -final currentBuildNumber = int.parse(currentVersion['buildNumber'] ?? '0'); - -if (serverVersion > currentBuildNumber) { - // 有新版本,显示升级对话框 - await AppUpgradeSimple.instance.checkUpdate( +// 2. 根据结果处理 +if (upgradeInfo != null && upgradeInfo.hasUpdate) { + // 有新版本,可以显示红点或在用户点击时调用弹窗 + print('发现新版本: ${upgradeInfo.versionName}'); + + // 在需要展示弹窗的时机(如用户点击按钮): + AppUpgradeSimple.instance.showPreparedUpgrade( context: context, - future: () async { - return AppUpgradeVersion.fromJson(data); - }, + info: upgradeInfo, // 传入刚才获取的 info ); } ``` @@ -116,16 +124,32 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig( )); ``` -## 🎨 UI 能力 +## 🎨 UI 能力与富文本 -- **发现新版本对话框**(强制/非强制) +### 富文本更新日志 + +更新内容 (`updateContent`) 支持简单的 Markdown 风格富文本,让更新日志更清晰: + +- **粗体**:`**重要内容**` +- **斜体**:`__斜体内容__` +- **代码块**:`` `version 2.0` `` +- **高亮**:`[特别注意]` + +示例: +```text +1. 新增 **深色模式** 支持 +2. 修复 `Login` 页面崩溃问题 +3. [推荐] 性能大幅优化 +``` + +### 对话框特性 + +- **发现新版本**(强制/非强制) - 强制更新:不可关闭,必须更新 - 非强制更新:可稍后更新,支持后台下载 - **版本信息卡片**:显示当前版本、新版本、APK 大小 - **下载进度展示**:实时显示下载进度百分比和状态 - **安装状态检测**:自动检测安装结果,支持重试 -- **更新内容展示**:支持 Markdown 格式(粗体、斜体、代码块等) -- **Android 更新方式选择**:应用市场、浏览器下载、应用内下载 ## ⚙️ Android 配置 @@ -192,34 +216,16 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig( ``` -### 3) Gradle(可选) - -```kotlin -android { - compileSdk 34 - - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - isCoreLibraryDesugaringEnabled = true - } -} - -dependencies { - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") -} -``` - ## 📡 服务端返回协议 -服务端需要返回包含以下字段的 JSON(关键字段必须): +服务端需要返回包含以下字段的 JSON: ```json { "isForceUpdate": false, "versionBuildNumber": 101, "versionName": "1.0.1", - "updateContent": "1. 修复登录\n2. 优化UI\n3. 提升性能", + "updateContent": "1. 新增 **深色模式**\n2. 修复 `Bug`", "downloadUrl": "https://your-cdn.com/app-release.apk", "appStoreUrl": "https://apps.apple.com/app/id123456789", "apkSize": 25165824, @@ -229,91 +235,29 @@ dependencies { } ``` -**字段说明:** +### 关键字段说明 -- `versionBuildNumber`(必填):版本号(整数),用于版本比较 -- `versionName`(必填):版本名称(字符串),如 "1.0.1" -- `isForceUpdate`(可选):是否强制更新,默认 `false` -- `updateContent`(可选):更新内容说明,支持换行和 Markdown 格式 -- `downloadUrl`(Android 必填):APK 下载地址 -- `appStoreUrl`(iOS 必填):App Store 链接 -- `apkSize`(可选):APK 文件大小(字节) -- `apkMd5`(可选):APK 文件的 MD5 值,用于校验 -- `appMarkets`(可选):应用市场白名单,见下方说明 -- `supportedMethods`(可选):支持的更新方式,默认全部支持 - -**版本比较逻辑:** - -插件会优先比较 `versionBuildNumber`,如果相同则比较 `versionName`。如果服务端版本大于当前版本,则判定为有新版本。 - -### 应用市场白名单 (appMarkets) - -`appMarkets` 用于限制支持的应用市场(Android),作为白名单使用。配置后,插件会: - -1. **检测设备已安装的应用市场** -2. **匹配白名单**:只允许跳转到白名单中且设备已安装的应用市场 -3. **智能处理**: - - 如果设备上没有白名单中的任何应用市场,提示用户"不支持当前设备的应用市场",引导用户选择其他更新方式(如浏览器下载、应用内下载) - - 如果设备有白名单中的应用市场,直接跳转到设备默认应用市场 - -**配置示例:** - -```dart -// Dart 代码 -appMarkets: [AppMarket.huawei, AppMarket.xiaomi, AppMarket.oppo] - -// 服务端返回 -"appMarkets": ["huawei", "xiaomi", "oppo"] -``` - -**支持的应用市场类型:** - -| 值 | 显示名称 | 对应包名 | -|---|---------|---------| -| `googlePlay` | Google Play | `com.android.vending` | -| `huawei` | 华为应用市场 | `com.huawei.appmarket` | -| `xiaomi` | 小米应用商店 | `com.xiaomi.market` | -| `oppo` | OPPO软件商店 | `com.oppo.market` | -| `vivo` | vivo应用商店 | `com.bbk.appstore` | -| `tencent` | 腾讯应用宝 | `com.tencent.android.qqdownloader` | -| `coolapk` | 酷安 | `com.coolapk.market` | - -**不配置 appMarkets 的行为:** - -如果不配置 `appMarkets`,插件会使用默认行为:跳转到设备的默认应用市场(使用 `market://` 协议)。 - -### 支持的更新方式 (supportedMethods) - -`supportedMethods` 用于指定支持的更新方式,可选值: - -- `market`:应用市场更新 -- `browser`:浏览器下载 -- `inApp`:应用内下载 - -如果不配置,默认支持所有方式。如果只配置一种方式,将直接使用该方式,不会显示选择对话框。 +- **versionBuildNumber**(必填):版本号(整数),用于比较。如果大于当前版本号,则提示更新。 +- **versionName**(必填):版本名称,如 "1.0.1"。如果 BuildNumber 相同但 VersionName 更大(字典序),也会提示更新。 +- **appMarkets**(可选):Android 应用市场白名单。配置后,优先尝试跳转已安装且在白名单中的市场。 +- **supportedMethods**(可选):指定支持的更新方式(market/browser/inApp)。 ## 🔧 进阶能力 ### 1) 网络配置 +支持开发/生产环境切换,以及自定义 HTTP 配置(如 Headers、超时): + ```dart // 自动选择(Debug 绕过证书、Release 严格校验) AppUpgradePlugin().configureHttp(HttpConfig.auto); -// 开发环境配置(绕过证书验证) -AppUpgradePlugin().configureHttp(HttpConfig.development); - -// 生产环境配置(严格证书验证) -AppUpgradePlugin().configureHttp(HttpConfig.production); - -// 或手动配置: +// 手动配置: AppUpgradePlugin().configureHttp(const HttpConfig( ignoreCertificate: false, enableLog: true, connectTimeout: 30, - receiveTimeout: 60, - defaultMethod: 'GET', - headers: {'User-Agent': 'MyApp/1.0'}, + headers: {'Authorization': 'Bearer xxx'}, )); ``` @@ -327,43 +271,9 @@ final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(conte // 检查并请求安装权限 final hasInstall = await PermissionHelper.checkAndRequestInstallPermission(context: context); - -// 检查并请求通知权限 -final hasNotification = await PermissionHelper.checkAndRequestNotificationPermission(context: context); - -// 精确跳转安装权限设置 -await AppUpgradePlugin().openInstallPermissionSettings(); ``` -**权限处理说明:** - -- **存储权限**:Android 13+ 无需权限(使用应用私有目录),Android 10-12 会尝试请求但失败时使用私有目录,Android 9 及以下需要权限 -- **安装权限**:默认情况下(`requireInstallPermission: false`),插件会直接调用系统安装流程,由系统处理权限检查。如果设置为 `true`,会在安装前检查权限 -- **通知权限**:Android 13+ 需要,用于显示下载进度通知 - -### 3) 安装策略(Android) - -```dart -// 系统流程(推荐):系统处理权限并弹出安装确认 -await AppUpgradePlugin().installApkWithSystemFlow(apkPath); - -// 传统安装:需要事先自行处理安装权限 -await AppUpgradePlugin().installApk(apkPath); - -// 按配置策略安装 -await AppUpgradePlugin().installApkWithConfig( - apkPath, - config: InstallConfig.smart, -); -``` - -**策略对比:** - -- `systemFlow`:系统处理权限检查与确认,体验最佳(推荐) -- `preCheckPermission`:先检查权限,无权时返回错误,便于精细控制 -- `smart`:有权用预检查,无权走系统流程 - -### 4) 预下载 APK +### 3) 预下载 APK ```dart // 后台预下载 APK(不显示 UI) @@ -373,89 +283,8 @@ final apkPath = await AppUpgradeSimple.instance.preDownloadApk( print('下载进度: ${progress.percentage}%'); }, ); - -if (apkPath != null) { - print('下载完成: $apkPath'); -} ``` -### 5) 查找已下载的 APK - -```dart -// 查找指定版本的已下载 APK -final apkPath = await AppUpgradeSimple.instance.findDownloadedApk('1.0.1'); -if (apkPath != null) { - // 直接安装 - await AppUpgradePlugin().installApkWithSystemFlow(apkPath); -} -``` - -### 6) 清理下载缓存 - -```dart -// 清理所有已下载的 APK 文件 -await AppUpgradeSimple.instance.clearDownloadCache(); -``` - -## 🐛 常见问题 - -- **安装失败显示"解析包时出现问题"**:检查 APK 完整性、签名与架构匹配 -- **权限申请失败**:确认 Manifest 权限、FileProvider 配置、在 MaterialApp 环境调用 -- **下载失败/进度不更新**:检查网络、下载 URL 可用性、服务端是否支持断点续传 -- **iOS 不跳转**:确认 `appStoreUrl` 为有效的 App Store 链接 -- **"前往浏览器下载"无反应**:确认已在 AndroidManifest.xml 中添加 `` 声明(Android 11+ 必需) -- **FileProvider 配置错误**:确认 `file_paths.xml` 中已添加 `` 配置,用于权限被拒绝时的备用存储路径 -- **Android 9 下载权限错误**:插件会自动检测权限,无权限时使用应用私有目录,无需额外配置 -- **安装检测超时**:默认超时时间为 45 秒,可通过 `UpgradeConfig.installTimeout` 调整 - -## 📚 主要 API 清单 - -### AppUpgradeSimple(推荐使用) - -- `configure(UpgradeConfig)`:配置升级参数 -- `checkUpdate({context, future, showNoUpdateToast, autoInstall, onComplete, config})`:检查更新并显示 UI -- `preDownloadApk({url, onProgress})`:预下载 APK(Android) -- `findDownloadedApk(version)`:查找已下载的 APK(Android) -- `getAppInfo()`:获取当前应用信息 -- `clearDownloadCache()`:清理下载缓存 -- `checkNetworkStatus()`:检查网络连接状态 - -### AppUpgradePlugin(底层能力) - -- `configureHttp(HttpConfig)`:网络层配置 -- `checkUpdate(url, {params})`:检查更新(返回 UpgradeInfo) -- `downloadApk(url, {onProgress, savePath})`:下载 APK(Android) -- `installApk(filePath)`:安装 APK(需要先处理权限) -- `installApkWithSystemFlow(filePath)`:使用系统流程安装(推荐) -- `installApkWithConfig(filePath, {config})`:按配置策略安装 -- `openInstallPermissionSettings()`:跳转安装权限设置(Android) -- `getDeviceInfo()`、`getAndroidSdkVersion()`:获取设备信息(Android) -- `goToAppStore(url, {context})`:跳转到应用商店 -- `getInstalledMarkets()`:获取设备已安装的应用市场列表(Android) -- `getDownloadPath()`:获取下载目录路径 -- `checkApkExists(version, md5)`:检查指定版本的 APK 是否已下载 - -### PermissionHelper(Android) - -- `checkAndRequestStoragePermission(context)`:检查并请求存储权限 -- `checkAndRequestInstallPermission(context)`:检查并请求安装权限 -- `checkAndRequestNotificationPermission(context)`:检查并请求通知权限 -- `checkInstallPermission()`:检查安装权限状态 - -### 配置类 - -- `UpgradeConfig`:升级配置(超时、自动安装、日志等) -- `HttpConfig`:HTTP 配置(证书、超时、Headers 等) -- `InstallConfig`:安装策略配置 - -### 模型类 - -- `AppUpgradeVersion`:服务端返回的版本信息模型 -- `UpgradeInfo`:内部使用的升级信息模型 -- `AppMarket`:应用市场枚举 -- `AppUpgradeMethod`:更新方式枚举 -- `DownloadProgress`:下载进度信息 - ## 🤝 贡献 欢迎提交 Issue 与 Pull Request! @@ -463,9 +292,3 @@ await AppUpgradeSimple.instance.clearDownloadCache(); ## 📄 许可证 MIT License - -## 🔗 参考 - -- [Flutter 官网](https://flutter.dev) -- [Android 安装权限文档](https://developer.android.com/reference/android/Manifest.permission#REQUEST_INSTALL_PACKAGES) -- [FileProvider 使用指南](https://developer.android.com/reference/androidx/core/content/FileProvider) diff --git a/lib/app_upgrade_simple.dart b/lib/app_upgrade_simple.dart index 803e911..ee0cf4a 100644 --- a/lib/app_upgrade_simple.dart +++ b/lib/app_upgrade_simple.dart @@ -236,71 +236,12 @@ class AppUpgradeSimple { try { assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); - // 1. 获取服务器版本信息 - final serverInfo = await future(); - if (serverInfo == null) { - // 获取失败或无数据,视作无更新(已是最新) - if (effectiveConfig.enableDebugLog) { - debugPrint('🔍 检查更新结果: 未返回版本信息'); - } + final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig); + if (info == null) { onComplete?.call(true); return; } - if (effectiveConfig.enableDebugLog) { - debugPrint('🔍 获取到服务器版本: $serverInfo'); - } - - // 2. 获取当前App信息 - final appInfo = await _plugin.getAppInfo(); - final currentVersionName = appInfo['version'] ?? ''; - final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; - - if (effectiveConfig.enableDebugLog) { - debugPrint('📱 当前版本: $currentVersionName (Build: $currentBuildNumber)'); - } - - // 3. 比较版本并构建 UpgradeInfo - bool hasUpdate = false; - if (serverInfo.versionBuildNumber > 0) { - // 优先比较 buildNumber - if (serverInfo.versionBuildNumber > currentBuildNumber) { - hasUpdate = true; - } else if (serverInfo.versionBuildNumber == currentBuildNumber) { - // buildNumber 相同,比较版本名 - if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) { - hasUpdate = true; - } - } - } else { - // 只比较版本名 - if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) { - hasUpdate = true; - } - } - - if (effectiveConfig.enableDebugLog) { - debugPrint('📊 版本比较结果: ${hasUpdate ? "有新版本" : "已是最新"}'); - } - - // 构建内部使用的 UpgradeInfo - final info = UpgradeInfo( - hasUpdate: hasUpdate, - isForceUpdate: serverInfo.isForce, - versionName: serverInfo.versionName, - versionBuildNumber: serverInfo.versionBuildNumber, - currentVersionName: currentVersionName, - currentBuildNumber: currentBuildNumber, - updateContent: serverInfo.updateContent, - downloadUrl: serverInfo.downloadUrl, - appStoreUrl: serverInfo.appStoreUrl, - apkSize: serverInfo.apkSize, - apkMd5: serverInfo.apkMd5, - appMarkets: serverInfo.appMarkets, - supportedMethods: serverInfo.supportedMethods ?? - const [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp], - ); - if (!info.hasUpdate) { if (finalShowNoUpdateToast && context.mounted) { _showToast('已是最新版本', context, effectiveConfig); @@ -326,6 +267,142 @@ class AppUpgradeSimple { } } + /// 静默检查更新(不弹出任何 UI) + /// + /// 返回 [UpgradeInfo],其中包含服务端版本信息以及 hasUpdate 标记。 + /// 场景示例: + /// - App 冷启动时后台检查更新,但不打扰用户 + /// - 进入「设置-检查更新」页面前,先决定是否展示弹窗 + /// + /// 搭配 [showPreparedUpgrade] 可避免重复请求服务端。 + Future silentCheckUpdate({ + required Future Function() future, + UpgradeConfig? config, + }) async { + final effectiveConfig = config ?? _config; + try { + final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig); + if (effectiveConfig.enableDebugLog) { + if (info == null) { + debugPrint('🔕 静默检查结果: 未返回版本信息'); + } else { + debugPrint('🔕 静默检查完成: hasUpdate=${info.hasUpdate}'); + } + } + return info; + } catch (e) { + if (effectiveConfig.enableDebugLog) { + debugPrint('静默检查更新失败: $e'); + } + return null; + } + } + + /// 使用已知的 [UpgradeInfo] 展示升级弹窗 + /// + /// 通常与 [silentCheckUpdate] 搭配:先静默检查并缓存结果,用户点击 + /// 「检查更新」按钮时再调用此方法展示 UI,无需再次访问服务端。 + Future showPreparedUpgrade({ + required BuildContext context, + required UpgradeInfo info, + bool? autoInstall, + BoolCallback? onComplete, + UpgradeConfig? config, + }) async { + final effectiveConfig = config ?? _config; + final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall; + + assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); + + if (!info.hasUpdate) { + if (effectiveConfig.enableDebugLog) { + debugPrint('🔔 showPreparedUpgrade: 无新版本,跳过弹窗'); + } + if (effectiveConfig.showNoUpdateToast && context.mounted) { + _showToast('已是最新版本', context, effectiveConfig); + } + onComplete?.call(true); + return; + } + + await _showUpgradeDialog( + context: context, + info: info, + autoInstall: finalAutoInstall, + onComplete: onComplete, + config: effectiveConfig, + ); + } + + /// 内部方法:获取版本信息并构建 UpgradeInfo + Future _prepareUpgradeInfo({ + required Future Function() future, + required UpgradeConfig config, + }) async { + // 1. 获取服务器版本信息 + final serverInfo = await future(); + if (serverInfo == null) { + if (config.enableDebugLog) { + debugPrint('🔍 检查更新结果: 未返回版本信息'); + } + return null; + } + + if (config.enableDebugLog) { + debugPrint('🔍 获取到服务器版本: $serverInfo'); + } + + // 2. 获取当前App信息 + final appInfo = await _plugin.getAppInfo(); + final currentVersionName = appInfo['version'] ?? ''; + final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; + + if (config.enableDebugLog) { + debugPrint('📱 当前版本: $currentVersionName (Build: $currentBuildNumber)'); + } + + // 3. 比较版本并构建 UpgradeInfo + bool hasUpdate = false; + if (serverInfo.versionBuildNumber > 0) { + // 优先比较 buildNumber + if (serverInfo.versionBuildNumber > currentBuildNumber) { + hasUpdate = true; + } else if (serverInfo.versionBuildNumber == currentBuildNumber) { + // buildNumber 相同,比较版本名 + if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) { + hasUpdate = true; + } + } + } else { + // 只比较版本名 + if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) { + hasUpdate = true; + } + } + + if (config.enableDebugLog) { + debugPrint('📊 版本比较结果: ${hasUpdate ? "有新版本" : "已是最新"}'); + } + + // 构建 UpgradeInfo + return UpgradeInfo( + hasUpdate: hasUpdate, + isForceUpdate: serverInfo.isForce, + versionName: serverInfo.versionName, + versionBuildNumber: serverInfo.versionBuildNumber, + currentVersionName: currentVersionName, + currentBuildNumber: currentBuildNumber, + updateContent: serverInfo.updateContent, + downloadUrl: serverInfo.downloadUrl, + appStoreUrl: serverInfo.appStoreUrl, + apkSize: serverInfo.apkSize, + apkMd5: serverInfo.apkMd5, + appMarkets: serverInfo.appMarkets, + supportedMethods: serverInfo.supportedMethods ?? + const [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp], + ); + } + /// 预下载APK(不显示UI,后台下载) Future preDownloadApk({ required String url, @@ -1072,12 +1149,7 @@ mixin _UpgradeDialogLogic on State { ), ), const SizedBox(width: 8), - Expanded( - child: _buildRichText( - line, - colorScheme, - ), - ), + Expanded(child: _buildRichText(line, colorScheme)), ], ), );