feat: Java有关重构#11
Conversation
…初始化、设置等逻辑移动到JavaService内
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a robust Java management system, featuring a new JavaService for runtime state management and enhanced discovery logic in JavaUtils that includes Windows registry scanning. The UI has been updated to support manual Java path selection, runtime refreshing, and macOS-specific menu integrations. My feedback highlights several critical issues: the use of Isolate.run for refreshing runtimes will fail due to memory isolation and plugin limitations, a layout conflict exists between MainAxisSize.min and Spacer in the settings page, Platform.version is incorrectly used for architecture detection, registry handles lack proper resource cleanup, and the javaSelectedPath setter fails to persist changes to storage.
|
镜像源这个倒是没必要规定,自己觉得哪个镜像源快或者是用官方源也没啥所谓,反正应该也没人看pubspec.lock的变更 |
主要是会一直在提交列表里 |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a robust JavaService and JavaUtils to manage, search, and configure Java runtimes across different platforms, updating the launchers and settings UI accordingly. The code review identified several critical issues, including a UX bug in _pickAndAddJavaRuntime that unexpectedly closes the settings page, potential launcher crashes from saving empty system Java paths, a registry handle leak in JavaUtils, and a race condition where the main UI becomes interactive before Java initialization completes. Additionally, feedback was provided regarding improper reset logic when deleting non-active Java runtimes, and the need for better error handling during JSON parsing and background isolate execution.
| Future<void> _pickAndAddJavaRuntime() async { | ||
| // 打开FilePicker | ||
| final result = await FilePicker.platform.pickFiles( | ||
| dialogTitle: '选择Java路径', | ||
| type: FileType.custom, | ||
| allowedExtensions: Platform.isWindows ? ['exe'] : [], | ||
| ); | ||
|
|
||
| setState(() { | ||
| _currentJavaPath = prefs.getString('java'); | ||
| }); | ||
| } | ||
| if (!mounted) return; | ||
|
|
||
| /// | ||
| /// 写入当前 Java | ||
| /// | ||
| Future<void> _setCurrentJavaPathToPrefs(String path) async { | ||
| final prefs = await SharedPreferences.getInstance(); | ||
| await prefs.setString('java', path); | ||
| // 未选择文件 | ||
| if (result == null) { | ||
| showCustomDialog( | ||
| context: context, | ||
| title: '提示', | ||
|
|
||
| setState(() { | ||
| _currentJavaPath = path; | ||
| }); | ||
| } | ||
| content: Text('未选择任何文件'), | ||
|
|
||
| /// | ||
| /// 设置为系统 Java | ||
| /// | ||
| Future<void> _setSystemJava() async { | ||
| final prefs = await SharedPreferences.getInstance(); | ||
| await prefs.remove('java'); | ||
| actions: [ | ||
| if (mounted) | ||
| TextButton( | ||
| onPressed: () => Navigator.of(context).pop(), | ||
| child: Text('关闭'), | ||
| ), | ||
| ], | ||
| ); | ||
|
|
||
| setState(() { | ||
| _currentJavaPath = 'default'; | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 读取选择的文件的信息 | ||
| final file = result.files.single; | ||
| final fileName = file.name.toLowerCase(); | ||
| final validNames = Platform.isWindows | ||
| ? ['java.exe', 'javaw.exe'] | ||
| : ['java', 'javaw']; | ||
|
|
||
| final exe = file.path!; | ||
| final info = await JavaUtils.probeJavaExecutable(exe); | ||
|
|
||
| // 选择的文件文件名合法 | ||
| if (file.path != null && validNames.contains(fileName)) { | ||
| if (!mounted) return; | ||
|
|
||
| showCustomDialog( | ||
| context: context, | ||
| title: '正在添加Java', | ||
| content: SizedBox( | ||
| height: 80, | ||
| child: Center(child: CircularProgressIndicator()), | ||
| ), | ||
|
|
||
| barrierDismissible: false, | ||
| ); | ||
|
|
||
| // | ||
| // 获取系统默认 Java 信息 | ||
| // | ||
| Future<JavaInfo?> _getSystemDefaultJavaInfo() async { | ||
| try { | ||
| final javaVersionProcess = await Process.run('java', ['-version']); | ||
|
|
||
| if (javaVersionProcess.exitCode != 0) { | ||
| LogUtil.log( | ||
| '获取系统默认 Java 信息失败,退出码:${javaVersionProcess.exitCode}', | ||
| level: 'WARN', | ||
| if (info != null) { | ||
| // 查重逻辑 | ||
| final alreadyExists = JavaService.javaRuntimes.any( | ||
| (runtime) => runtime.executable == exe, | ||
| ); | ||
| } | ||
|
|
||
| final versionOutput = (javaVersionProcess.stderr as String).isNotEmpty | ||
| ? javaVersionProcess.stderr as String | ||
| : javaVersionProcess.stdout as String; | ||
| if (alreadyExists) { | ||
| Navigator.of(context).pop(); | ||
|
|
||
| final parsedVersion = JavaManager.parseVersionOutput(versionOutput); | ||
| showCustomDialog( | ||
| context: context, | ||
| title: '提示', | ||
|
|
||
| if (parsedVersion == null) { | ||
| LogUtil.log('无法解析系统默认 Java 版本信息', level: 'WARN'); | ||
| return null; | ||
| } | ||
| content: Text('该Java已存在'), | ||
|
|
||
| String executablePath = ''; | ||
|
|
||
| try { | ||
| if (Platform.isWindows) { | ||
| final where = await Process.run('where', ['java']); | ||
|
|
||
| if (where.exitCode == 0) { | ||
| executablePath = (where.stdout as String) | ||
| .toString() | ||
| .split('\n') | ||
| .first | ||
| .trim(); | ||
| } | ||
| } else { | ||
| final which = await Process.run('which', ['java']); | ||
|
|
||
| if (which.exitCode == 0) { | ||
| executablePath = (which.stdout as String) | ||
| .toString() | ||
| .split('\n') | ||
| .first | ||
| .trim(); | ||
| } | ||
| actions: [ | ||
| if (mounted) | ||
| TextButton( | ||
| onPressed: () => Navigator.of(context).pop(), | ||
| child: Text('关闭'), | ||
| ), | ||
| ], | ||
| ); | ||
|
|
||
| return; | ||
| } | ||
| } catch (e) { | ||
| LogUtil.log('获取系统默认 Java 路径时出错:$e', level: 'WARN'); | ||
| } | ||
|
|
||
| return JavaInfo( | ||
| version: parsedVersion['version'] ?? 'unknown', | ||
| vendor: parsedVersion['vendor'], | ||
| path: executablePath, | ||
| os: Platform.operatingSystem, | ||
| arch: Platform.version, | ||
| ); | ||
| } catch (e) { | ||
| LogUtil.log('执行 "java -version" 时出错:$e', level: 'WARN'); | ||
| return null; | ||
| // 添加并写入SharedPreference | ||
| final isJdk = await JavaUtils.looksLikeJdk(exe); | ||
|
|
||
| JavaService.javaRuntimes.add( | ||
| JavaRuntime(info: info, executable: exe, isJdk: isJdk), | ||
| ); | ||
|
|
||
| JavaService.updateJavaRuntimes(JavaService.javaRuntimes); | ||
|
|
||
| Navigator.of(context).pop(); | ||
|
|
||
| // 调用setState触发页面更新 | ||
| setState(() {}); | ||
|
|
||
| if (!mounted) return; | ||
|
|
||
| showCustomDialog( | ||
| context: context, | ||
| title: '提示', | ||
| content: Text('添加 ${info.vendor} ${info.version} 成功!'), | ||
|
|
||
| actions: [ | ||
| if (mounted) | ||
| TextButton( | ||
| onPressed: () => Navigator.of(context).pop(), | ||
| child: Text('关闭'), | ||
| ), | ||
| ], | ||
| ); | ||
|
|
||
| return; | ||
| } | ||
| } | ||
|
|
||
| // 最后分支,选择的文件文件名不合法等情况 | ||
| if (!mounted) return; | ||
|
|
||
| Navigator.of(context).maybePop(); | ||
|
|
||
| showCustomDialog( | ||
| context: context, | ||
| title: '提示', | ||
| content: Text('请选择正确的Java可执行文件'), | ||
|
|
||
| actions: [ | ||
| TextButton( | ||
| onPressed: () => {if (mounted) Navigator.of(context).pop()}, | ||
| child: Text('关闭'), | ||
| ), | ||
| ], | ||
| ); | ||
| } |
There was a problem hiding this comment.
在 _pickAndAddJavaRuntime 方法中存在一个严重的逻辑缺陷:\n1. 如果用户选择的文件名不合法(不包含 java 或 javaw),代码不会进入 if (file.path != null && validNames.contains(fileName)) 分支,而是直接走到方法末尾的“最后分支”。\n2. 在“最后分支”中,代码会无条件调用 Navigator.of(context).maybePop()。由于此时“正在添加Java”的加载弹窗从未被打开,这一调用会直接将当前的设置页面(JavaPage)关闭,导致用户被意外退回到主设置菜单,并在此之上弹出一个“请选择正确的Java可执行文件”的提示框,这属于严重的 UX 缺陷。\n3. 此外,file.path! 存在强转非空风险,且手动添加的路径没有经过 resolveSymbolicLinks() 规范化,可能导致与自动搜索到的路径产生重复。\n\n建议重构该方法,先进行完整的文件名和路径校验,若不合法则直接弹窗提示并返回;校验通过后再显示加载弹窗,并在 try-catch-finally 中安全地处理解析、查重和弹窗关闭逻辑。
Future<void> _pickAndAddJavaRuntime() async {
// 打开FilePicker
final result = await FilePicker.platform.pickFiles(
dialogTitle: '选择Java路径',
type: FileType.custom,
allowedExtensions: Platform.isWindows ? ['exe'] : [],
);
if (!mounted || result == null) {
if (result == null && mounted) {
showCustomDialog(
context: context,
title: '提示',
content: const Text('未选择任何文件'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
}
return;
}
final file = result.files.single;
final filePath = file.path;
if (filePath == null) return;
final fileName = file.name.toLowerCase();
final validNames = Platform.isWindows
? ['java.exe', 'javaw.exe']
: ['java', 'javaw'];
if (!validNames.contains(fileName)) {
showCustomDialog(
context: context,
title: '提示',
content: const Text('请选择正确的Java可执行文件'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
return;
}
// 显示正在添加的 Dialog
showCustomDialog(
context: context,
title: '正在添加Java',
content: const SizedBox(
height: 80,
child: Center(child: CircularProgressIndicator()),
),
barrierDismissible: false,
);
try {
final exe = await File(filePath).resolveSymbolicLinks();
final info = await JavaUtils.probeJavaExecutable(exe);
if (!mounted) return;
if (info == null) {
Navigator.of(context).pop(); // 关闭正在添加的 Dialog
showCustomDialog(
context: context,
title: '提示',
content: const Text('无法解析该 Java 可执行文件信息'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
return;
}
// 查重逻辑
final alreadyExists = JavaService.javaRuntimes.any(
(runtime) => runtime.executable == exe,
);
if (alreadyExists) {
Navigator.of(context).pop(); // 关闭正在添加的 Dialog
showCustomDialog(
context: context,
title: '提示',
content: const Text('该Java已存在'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
return;
}
// 添加并写入SharedPreference
final isJdk = await JavaUtils.looksLikeJdk(exe);
JavaService.javaRuntimes.add(
JavaRuntime(info: info, executable: exe, isJdk: isJdk),
);
await JavaService.updateJavaRuntimes(JavaService.javaRuntimes);
if (!mounted) return;
Navigator.of(context).pop(); // 关闭正在添加的 Dialog
setState(() {});
showCustomDialog(
context: context,
title: '提示',
content: Text('添加 ${info.vendor} ${info.version} 成功!'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
} catch (e) {
if (mounted) {
Navigator.of(context).pop(); // 关闭正在添加的 Dialog
showCustomDialog(
context: context,
title: '提示',
content: Text('添加失败:$e'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
);
}
}
}| if (_javaSelectedPath.isEmpty) { | ||
| if (systemJavaInfo != null) { | ||
| _javaSelectedPath = systemJavaInfo.path; | ||
| } else if (javaRuntimes.isNotEmpty) { | ||
| _javaSelectedPath = _javaRuntimes.first.executable; | ||
| } | ||
|
|
||
| prefs.setString('javaSelectedPath', _javaSelectedPath); | ||
| } else { | ||
| // 缓存的java已无效,重复上方逻辑 | ||
| final info = await JavaUtils.probeJavaExecutable(_javaSelectedPath); | ||
|
|
||
| if (info == null) { | ||
| if (systemJavaInfo != null) { | ||
| _javaSelectedPath = systemJavaInfo.path; | ||
| } else if (javaRuntimes.isNotEmpty) { | ||
| _javaSelectedPath = _javaRuntimes.first.executable; | ||
| } | ||
|
|
||
| prefs.setString('javaSelectedPath', _javaSelectedPath); | ||
| } |
There was a problem hiding this comment.
当系统默认 Java 存在但其路径为空(例如 java -version 成功但 where/which 查找失败)时,systemJavaInfo.path 将会是空字符串 ""。\n此时如果 _javaSelectedPath 为空,代码会直接将 _javaSelectedPath 设为 systemJavaInfo.path(即 ""),并写入 SharedPreferences。这会导致启动游戏时因可执行文件路径为空而报错。\n建议在检查 systemJavaInfo 时,同时确保其 path 不为空。
if (_javaSelectedPath.isEmpty) {
if (systemJavaInfo != null && systemJavaInfo.path.isNotEmpty) {
_javaSelectedPath = systemJavaInfo.path;
} else if (javaRuntimes.isNotEmpty) {
_javaSelectedPath = _javaRuntimes.first.executable;
}
prefs.setString('javaSelectedPath', _javaSelectedPath);
} else {
// 缓存的java已无效,重复上方逻辑
final info = await JavaUtils.probeJavaExecutable(_javaSelectedPath);
if (info == null) {
if (systemJavaInfo != null && systemJavaInfo.path.isNotEmpty) {
_javaSelectedPath = systemJavaInfo.path;
} else if (javaRuntimes.isNotEmpty) {
_javaSelectedPath = _javaRuntimes.first.executable;
}
prefs.setString('javaSelectedPath', _javaSelectedPath);
}
}| final versionKey = Registry.openPath( | ||
| root, | ||
| path: '$path\\$versionKeyName', | ||
| desiredAccessRights: AccessRights.readOnly, | ||
| ); | ||
|
|
||
| // 读取路径 | ||
| final javaHome = versionKey.getStringValue('JavaHome'); | ||
| final installationPath = versionKey.getStringValue( | ||
| 'InstallationPath', | ||
| ); | ||
|
|
||
| try { | ||
| String? javaPath = javaHome ?? installationPath; | ||
|
|
||
| if (javaPath != null && javaPath.isNotEmpty) { | ||
| candidates.add(Directory(javaPath)); | ||
| } | ||
| } finally { | ||
| // 确保键被关闭 | ||
| versionKey.close(); | ||
| } |
There was a problem hiding this comment.
在读取注册表时,versionKey 是在 try-finally 块外部打开的,并且 getStringValue 也是在外部调用的。如果 Registry.openPath 抛出异常,或者 getStringValue 找不到对应的键值而抛出异常,versionKey.close() 将不会被执行,从而导致注册表句柄泄漏。\n此外,getStringValue 在键值不存在时会抛出异常,应该对其进行捕获。建议将打开和读取操作放入 try-catch-finally 块中。
RegistryKey? versionKey;
try {
versionKey = Registry.openPath(
root,
path: '$path\\$versionKeyName',
desiredAccessRights: AccessRights.readOnly,
);
String? javaHome;
try {
javaHome = versionKey.getStringValue('JavaHome');
} catch (_) {}
String? installationPath;
try {
installationPath = versionKey.getStringValue('InstallationPath');
} catch (_) {}
final javaPath = javaHome ?? installationPath;
if (javaPath != null && javaPath.isNotEmpty) {
candidates.add(Directory(javaPath));
}
} catch (_) {
// 忽略单个版本键的读取失败
} finally {
versionKey?.close();
}| builder: (context, snapshot) { | ||
| if (snapshot.connectionState == ConnectionState.done && | ||
| JavaService.javaRuntimes.isEmpty) { |
There was a problem hiding this comment.
在 JavaService.initFuture 尚未完成(即 ConnectionState 不是 done)时,FutureBuilder 会直接渲染出完整的、可交互的主界面。\n如果用户在初始化完成前点击了“启动”或进入“设置”,可能会因为 javaSelectedPath 尚未完成初始化而导致 race condition 或崩溃。\n建议在初始化未完成时,显示一个加载指示器(如 CircularProgressIndicator),阻止用户进行交互,直到 Java 初始化完全结束。
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (JavaService.javaRuntimes.isEmpty) {| // 从列表中移除 | ||
| JavaService.javaRuntimes.remove(javaRuntime); | ||
|
|
||
| typeChipLabel: javaRuntime.isJdk ? 'JDK' : 'JRE', | ||
| // 刷新UI 并执行异步操作 | ||
| setState(() {}); | ||
|
|
||
| vendor: javaRuntime.info.vendor, | ||
| await JavaService.updateJavaRuntimes( | ||
| JavaService.javaRuntimes, | ||
| ); | ||
|
|
||
| isCurrent: isCurrentJava, | ||
| if (!mounted) return; | ||
|
|
||
| onTap: () => | ||
| _setCurrentJavaPathToPrefs(javaRuntime.executable), | ||
| ); | ||
| }, | ||
| // 若移除的为当前Java,将第一个设置为javaRuntimes的第一个 | ||
| if (JavaService.javaRuntimes.isNotEmpty) { | ||
| await JavaService.updateJavaSelectedPath( | ||
| JavaService.javaRuntimes.first.executable, | ||
| ); | ||
| } |
There was a problem hiding this comment.
在列表中删除某个 Java 运行时后,代码会无条件地将当前选择的 Java 路径重置为列表中的第一个 Java 运行时(JavaService.javaRuntimes.first.executable)。\n这会导致即使删除的不是当前选中的 Java,用户的选择也会被意外重置。\n建议仅在被删除的 Java 确实是当前选中的 Java 时,才执行重置逻辑。
| // 从列表中移除 | |
| JavaService.javaRuntimes.remove(javaRuntime); | |
| typeChipLabel: javaRuntime.isJdk ? 'JDK' : 'JRE', | |
| // 刷新UI 并执行异步操作 | |
| setState(() {}); | |
| vendor: javaRuntime.info.vendor, | |
| await JavaService.updateJavaRuntimes( | |
| JavaService.javaRuntimes, | |
| ); | |
| isCurrent: isCurrentJava, | |
| if (!mounted) return; | |
| onTap: () => | |
| _setCurrentJavaPathToPrefs(javaRuntime.executable), | |
| ); | |
| }, | |
| // 若移除的为当前Java,将第一个设置为javaRuntimes的第一个 | |
| if (JavaService.javaRuntimes.isNotEmpty) { | |
| await JavaService.updateJavaSelectedPath( | |
| JavaService.javaRuntimes.first.executable, | |
| ); | |
| } | |
| final wasSelected = JavaService.javaSelectedPath == javaRuntime.executable; | |
| // 从列表中移除 | |
| JavaService.javaRuntimes.remove(javaRuntime); | |
| // 刷新UI 并执行异步操作 | |
| setState(() {}); | |
| await JavaService.updateJavaRuntimes( | |
| JavaService.javaRuntimes, | |
| ); | |
| if (!mounted) return; | |
| // 若移除的为当前Java,将第一个设置为javaRuntimes的第一个 | |
| if (wasSelected && JavaService.javaRuntimes.isNotEmpty) { | |
| await JavaService.updateJavaSelectedPath( | |
| JavaService.javaRuntimes.first.executable, | |
| ); | |
| } |
| } else { | ||
| cachedRuntimes = await readJavaRuntimesFromPrefs(cachedJson); | ||
|
|
||
| // 遍历缓存的列表 | ||
| for (final javaRuntime in cachedRuntimes) { | ||
| // 仅检测对应文件是否存在 | ||
| if (await File(javaRuntime.executable).exists()) { | ||
| _javaRuntimes.add(javaRuntime); | ||
| validPaths.add(javaRuntime.executable); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
如果 SharedPreferences 中缓存的 javaRuntimes JSON 字符串损坏或格式不兼容,readJavaRuntimesFromPrefs 会抛出异常,导致整个 init() 初始化流程崩溃。\n建议使用 try-catch 包裹缓存解析逻辑,在解析失败时记录日志并清除损坏的缓存,然后自动触发重新搜索,以提高应用的健壮性。
| } else { | |
| cachedRuntimes = await readJavaRuntimesFromPrefs(cachedJson); | |
| // 遍历缓存的列表 | |
| for (final javaRuntime in cachedRuntimes) { | |
| // 仅检测对应文件是否存在 | |
| if (await File(javaRuntime.executable).exists()) { | |
| _javaRuntimes.add(javaRuntime); | |
| validPaths.add(javaRuntime.executable); | |
| } | |
| } | |
| } | |
| } else { | |
| List<JavaRuntime> cachedRuntimes = []; | |
| try { | |
| cachedRuntimes = await readJavaRuntimesFromPrefs(cachedJson); | |
| } catch (e) { | |
| LogUtil.log('解析缓存的 Java 运行时失败:$e', level: 'WARN'); | |
| } | |
| // 遍历缓存的列表 | |
| for (final javaRuntime in cachedRuntimes) { | |
| // 仅检测对应文件是否存在 | |
| if (await File(javaRuntime.executable).exists()) { | |
| _javaRuntimes.add(javaRuntime); | |
| validPaths.add(javaRuntime.executable); | |
| } | |
| } | |
| } |
| try { | ||
| // 在子线程获取数据 | ||
| final searchResults = await Isolate.run( | ||
| () => JavaUtils.searchPotentialJavaExecutables(), | ||
| ); | ||
|
|
||
| // 将javaRuntimes转换为Map | ||
| final Map<String, JavaRuntime> runtimeMap = { | ||
| for (var runtime in JavaService.javaRuntimes) | ||
| runtime.executable: runtime, | ||
| }; | ||
|
|
||
| // 通过Map查重 | ||
| int addedCount = 0; | ||
| for (var result in searchResults) { | ||
| if (!runtimeMap.containsKey(result.executable)) { | ||
| runtimeMap[result.executable] = result; | ||
| // 发现新Java时计数 | ||
| addedCount++; | ||
| } | ||
| } | ||
|
|
||
| final totalList = runtimeMap.values.toList(); | ||
|
|
||
| // 更新并写入 | ||
| await JavaService.updateJavaRuntimes(totalList); | ||
|
|
||
| if (!mounted) return; | ||
|
|
||
| String message = addedCount > 0 | ||
| ? '刷新完成,搜索到了$addedCount个Java(共有${totalList.length}个)' | ||
| : '刷新完成,未发现新的Java (共有${totalList.length}个)'; | ||
|
|
||
| ScaffoldMessenger.of( | ||
| context, | ||
| ).showSnackBar(SnackBar(content: Text(message))); | ||
| } finally { | ||
| if (mounted) setState(() => _isRefreshing = false); | ||
| } |
There was a problem hiding this comment.
在刷新 Java 列表时,如果 Isolate.run 抛出异常(例如由于文件系统或注册表访问权限问题),异常将无法被捕获并导致应用崩溃。\n建议使用 try-catch 捕获异常,并在界面上向用户展示友好的错误提示(如 SnackBar)。
try {
// 在子线程获取数据
final searchResults = await Isolate.run(
() => JavaUtils.searchPotentialJavaExecutables(),
);
// 将javaRuntimes转换为Map
final Map<String, JavaRuntime> runtimeMap = {
for (var runtime in JavaService.javaRuntimes)
runtime.executable: runtime,
};
// 通过Map查重
int addedCount = 0;
for (var result in searchResults) {
if (!runtimeMap.containsKey(result.executable)) {
runtimeMap[result.executable] = result;
// 发现新Java时计数
addedCount++;
}
}
final totalList = runtimeMap.values.toList();
// 更新并写入
await JavaService.updateJavaRuntimes(totalList);
if (!mounted) return;
String message = addedCount > 0
? '刷新完成,搜索到了$addedCount个Java(共有${totalList.length}个)'
: '刷新完成,未发现新的Java (共有${totalList.length}个)';
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('刷新失败:$e')),
);
}
} finally {
if (mounted) setState(() => _isRefreshing = false);
}
重构内容:
searchPotentialJavaExecutables)内的遍历,在Windows下采用搜索注册表的方法(参考PCL2),对于Mac与Linux没有地方测试(java更改为javaSelectedPath,添加javaRuntimes缓存Java的信息实在是铲不动了太多东西要改的了
没有ChangeNotifier真的很难改,这算前面挖的坑了
所以Java部分先改到这 (•_•),我打算开始改UI了
对了最好在contribution.md里面提一嘴统一PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL,在我这里很容易因为
pub get一下pubspec.lock就会迎来大变实际上还是有点问题的 在初始化java的时候会阻塞UI,但是改这个要动很多东西所以没打算在现在弄
希望能早点合并喵