FlutterUnit 周边 | 组件数据国际化完成
上一篇介绍了通过 Deepseek 完成 FlutterUnit 组件数据的 10 国语言翻译,本篇将解析这些数据,并且重新设计数据库,让 FlutterUnit 在应用层的数据支持多国语言。
英文列表 | 英文详情 |
---|---|
|
|
中文列表 | 中文详情 |
---|---|
|
|
1. 数据库的改造
之前的数据库没有考虑到需要进行数据的国际化,所以介绍信息也是直接作为表中的字段,拿 widget 表来说,承载了所有界面表现需要的字段,
- widget 表
现在要将国际化的数据通过表记录,考虑到后续的可拓展性,比如还有添加其他的语言,或者增加其他需要国际化的字段。这里打算保证 widget 表数据的唯一性,增加 widget_desc
表记录组件国际化相关的数据,通过 widget_id
和 widget 表进行关联。如下是 Container 组件在数据库中的存储形式:
- widget 表,负责确保组件的唯一性:
- widget_desc 表,负责记录组件介绍的国际化描述文本:
node 表记录组件介绍节点,一个组件对应多个 node 数据,节点的介绍信息也是同理,拆分出由 node_desc
表维护组件节点的介绍信息:
- node 表
- node_desc 表
2. 收集数据与解析
数据表定义完毕,接下来的目标收集上一篇章,Deepseek AI 接口生成的翻译文件。将他们整合在一起,录入数据库:
对于数据的收集,这里定义了 3 个对象:
- WidgetData 负责收录一个组件对应的全部信息。
- WidgetDesc 记录组件国际化的描述信息。
- NodeData 记录一个组件介绍节点持有的数据信息。
其中一个 WidgetData 持有若干个国际化描述,以及国际化和组件节点的映射数据:
class WidgetData {
final int id;
final String name;
final List<WidgetDesc> desc;
final int family;
final double lever;
final int deprecated;
final String linkWidget;
final Map<String, List<NodeData>> nodes;
...
}
class WidgetDesc {
final String locale;
final String name;
final String info;
...
}
class NodeData {
final int id;
final int widgetId;
final String name;
final String desc;
final String locale;
final int priority;
final String filepath;
...
}
定义 WidgetParser 解析类维护同步解析逻辑,在构造时传入组件的总目录:
class WidgetParser {
final String widgetPackage;
WidgetParser(this.widgetPackage);
Future<WidgetData> parserWidget(String widgetDir) async {
// TODO 解析组件文件夹,收录信息
}
}
和上篇处理组件翻译类似,这里通过 parserWidget 解析指定路径的组件。其中的过程包括:
_findDescFiles
: 寻找组件文件夹下的国际化描述数据文件。-
_parserNodes
: 解析描述中的节点数据,构造节点列表和语言的映射。
- 解析所有对的描述文件,构造一个 WidgetData 对象。
Iterable<String> _findDescFiles(String dir){
Directory directory = Directory(dir);
List<FileSystemEntity> files = directory.listSync();
return files
.where((e) => (e is File) && p.basename(e.path).startsWith('desc_'))
.map((e) => e.path);
}
Future<WidgetData> parserWidget(String widgetDir) async {
Iterable<String> descPathList = _findDescFiles(widgetDir);
int id = 0;
String name = '';
List<WidgetDesc> desc = [];
int family = 0;
double lever = 0;
int deprecated = 0;
String linkWidget = '';
Map<String, List<NodeData>> nodesMap = {};
for (String descPath in descPathList) {
String filename = p.basenameWithoutExtension(descPath);
String locale = filename.replaceAll('desc_', '');
locale = locale.replaceAll('_', '-').toLowerCase();
dynamic descMap = json.decode(await File(descPath).readAsString());
descMap['locale'] = locale;
if (filename.contains('zh-CN')) {
id = descMap['id'] ?? 0;
name = descMap['name'] ?? '';
family = descMap['family'] ?? 0;
lever = descMap['lever'].toDouble() ?? 0;
deprecated = descMap['deprecated'] ?? 0;
linkWidget = descMap["linkIds"].join(',') ?? '';
}
desc.add(WidgetDesc.fromMap(descMap));
nodesMap[locale] = _parserNodes(widgetDir, descMap);
}
return WidgetData(
id: id,
name: name,
desc: desc,
family: family,
lever: lever,
linkWidget: linkWidget,
deprecated: deprecated,
nodes: nodesMap,
);
}
解析完毕后,得到的 WidgetData 对象,就持有了实现数据库国际化的所有数据。接下来的工作是如何把这个对象录入到数据库中。
List<NodeData> _parserNodes(String directory, dynamic descMap) {
List<NodeData> nodes = [];
int priority = 0;
for (dynamic node in descMap['nodes']) {
nodes.add(NodeData(
id: -1,
widgetId: descMap['id'],
locale: descMap['locale'],
name: node['name'],
filepath: p.join(directory, node['file']),
desc: node['desc'].join('\n'),
priority: priority,
));
priority++;
}
return nodes;
}
3. 数据库录入
这里定义 WidgetSync 类来同步数据库内容,其中:
- 依赖
WidgetParser
解析得到WidgetData
对象 - WidgetDao 负责数据库操作,
insertWidget
方法负责将 WidgetData 中的数据录入数据库。
class WidgetSync {
final String project;
WidgetSync(this.project);
late WidgetParser parser = WidgetParser(project);
WidgetDao dao = WidgetDao();
Future<void> saveWidget(String widgetDir) async {
WidgetData data = await parser.parserWidget(widgetDir);
await dao.insertWidget(data);
}
}
数据的录入主要就是通过 sql 语句,将对象记录的数据插入到 widget
、widget_desc
、node
、node_desc
四张表里。以供 app 实现组件信息的展示功能。
Future<void> insertWidget(WidgetData data) async {
final db = await FlutterDb.db.database;
String insertWidget = """
INSERT INTO
widget
(id,name,family,lever,deprecated,linkWidget)
VALUES
(?,?,?,?,?,?);
""";
List<Object?> args = [data.id, data.name, data.family, data.lever, data.deprecated, data.linkWidget];
await db.rawInsert(insertWidget, args);
String insertWidgetDesc = """
INSERT INTO
widget_desc
(widget_id,name,info,locale)
VALUES
(?,?,?,?);
""";
for (WidgetDesc desc in data.desc) {
List<Object?> args = [data.id, desc.name, desc.info, desc.locale];
await db.rawInsert(insertWidgetDesc, args);
}
await insertNodes(data.nodes);
}
WidgetData 是一个组件的全量数据,包含介绍的节点列表,所以插入过程中可以同时处理 node 表数据的插入:
Future<int> insertNode(NodeData node) async {
int id = flake.id();
final db = await FlutterDb.db.database;
String insertWidget = """
INSERT INTO node
(id,widgetId,priority,code)
VALUES (?,?,?,?);
""";
String code = await File(node.filepath).readAsString();
List<Object?> args = [id, node.widgetId, node.priority, code];
await db.rawInsert(insertWidget, args);
return id;
}
Future<void> insertNodeDesc(int id, NodeData node) async {
final db = await FlutterDb.db.database;
String insertWidget = """
INSERT INTO node_desc
(node_id,name,subtitle,locale)
VALUES (?,?,?,?);
""";
List<Object?> args = [id, node.name, node.desc, node.locale];
await db.rawInsert(insertWidget, args);
}
此时调用 WidgetSync#saveWidget
就可以完成一个组件的所有数据录入。最后,只需要遍历所有的组件,触发 saveWidget
即可。这样就得到了对 FlutterUnit 组件展示来说至关重要的数据库: flutter.db
final Map<int, String> familyMap = {
0: 'StatelessWidget',
1: 'StatefulWidget',
2: 'SingleChildRenderObjectWidget',
3: 'MultiChildRenderObjectWidget',
4: 'Sliver',
5: 'ProxyWidget',
6: 'Other',
};
Future<void> syncAll() async {
for (String family in familyMap.values) {
await syncFamily(family);
}
}
Future<void> syncFamily(String family) async {
String dir = p.join(project, 'modules', 'widget_system', 'widgets', 'lib');
String familyDir = p.join(dir, family);
Directory directory = Directory(familyDir);
List<FileSystemEntity> entity = directory.listSync();
for (FileSystemEntity e in entity) {
if (e is Directory) {
await saveWidget(e.path);
}
}
}
5. FlutterUnit 应用层的处理
数据库目前已经内含 10 国语言的数据,接下来需要升级应用层的查询处理。之前介绍过,FlutterUnit 采用模块化的设计:
- widget: 负责维护所有的组件对应的案例代码
- widget_module: 负责组件展示的业务逻辑和界面构建逻辑。
- widget_repository:负责组件相关数据的操作逻辑。
当前增加数据库多语言查询,其实对业务逻辑和视图构建没有太大的影响。只需要处理 widget_repository
中数据库查询操作即可:比如 WidgetDao
中, queryWidgetByName 方法根据组件名查询组件,现在增加 locale
参数表示查询的语言,通过 widget 和 widget_desc 联表查询,就可以得到之前的数据结构。上层的模型层、视图层都不用任何变化:
Future<Map<String, dynamic>?> queryWidgetByName(String name, {String? locale}) async {
String querySql = """
SELECT
widget.id,
widget.name,
widget.family,
widget.linkWidget,
widget.lever,
widget_desc.name AS nameCN,
widget_desc.info
FROM widget
INNER JOIN widget_desc
ON widget.id = widget_desc.widget_id
WHERE
widget.name = ? AND
widget_desc.locale = ?
;
""";
List<Map<String, Object?>> result = await database.rawQuery(querySql, [name,locale??'zh-cn']);
if (result.isNotEmpty) {
return result.first;
}
return null;
}
这就是模块化以及合理划分层次的好处,新的需求来临时,只需要修改和它相关模块的相关层级即可。外界只在乎它提供的能力,不在意其具体的功能实现,这样就可以局部地升级代码,不会对整个系统产生其他影响。最后在查询时,业务层逻辑的事件,只需要额外承载 locale
参数,作为查询的参数,就可以将 旧的螺丝钉 换成 支持十国语言的新螺丝钉。
extension WidgetContext on BuildContext{
void switchWidgetFamily(WidgetFamily family){
Locale locale = read<AppConfigBloc>().state.language.locale;
String lang = '${locale.languageCode}-${locale.countryCode}'.toLowerCase();
read<WidgetsBloc>().add(EventTabTap(family,locale: lang));
}
}
本文简单介绍了一下 FlutterUnit 支持十国语言的过程,具体细节可以自己查阅开源的项目源码。在 AI 大模型的能力加持下,普通的开发者可以调度更加强大的力量,完成在之前很难实现的功能。包括目前正在进行的 Flutter 组件 Logo 设计,也有 AI 帮忙设计和优化。大家敬请期待 FlutterUnit 的逐步完善,感谢支持 ~