فهرست منبع

[new]优化支持开发模式字符串热更新及时显示,无需重启应用,需按照一定方式配置,具体请查看README.md

zk 6 ماه پیش
والد
کامیت
65a9014a7b

+ 5 - 1
CHANGELOG.md

@@ -14,4 +14,8 @@
 
 * 优化字符串当使用时才调用.tr,而非创建时
 * 优化支持多语言自动生成getx所需的string.gen.dart文件
-* 优化多语言支持watch功能
+* 优化多语言支持watch功能
+
+## 0.0.6
+
+* 优化支持开发模式字符串热更新及时显示,无需重启应用,需按照一定方式配置,具体请查看README.md

+ 16 - 0
README.md

@@ -34,3 +34,19 @@ dart run build_runner clean
 ```bash
 dart run build_runner watch --delete-conflicting-outputs
 ```
+
+### 额外用法,可不用
+
+0.0.6以上版本开发模式支持支持字符串热更新及时显示,但需按照以下方式配置:
+
+```
+1.需要将string_get_runner插件从dev_dependencies迁移至dependencies,因为0.0.6新增部分组件
+2.assets资源目录需增加需要配置需要热更新的语言目录
+    例如:
+      assets:
+        - assets/string/base/
+        - assets/string/en_US/
+        ...
+3.跟容器组件需用StringHotRenewalApp组件包裹
+
+```

+ 1 - 1
build.yaml

@@ -6,7 +6,7 @@ targets:
 
 builders:
   string_get_runner:
-    import: 'package:string_get_runner/string_xml_watcher_builder.dart'
+    import: 'package:string_get_runner/src/builder/string_xml_watcher_builder.dart'
     builder_factories: ['stringXmlWatcherBuilder']
     build_extensions:
       "assets/string/*/*.xml": [ "lib/resource/string.gen.dart" ]

+ 2 - 1
lib/string_xml_watcher_builder.dart

@@ -2,7 +2,8 @@ import 'package:build/build.dart';
 import 'package:xml/xml.dart';
 import 'package:glob/glob.dart';
 import 'package:path/path.dart' as path;
-import 'flutter_string_get_config.dart';
+
+import '../config/flutter_string_get_config.dart';
 
 Builder stringXmlWatcherBuilder(BuilderOptions options) {
   return StringXmlWatcherBuilder();

lib/flutter_string_get_config.dart → lib/src/config/flutter_string_get_config.dart


+ 20 - 0
lib/src/util/de_bounce.dart

@@ -0,0 +1,20 @@
+class Debounce {
+  // 设定的时间间隔,单位为毫秒
+  final int debounceTime;
+
+  // 记录上次点击的时间
+  DateTime? _lastClickTime;
+
+  Debounce({this.debounceTime = 300});
+
+  // 点击事件处理方法
+  void onClick(Function action) {
+    DateTime now = DateTime.now();
+    if (_lastClickTime == null ||
+        now.difference(_lastClickTime!) >
+            Duration(milliseconds: debounceTime)) {
+      _lastClickTime = now;
+      action();
+    }
+  }
+}

+ 59 - 0
lib/src/util/string_gen_util.dart

@@ -0,0 +1,59 @@
+import 'dart:convert';
+
+import 'package:flutter/services.dart';
+import 'package:xml/xml.dart';
+import 'package:path/path.dart' as path;
+
+class StringGenUtil {
+  StringGenUtil._();
+
+  static String extractBaseFolder(String pattern) {
+    final parts = pattern.split('/');
+    final buffer = StringBuffer();
+
+    for (final part in parts) {
+      if (part.contains('*')) break; // 遇到 * 就停止
+      buffer.write(part);
+      buffer.write('/');
+    }
+
+    return buffer.toString();
+  }
+
+  static Future<List<String>> listStringXmlFiles(String folder) async {
+    final manifestJson = await rootBundle.loadString('AssetManifest.json');
+    final Map<String, dynamic> manifestMap = json.decode(manifestJson);
+
+    return manifestMap.keys
+        .where((key) => key.startsWith(folder) && key.endsWith('.xml'))
+        .toList();
+  }
+
+  static String processXmlText(String original) {
+    return original
+        .replaceAll('\r\n', ' ')
+        .replaceAll('\n', ' ')
+        .replaceAll(RegExp(r'\s+'), ' ')
+        .trim()
+        .replaceAll("'", "\\'");
+  }
+
+  static String extractLanguageCode(String filePath, String defaultLanguage) {
+    final segments = path.split(filePath);
+    final stringIndex = segments.indexOf('string');
+
+    if (stringIndex >= 0 && stringIndex + 1 < segments.length) {
+      final langDir = segments[stringIndex + 1];
+      return langDir == 'base' ? defaultLanguage : langDir;
+    }
+
+    return defaultLanguage;
+  }
+
+  static String toCamelCase(String snakeCase) {
+    return snakeCase.split('_').map((word) {
+      if (word == snakeCase.split('_').first) return word;
+      return word[0].toUpperCase() + word.substring(1);
+    }).join('');
+  }
+}

+ 114 - 0
lib/src/widget/string_hot_renewal_app.dart

@@ -0,0 +1,114 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:xml/xml.dart';
+import '../config/flutter_string_get_config.dart';
+import '../util/de_bounce.dart';
+import '../util/string_gen_util.dart';
+
+class StringHotRenewalApp extends StatelessWidget {
+  final Widget child;
+
+  const StringHotRenewalApp({required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    if (kDebugMode) {
+      return HotRenewalView(child: child);
+    } else {
+      return child;
+    }
+  }
+}
+
+class HotRenewalView extends StatefulWidget {
+  final Widget child;
+
+  late final String parentDirPath;
+
+  final FlutterStringGetConfig config = FlutterStringGetConfig.fromProject();
+
+  HotRenewalView({required this.child}) {
+    parentDirPath = StringGenUtil.extractBaseFolder(config.inputDir);
+  }
+
+  @override
+  State<HotRenewalView> createState() => _HotRenewalViewState();
+}
+
+class _HotRenewalViewState extends State<HotRenewalView> {
+  final Debounce reloadDebounce = Debounce(debounceTime: 1000);
+
+  final Map<String, int> _lastXmlHashMap = {}; // 记录每个 XML 的上次内容哈希值
+
+  @override
+  void reassemble() {
+    super.reassemble();
+    reloadDebounce.onClick(() => _reloadTranslations());
+  }
+
+  void _reloadTranslations() async {
+    final dirList =
+        await StringGenUtil.listStringXmlFiles(widget.parentDirPath);
+    if (dirList.isEmpty) return;
+
+    bool hasChanged = false;
+    final updatedXmlMap = <String, String>{};
+
+    for (final path in dirList) {
+      try {
+        final xml = await rootBundle.loadString(path);
+        final hash = xml.hashCode;
+
+        if (_lastXmlHashMap[path] != hash) {
+          hasChanged = true;
+          _lastXmlHashMap[path] = hash;
+          updatedXmlMap[path] = xml;
+        }
+      } catch (e) {}
+    }
+
+    if (!hasChanged) {
+      return;
+    }
+
+    final map = await convertMapData(updatedXmlMap, widget.config.language);
+    Get.clearTranslations();
+    Get.addTranslations(map);
+    Get.forceAppUpdate();
+    print('♻️ 热重载触发,更新翻译');
+  }
+
+  Future<Map<String, Map<String, String>>> convertMapData(
+      Map<String, String> xmlContentMap, String defaultLanguage) async {
+    Map<String, Map<String, String>> multiLangMap = {};
+
+    for (final entry in xmlContentMap.entries) {
+      final assetPath = entry.key;
+      final xmlString = entry.value;
+
+      final languageCode =
+          StringGenUtil.extractLanguageCode(assetPath, defaultLanguage);
+      final document = XmlDocument.parse(xmlString);
+
+      for (final element in document.findAllElements('string')) {
+        final name = element.getAttribute('name');
+        final value = StringGenUtil.processXmlText(element.text);
+
+        multiLangMap.putIfAbsent(languageCode, () => {});
+        if (name != null) {
+          multiLangMap[languageCode]![name] = value;
+        }
+      }
+    }
+
+    return multiLangMap;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+}

+ 2 - 0
lib/string_get_runner.dart

@@ -1 +1,3 @@
+library string_get_runner;
 
+export 'package:string_get_runner/src/widget/string_hot_renewal_app.dart';

+ 6 - 1
pubspec.yaml

@@ -1,6 +1,6 @@
 name: string_get_runner
 description: Multilingual String Generation Plugin for GetX
-version: 0.0.5
+version: 0.0.6
 homepage: http://git.atmob.com/Atmob-Flutter/string_get_runner
 repository: http://git.atmob.com/Atmob-Flutter/string_get_runner
 documentation: http://git.atmob.com/Atmob-Flutter/string_get_runner
@@ -9,6 +9,11 @@ environment:
   sdk: ^3.0.0
 
 dependencies:
+  flutter:
+    sdk: flutter
+
+  get: '>=4.0.0 <6.0.0'
+
   build: ^2.0.0
   yaml: ^3.0.0
   xml: ^6.0.0