在 《 FlutterUnit 3.1.0 》 中提到过,我正在用 Rust 设计并搭建了一套后端服务。 FlutterUnit 3.2.0 将基于这个服务接口,提供一些网络相关的功能。另外 FlutterUnit 3.2.0 版本对国际化的流程进行了优化,支持 10 国语言,并通过脚本工具来辅助维护。
开源地址: github.com/toly1994328…
一、 最新资讯
3.2.0 版本中,增加 最新资讯 功能,在首页展示可滑页的文章列表。展示资讯内容,包括flutter、dart相关的新版本、新特性、新动向。感谢 恋猫de小郭 对最新资讯的文章支持,后续我也会关注flutter的新资讯,写写文章。
1. 界面效果
最新资讯在首页的上方,可以左右无限滑动的 PageView,并有指示器示意当前条目位置,效果如下:
- 桌面端效果
- 移动端效果
1 | 2 |
---|---|
2. 后端的数据
最新资讯的数据,自然是需要及时更新的。我通过 rust 搭建的后端服务,已经可以正常工作了。最新资讯的数据,来源于后端的文章 (article) 系统。最新资讯通过 标签(tag) 来查询数据,标签和文章是一对多的关系,标签表和文章表通过 t_article_tag 关联。
也就是说,现在想在首页新加一个条目,只需要在 article
里加一条数据,然后在 t_article_tag
表中增加对应文章 id 的映射即可。 此时文章信息的数据修改,界面上也可以感知。
3. 前端实现简介
最新资讯是一个独立的单元,它隶属于文章系统,FlutterUnit 中的文章由 note 模块维护
所以我在note 模块创建一个bloc,来维护最新资讯的数据。NewsBloc 中通过 HttpArticleRepository
获取最资讯的网络数据;通过 TimeoutCache
混入缓存的能力。一开始通过 initByCache
方法获取缓存中的数据:
最新资讯数据的生命周期是全程的,应用启动成功时,会触发 load
方法加载数据。这里为了避免每次打开都请求接口,做了30分钟的本地持久化缓存,缓存也可以加快从进入应用到内容展示时间。
TimeoutCache 在 find 时会校验缓存的时间决定是否过期,如果没有过期,使用缓存数据展示,否则通过 refreshFromNet
方法从网络加载数据,并通过 save
方法缓存网络请求的数据:
void load() async {
List<ArticlePo>? retCache = await find();
if (retCache != null) {
emit(NewsState(headerNews: retCache));
return;
}
refreshFromNet();
}
Future<void> refreshFromNet() async {
ApiRet<List<ArticlePo>> ret = await _repository.getArticlesByTag(1);
if (ret.success) {
save(ret.data);
emit(NewsState(headerNews: ret.data));
}
}
首页下拉刷新时,属于用户明确的意向动作。此时会触发NewsBloc 的 refreshFromNet 的方法,访问网络更新最新数据。下拉刷新的位置在组件模块,NewsBloc作为全局的bloc,可以在任何地方被访问。
二、世界留言板
本来打算做个人小笔记的,但是感觉步子可以迈小点。于是先推出一个世界留言板,目前所有人都可以在这里查看、编辑留言、新建留言板。记录你的任何小技巧、遇到的问题、想说的话等...
1. 界面效果
FlutterUnit 里为留言板单独留了一个板块,通过首页导航进行切换。效果如下:
- 桌面端效果
- 移动端效果
1 | 2 |
---|---|
目前的功能还比较简单,仅支持新建和编辑。后面有时间可以基于此演化为一个小论坛、结合用户系统也可以做个人的笔记,可拓展的前景还是很广阔的。
2. 后端的数据
article 表是一切数据的源泉。留言板中的操作,本质上都是对这个表的修改:
后端接口提供了增删改查、打开、写入相关的接口,完成最基本的文章功能:
3. 前端实现简介
这同样隶属于文章系统,代码在 note 模块下。业务逻辑在 ArtSysBloc
中,ArticleRepository 接口负责文章相关的数据处理;HttpArticleRepository
是他的一个实现,用于加载网络文章数据。后面准备做一个本地缓存数据库,优先取用本地缓存。
状态类中有三个核心数据:articles 是文章列表, active 是当前激活的文章,status 是列表的加载状态。根据这三个数据就可以构建对应的视图。
class ArtSysState {
final List<ArticlePo> articles;
final ArticlePo? active;
final ListStatus status;
ArtSysState({
required this.articles,
this.active,
this.status = const LoadingStatus(),
});
三、组件 logo
组件的 logo 设计是一个持久化的过程。可以看出首屏的组件已经有了自己独立的 logo :
进入详情页时,也会带上对应的 logo,并进行 Hero 动画:
图标的资源在 assets/images/widgets
目录下:
组件的 logo 图通过 figma 设计,感兴趣的朋友也可以提供暂未设计的 Flutter 组件 logo,如何能接入 ai 设计这些是最好不过了。
四、国际化处理
在 《DeepSeek 助力 FlutterUnit 组件数据国际化》 一文中,已经完成了基于 deepseek api 进行国际化翻译。目前不止是应用内的文字,也包括组件相关数据本身,都支持了 10 国语言的翻译:
但对于 app 应用内部本身字符的国际化来说,每增加一个字段或者修改内容,就需要在每个文件里增加对应的键值对,这无疑是非常难以维护的。
然后想到了一个方案,可以基于 xlsx
表格来统一维护字符内容。然后通过 dart 脚本解析 xlsx
的内容:
输出对应的 arb 文件,这样数据具有唯一的 xlsx
来源,更新修改在表格文档里交互处理也比较方便。更重要的是预览方面,而且和编程开发解耦,文档内容可以交由专门的人维护,比如产品。
下面是 AI 帮忙写的脚本,大家可以参考一下:
import 'dart:convert';
import 'dart:io';
import 'package:excel/excel.dart';
import 'package:path/path.dart' as p;
void main() async {
String dataPath = r'数据文件.xlsx';
final bytes = File(dataPath).readAsBytesSync();
final excel = Excel.decodeBytes(bytes);
final sheet = excel.tables.values.first;
if (sheet == null) {
print('❌ 没有找到表格数据');
return;
}
final rows = sheet.rows;
if (rows.length < 2) {
print('❌ 表格中没有有效内容');
return;
}
final headers = rows[0].map((cell) => cell?.value?.toString() ?? '').toList();
final langIndices = <String, int>{};
// 获取语言列索引
for (var i = 1; i < headers.length; i++) {
final langCode = headers[i];
if (langCode.isNotEmpty) {
langIndices[langCode] = i;
}
}
// 每种语言的 key-value map
final Map<String, Map<String, String>> langMaps = {
for (final lang in langIndices.keys) lang: {}
};
for (var r = 1; r < rows.length; r++) {
final row = rows[r];
final key = row[0]?.value?.toString() ?? '';
if (key.isEmpty) continue;
for (final entry in langIndices.entries) {
final lang = entry.key;
final col = entry.value;
final value = col < row.length ? row[col]?.value?.toString() ?? '' : '';
if (value.isNotEmpty) {
langMaps[lang]![] = value;
}
}
}
// 写入 arb 文件
final outputDir = Directory('output/l10n');
if (!outputDir.existsSync()) {
outputDir.createSync(recursive: true);
}
for (final entry in langMaps.entries) {
final lang = entry.key;
final data = entry.value;
final fileName = 'app_$lang.arb';
final filePath = p.join(outputDir.path, fileName);
final arbContent = const JsonEncoder.withIndent(' ').convert(data);
await File(filePath).writeAsString(arbContent, encoding: utf8);
print('✅ 已生成 $fileName (${data.length} 条)');
}
}
尾声
工作之后,维护开源项目的时间骤减,不过 FlutterUnit 仍在不断完善。目前已经升级到了 flutter 最新版 3.32.0 。 现在 FlutterUnit 坐拥前后台,有什么好的想法或者建议欢迎提出。那本文就到这里,下次再见 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。