flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
||||
part 'app_version_entity.g.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【RemoteVersionEntity】远端版本策略实体(Isar 集合 + API 响应模型)
|
||||
///
|
||||
/// 一、双重身份:
|
||||
/// 1. Isar @collection → 可持久化到本地数据库,离线时读取缓存策略;
|
||||
/// 2. fromJson 工厂 → 直接解析后端 GET /api/app/version 返回的 data。
|
||||
///
|
||||
/// 二、字段对照(后端 JSON → 本实体):
|
||||
/// latestVersion → 最新对外展示版本号(如 "1.1.0")
|
||||
/// latestBuild → 最新构建号(整型,用于版本比对)
|
||||
/// minSupportBuild → 最低兼容构建号(本地 build < 此值 → 强制更新)
|
||||
/// force → 是否强制更新(非最低兼容场景的开关)
|
||||
/// updateDesc → 更新日志列表
|
||||
/// androidUrl → 安卓下载地址
|
||||
/// iosUrl → iOS App Store 链接
|
||||
/// enableHotfix → 热更新开关(仅安卓生效,iOS 禁止下发 dart 代码)
|
||||
///
|
||||
/// 三、Isar 注意:
|
||||
/// - 改字段后必须重跑 build_runner 重新生成 Schema;
|
||||
/// - Isar 只在本文件被 native 端 import(条件导出),web 不会编译到。
|
||||
/// ============================================================
|
||||
@collection
|
||||
class RemoteVersionEntity {
|
||||
RemoteVersionEntity();
|
||||
|
||||
Id id = Isar.autoIncrement;
|
||||
|
||||
String? latestVersion;
|
||||
int? latestBuild;
|
||||
int? minSupportBuild;
|
||||
bool? force;
|
||||
|
||||
/// 更新日志(`List<String>` Isar 原生支持)
|
||||
List<String>? updateDesc;
|
||||
|
||||
String? androidUrl;
|
||||
String? iosUrl;
|
||||
bool? enableHotfix;
|
||||
|
||||
/// 从后端 JSON 解析(data 层,不含外层 code)
|
||||
factory RemoteVersionEntity.fromJson(Map<String, dynamic> json) {
|
||||
return RemoteVersionEntity()
|
||||
..latestVersion = json['latestVersion'] as String?
|
||||
..latestBuild = json['latestBuild'] as int?
|
||||
..minSupportBuild = json['minSupportBuild'] as int?
|
||||
..force = json['force'] as bool?
|
||||
..updateDesc = (json['updateDesc'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList()
|
||||
..androidUrl = json['androidUrl'] as String?
|
||||
..iosUrl = json['iosUrl'] as String?
|
||||
..enableHotfix = json['enableHotfix'] as bool?;
|
||||
}
|
||||
|
||||
/// 判断是否需要强制更新
|
||||
/// [localBuild] 本机构建号(package_info_plus 读取)
|
||||
bool needsForceUpdate(int localBuild) {
|
||||
final minBuild = minSupportBuild ?? 0;
|
||||
if (localBuild < minBuild) return true;
|
||||
if (localBuild < (latestBuild ?? 0) && (force ?? false)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 判断是否需要更新(含可选)
|
||||
bool needsUpdate(int localBuild) {
|
||||
return localBuild < (latestBuild ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'RemoteVersion(latest=$latestVersion, build=$latestBuild, '
|
||||
'minBuild=$minSupportBuild, force=$force, hotfix=$enableHotfix)';
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
/// ============================================================
|
||||
/// 【AppVersionService + RemoteVersionEntity】版本服务 —— 平台分发入口
|
||||
///
|
||||
/// 用法:
|
||||
/// import 'package:new_learn/core/version/app_version_service.dart';
|
||||
/// final service = AppVersionService.instance;
|
||||
/// await service.init();
|
||||
/// final update = await service.checkForUpdate();
|
||||
///
|
||||
/// 一、为什么拆成条件导出(和 app_database.dart 同模式):
|
||||
/// native 实现 import 了 isar_community + package_info_plus,
|
||||
/// web 端没有 Isar 支持(编译不过),所以 web 用空实现。
|
||||
///
|
||||
/// 二、条件导出规则:
|
||||
/// native(dart.library.io 为真)→ app_version_service_io.dart
|
||||
/// web(dart.library.io 为假) → app_version_service_web.dart
|
||||
///
|
||||
/// 三、两个文件必须保持 API 一致(类名、方法签名完全相同),
|
||||
/// 否则共享代码在某个平台编译不过。
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
export 'app_version_service_web.dart'
|
||||
if (dart.library.io) 'app_version_service_io.dart';
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
import 'package:new_learn/core/storage/database/app_database_io.dart';
|
||||
import 'app_version_entity.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【AppVersionService】版本核心服务 —— native 真实现(Android/iOS/桌面)
|
||||
///
|
||||
/// 一、职责:
|
||||
/// 1. init():通过 package_info_plus 读取本机 versionName、buildNumber;
|
||||
/// 2. fetchRemoteVersion():请求后端 GET /api/app/version,解析并存入 Isar;
|
||||
/// 3. checkForUpdate():版本比对,返回 RemoteVersionEntity 或 null;
|
||||
/// 4. showUpdateDialogIfNeeded():在页面 mounted 后唤起更新弹窗。
|
||||
///
|
||||
/// 二、隐私约束:
|
||||
/// ⚠️ 严禁在隐私协议同意前调用 init() 或任何网络请求。
|
||||
/// 只有 privacyAcceptedNotifier.value == true 后才能使用本服务。
|
||||
///
|
||||
/// 三、缓存策略:
|
||||
/// 接口成功 → 解析 RemoteVersionEntity → Isar 写入缓存(覆盖旧数据);
|
||||
/// 网络异常 → 从 Isar 读取上次缓存策略做降级判断。
|
||||
///
|
||||
/// 四、版本比对规则(全程用 buildNumber 整型比较):
|
||||
/// localBuild < minSupportBuild → 强制更新
|
||||
/// localBuild < latestBuild && force == true → 强制更新
|
||||
/// localBuild < latestBuild && force == false → 可选更新
|
||||
/// localBuild >= latestBuild → 无需更新
|
||||
/// ============================================================
|
||||
class AppVersionService {
|
||||
AppVersionService._();
|
||||
static final AppVersionService instance = AppVersionService._();
|
||||
|
||||
/// 本机版本名(如 "1.0.0")
|
||||
String _versionName = '';
|
||||
String get versionName => _versionName;
|
||||
|
||||
/// 本机构建号(如 1)
|
||||
int _buildNumber = 0;
|
||||
int get buildNumber => _buildNumber;
|
||||
|
||||
/// 最近一次获取到的远端版本信息(检查后可读取)
|
||||
RemoteVersionEntity? _remoteVersion;
|
||||
RemoteVersionEntity? get remoteVersion => _remoteVersion;
|
||||
|
||||
/// ① 初始化:读取本机版本信息
|
||||
/// ⚠️ 必须在隐私协议同意后调用
|
||||
Future<void> init() async {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
_versionName = info.version;
|
||||
_buildNumber = int.tryParse(info.buildNumber) ?? 0;
|
||||
debugPrint(
|
||||
'▶ 【版本】本机版本:$_versionName+$_buildNumber');
|
||||
} catch (e) {
|
||||
debugPrint('▶ 【版本】读取本机版本失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// ② 请求后端版本接口
|
||||
/// 成功:解析并存入 Isar 缓存
|
||||
/// 失败:从 Isar 缓存读取上次策略
|
||||
Future<RemoteVersionEntity?> fetchRemoteVersion() async {
|
||||
try {
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// 🎯 Mock 开关:改成 true 用模拟数据,false 请求真实接口
|
||||
// 修改 _mockData 模拟不同场景,热重启即可
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// const bool mock = false;
|
||||
const bool mock = true; // ← 取消注释这行,注释掉上面那行,开启 Mock
|
||||
|
||||
final dynamic data;
|
||||
if (mock) {
|
||||
// ====== 🎯 修改这里模拟不同场景 ======
|
||||
// 当前 App 版本:pubspec.yaml → version: 1.0.0+1(buildNumber=1)
|
||||
//
|
||||
// 场景 A - 无需更新:latestBuild = 1
|
||||
// 场景 B - 可选更新:latestBuild = 3, force = false
|
||||
// 场景 C - 强制更新:latestBuild = 3, force = true
|
||||
// 场景 D - 最低兼容强制更新:minSupportBuild = 5
|
||||
data = {
|
||||
"latestVersion": "1.1.1",
|
||||
"latestBuild": 3,
|
||||
"minSupportBuild": 1,
|
||||
"force": false,
|
||||
"updateDesc": [
|
||||
"新增学习报告功能",
|
||||
"优化视频播放体验",
|
||||
"修复已知问题",
|
||||
],
|
||||
"androidUrl": "https://www.example.com/app-release.apk",
|
||||
"iosUrl": "https://apps.apple.com/cn/example",
|
||||
"enableHotfix": false,
|
||||
};
|
||||
// 模拟 500ms 网络延迟(和 Vue3 里 setTimeout 一个效果)
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
} else {
|
||||
data = await DioClient.instance.get('/api/app/version');
|
||||
}
|
||||
// 兼容 {code, data: {...}} 和直接返回 data 两种格式
|
||||
final payload =
|
||||
(data is Map<String, dynamic> && data.containsKey('data'))
|
||||
? data['data'] as Map<String, dynamic>
|
||||
: data as Map<String, dynamic>;
|
||||
final entity = RemoteVersionEntity.fromJson(payload);
|
||||
// 存入 Isar 缓存(先清旧数据再写新数据)
|
||||
await _saveToCache(entity);
|
||||
debugPrint('▶ 【版本】远端版本:$entity');
|
||||
return entity;
|
||||
} catch (e) {
|
||||
debugPrint('▶ 【版本】请求远端版本失败:$e,尝试读取本地缓存');
|
||||
return _loadFromCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// ③ 版本比对,返回需要更新时的远端版本信息,无需更新返回 null
|
||||
Future<RemoteVersionEntity?> checkForUpdate() async {
|
||||
if (_buildNumber == 0) {
|
||||
// 本机版本未初始化,先 init
|
||||
await init();
|
||||
}
|
||||
final remote = await fetchRemoteVersion();
|
||||
_remoteVersion = remote;
|
||||
if (remote == null) return null;
|
||||
if (!remote.needsUpdate(_buildNumber)) {
|
||||
debugPrint('▶ 【版本】当前已是最新版本');
|
||||
return null;
|
||||
}
|
||||
return remote;
|
||||
}
|
||||
|
||||
/// 判断当前远端版本是否要求强制更新
|
||||
bool isForceUpdate() {
|
||||
final remote = _remoteVersion;
|
||||
if (remote == null) return false;
|
||||
return remote.needsForceUpdate(_buildNumber);
|
||||
}
|
||||
|
||||
/// 存入 Isar 缓存(覆盖旧数据)
|
||||
Future<void> _saveToCache(RemoteVersionEntity entity) async {
|
||||
try {
|
||||
final db = AppDatabase.instance;
|
||||
await db.isar.writeTxn(() async {
|
||||
// 清除旧缓存(只保留一条最新策略)
|
||||
await db.remoteVersions.clear();
|
||||
await db.remoteVersions.put(entity);
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('▶ 【版本】缓存远端版本失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 Isar 缓存读取
|
||||
Future<RemoteVersionEntity?> _loadFromCache() async {
|
||||
try {
|
||||
final db = AppDatabase.instance;
|
||||
final all = await db.remoteVersions.where().findAll();
|
||||
final cached = all.isNotEmpty ? all.first : null;
|
||||
if (cached != null) {
|
||||
debugPrint('▶ 【版本】使用缓存策略:$cached');
|
||||
}
|
||||
return cached;
|
||||
} catch (e) {
|
||||
debugPrint('▶ 【版本】读取缓存失败:$e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【AppVersionService】版本核心服务 —— web 空实现(Chrome/Edge 调试用)
|
||||
///
|
||||
/// web 端没有 package_info_plus(返回固定值)和 Isar(不支持缓存),
|
||||
/// 只做基础网络请求,无缓存降级。
|
||||
/// 生产走 Android/iOS,本文件仅供 Web 调试 UI 流程。
|
||||
///
|
||||
/// ⚠ 注意:本文件不 import isar 或 package_info_plus 的原生能力,
|
||||
/// web 端编译不会报错。
|
||||
/// ============================================================
|
||||
|
||||
/// 远端版本实体(web 简化版,非 Isar 集合)
|
||||
class RemoteVersionEntity {
|
||||
RemoteVersionEntity();
|
||||
|
||||
String? latestVersion;
|
||||
int? latestBuild;
|
||||
int? minSupportBuild;
|
||||
bool? force;
|
||||
List<String>? updateDesc;
|
||||
String? androidUrl;
|
||||
String? iosUrl;
|
||||
bool? enableHotfix;
|
||||
|
||||
factory RemoteVersionEntity.fromJson(Map<String, dynamic> json) {
|
||||
return RemoteVersionEntity()
|
||||
..latestVersion = json['latestVersion'] as String?
|
||||
..latestBuild = json['latestBuild'] as int?
|
||||
..minSupportBuild = json['minSupportBuild'] as int?
|
||||
..force = json['force'] as bool?
|
||||
..updateDesc = (json['updateDesc'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList()
|
||||
..androidUrl = json['androidUrl'] as String?
|
||||
..iosUrl = json['iosUrl'] as String?
|
||||
..enableHotfix = json['enableHotfix'] as bool?;
|
||||
}
|
||||
|
||||
bool needsForceUpdate(int localBuild) {
|
||||
final minBuild = minSupportBuild ?? 0;
|
||||
if (localBuild < minBuild) return true;
|
||||
if (localBuild < (latestBuild ?? 0) && (force ?? false)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool needsUpdate(int localBuild) {
|
||||
return localBuild < (latestBuild ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'RemoteVersion(latest=$latestVersion, build=$latestBuild, '
|
||||
'minBuild=$minSupportBuild, force=$force)';
|
||||
}
|
||||
|
||||
class AppVersionService {
|
||||
AppVersionService._();
|
||||
static final AppVersionService instance = AppVersionService._();
|
||||
|
||||
String _versionName = '0.0.0';
|
||||
String get versionName => _versionName;
|
||||
|
||||
int _buildNumber = 0;
|
||||
int get buildNumber => _buildNumber;
|
||||
|
||||
RemoteVersionEntity? _remoteVersion;
|
||||
RemoteVersionEntity? get remoteVersion => _remoteVersion;
|
||||
|
||||
/// web 端:package_info_plus 不可用,使用固定值
|
||||
Future<void> init() async {
|
||||
_versionName = '0.0.0';
|
||||
_buildNumber = 0;
|
||||
debugPrint('▶ 【版本】Web 调试模式:版本信息使用固定值');
|
||||
}
|
||||
|
||||
/// web 端:无 Isar 缓存,网络失败直接返回 null
|
||||
Future<RemoteVersionEntity?> fetchRemoteVersion() async {
|
||||
try {
|
||||
final data = await DioClient.instance.get('/api/app/version');
|
||||
final payload =
|
||||
(data is Map<String, dynamic> && data.containsKey('data'))
|
||||
? data['data'] as Map<String, dynamic>
|
||||
: data as Map<String, dynamic>;
|
||||
final entity = RemoteVersionEntity.fromJson(payload);
|
||||
debugPrint('▶ 【版本】远端版本:$entity');
|
||||
return entity;
|
||||
} catch (e) {
|
||||
debugPrint('▶ 【版本】Web 请求远端版本失败:$e(无缓存降级)');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<RemoteVersionEntity?> checkForUpdate() async {
|
||||
if (_buildNumber == 0) await init();
|
||||
final remote = await fetchRemoteVersion();
|
||||
_remoteVersion = remote;
|
||||
if (remote == null) return null;
|
||||
if (!remote.needsUpdate(_buildNumber)) return null;
|
||||
return remote;
|
||||
}
|
||||
|
||||
bool isForceUpdate() {
|
||||
final remote = _remoteVersion;
|
||||
if (remote == null) return false;
|
||||
return remote.needsForceUpdate(_buildNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/// ============================================================
|
||||
/// 【RemoteVersionEntity】远端版本实体 —— 平台分发入口
|
||||
///
|
||||
/// native → app_version_entity.dart(Isar @collection)
|
||||
/// web → app_version_service_web.dart(普通类,内联在 service web 文件中)
|
||||
///
|
||||
/// 和 app_version_service.dart 同模式:条件导出,调用方只 import 这一个文件。
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
export 'app_version_service_web.dart'
|
||||
// web 文件里定义了 RemoteVersionEntity 类,web 端直接用它
|
||||
if (dart.library.io) 'app_version_entity.dart';
|
||||
Reference in New Issue
Block a user