Flutter三方库适配OpenHarmony【word_counter】文本统计工具项目完整实战
Flutter三方库适配OpenHarmony【word_counter】文本统计工具项目完整实战
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
word_counter 是一个基于 Flutter 的文本统计工具项目,核心代码位于 lib/main.dart。用户在多行输入框中输入或粘贴文本后,页面会实时统计单词数、字符数、无空格字符数、句子数、段落数、阅读时间,并展示前 10 个高频词。项目没有接入分词库,统计逻辑主要基于 TextEditingController、字符串方法和 RegExp,非常适合用来讲解 Flutter 文本输入、派生状态和 OpenHarmony 表单适配。
这个项目适合学习 TextEditingController 生命周期、getter 派生统计值、onChanged 实时刷新、正则分词、Map 词频统计、排序与截取 Top 10、空状态切换 和 Material 3 工具型布局。

图片说明:本文围绕 Flutter 文本输入、正则统计和 OpenHarmony 承载工程展开,所有关键代码均来自 word_counter 的真实源码。
文本统计工具最重要的是把“统计规则”讲清楚。当前项目适合英文空格分词文本,对中文连续文本、复杂标点和 Unicode 单词边界支持有限。
一、项目背景与目标
1.1 项目定位
word_counter 是一个轻量文本分析工具。用户输入文本后,无需点击分析按钮,页面会通过 onChanged 触发重建,并用多个 getter 重新计算统计结果。它适合写作辅助、英文段落检查、阅读时间估算和高频词初步分析。
当前项目真实支持的功能包括:
- 多行文本输入,最大显示 8 行。
- 实时统计 Words。
- 实时统计 Characters。
- 实时统计 Sentences。
- 统计 No Spaces。
- 统计 Paragraphs。
- 按 200 words/min 估算 Reading。
- 展示 Top Words 高频词列表。
- 高频词统一转为小写。
- 高频词会清理单词首尾非单词字符。
- 高频词只统计长度大于 2 的词。
- 高频词按出现次数降序排列。
- 高频词最多展示前 10 个。
- 支持 Clear 清空文本。
- 空文本时显示提示文案。
1.2 技术目标
本文围绕真实源码拆解以下内容:
- Flutter 应用入口和青绿色 Material 3 主题。
TextEditingController如何作为文本来源。_wordCount如何按空白字符切分单词。_charCount和_charCountNoSpaces的区别。_sentenceCount如何按. ! ?统计句子。_paragraphCount如何按空行统计段落。_readingTimeMinutes如何按 200 words/min 估算。_wordFrequency如何清洗、计数、排序和截取。_buildStatCard如何复用统计展示。- OpenHarmony 侧如何验证输入、刷新、清空和列表展示。
1.3 核心实现速览
| 能力 | 当前实现 | 适配关注点 |
|---|---|---|
| 应用入口 | runApp(const WordCounterApp()) |
确认首屏加载 |
| 主题 | ColorScheme.fromSeed(seedColor: Colors.teal) |
确认青绿色样式 |
| 输入来源 | TextEditingController |
确认输入同步 |
| 实时刷新 | onChanged: (_) => setState(() {}) |
确认输入后统计变化 |
| 单词统计 | split(RegExp(r'\s+')) |
确认空白切分 |
| 句子统计 | split(RegExp(r'[.!?]+')) |
确认英文标点 |
| 段落统计 | split(RegExp(r'\n\s*\n')) |
确认空行分段 |
| 阅读时间 | _wordCount / 200 |
确认估算规则 |
| 高频词 | Map<String, int> |
确认词频排序 |
| 清空 | _controller.clear() |
确认状态回到空 |
二、环境准备与工程结构
2.1 工程结构
项目保持 Flutter 标准结构,同时包含 OpenHarmony 平台工程。
| 文件或目录 | 作用 |
|---|---|
lib/main.dart |
应用入口、文本控制器、统计 getter 和 UI |
pubspec.yaml |
SDK 约束、Flutter 依赖和 Material 图标配置 |
analysis_options.yaml |
Flutter lint 规则 |
test/ |
Flutter 测试目录 |
ohos/ |
OpenHarmony 平台承载工程 |
README.md |
项目说明文件 |
当前业务逻辑集中在 lib/main.dart,没有引入自然语言处理库或分词插件。
2.2 依赖配置
项目使用 Dart SDK ^3.9.2,依赖 Flutter SDK。
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
文本统计主要依赖 Dart 字符串能力和正则表达式。
2.3 常用命令
flutter pub get
flutter analyze
flutter test
flutter run
| 命令 | 用途 |
|---|---|
flutter pub get |
获取依赖 |
flutter analyze |
执行静态分析 |
flutter test |
执行测试 |
flutter run |
在目标设备运行 |
OpenHarmony 调试时,还需要结合本地 Flutter OpenHarmony 工具链完成构建、安装和运行。
三、应用入口与主题配置
3.1 import 依赖
项目只引入 Flutter Material。
import 'package:flutter/material.dart';
material.dart 提供 MaterialApp、Scaffold、AppBar、TextField、Card、ListTile、TextButton 等组件。
3.2 main 函数
入口函数启动根组件。
void main() {
runApp(const WordCounterApp());
}
3.3 WordCounterApp
根组件创建 MaterialApp。
class WordCounterApp extends StatelessWidget {
const WordCounterApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Word Counter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const WordCounterHomePage(title: 'Word Counter'),
);
}
}
这段代码包含三个关键点:
- 应用标题为
Word Counter。 - 使用
Colors.teal作为主题种子色。 - 首页为
WordCounterHomePage。
四、输入控制器与生命周期
4.1 控制器字段
页面状态中只有一个文本控制器。
final TextEditingController _controller = TextEditingController();
所有统计值都从 _controller.text 派生,不额外保存统计状态。
4.2 dispose 释放
控制器在页面销毁时释放。
void dispose() {
_controller.dispose();
super.dispose();
}
这是正确的生命周期处理方式。
4.3 输入框实现
TextField(
controller: _controller,
maxLines: 8,
decoration: InputDecoration(
hintText: 'Type or paste your text here...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
onChanged: (_) => setState(() {}),
)
maxLines: 8 让用户可以输入较长段落,onChanged 触发页面重建。
4.4 实时统计链路
输入变化后的链路如下:
- 用户输入或粘贴文本。
TextField.onChanged触发。setState让页面重建。- 各个 getter 重新读取
_controller.text。 - 统计卡片和高频词列表刷新。
当前实现没有防抖。短文本和中等文本体验直接;超长文本场景可以考虑延迟计算或增量统计。
五、单词统计
5.1 _wordCount
单词数通过 _wordCount getter 计算。
int get _wordCount {
final text = _controller.text.trim();
if (text.isEmpty) return 0;
return text.split(RegExp(r'\s+')).where((w) => w.isNotEmpty).length;
}
5.2 统计规则
该规则先 trim() 去掉首尾空白,然后按一个或多个空白字符切分。
text.split(RegExp(r'\s+'))
最后过滤空字符串。
where((w) => w.isNotEmpty)
5.3 示例
| 文本 | 结果 |
|---|---|
| 空字符串 | 0 |
hello |
1 |
hello world |
2 |
hello world |
2 |
hello\nworld |
2 |
该规则适合英文和以空格分隔的文本。
六、字符统计
6.1 _charCount
字符数直接读取文本长度。
int get _charCount => _controller.text.length;
这里包含空格、换行和标点。
6.2 _charCountNoSpaces
无空格字符数会移除所有空白字符。
int get _charCountNoSpaces {
return _controller.text.replaceAll(RegExp(r'\s'), '').length;
}
RegExp(r'\s') 会匹配空格、换行、制表符等空白字符。
6.3 对比示例
| 文本 | Characters | No Spaces |
|---|---|---|
abc |
3 | 3 |
a b c |
5 | 3 |
a\nb |
3 | 2 |
hello world |
11 | 10 |
两个指标面向不同场景:字符总数用于长度限制,无空格字符数用于内容密度估算。
七、句子和段落统计
7.1 _sentenceCount
句子数量按 . ! ? 切分。
int get _sentenceCount {
final text = _controller.text.trim();
if (text.isEmpty) return 0;
return text.split(RegExp(r'[.!?]+')).where((s) => s.trim().isNotEmpty).length;
}
7.2 句子规则示例
| 文本 | Sentences |
|---|---|
| 空字符串 | 0 |
Hello. |
1 |
Hello! How are you? |
2 |
One... Two? |
2 |
该规则主要覆盖英文句号、感叹号和问号。
7.3 _paragraphCount
段落数量按空行切分。
int get _paragraphCount {
final text = _controller.text.trim();
if (text.isEmpty) return 0;
return text.split(RegExp(r'\n\s*\n')).where((p) => p.trim().isNotEmpty).length;
}
7.4 段落规则示例
| 文本结构 | Paragraphs |
|---|---|
| 空字符串 | 0 |
| 单段文本 | 1 |
| 两段中间一个空行 | 2 |
| 多段并含空白行 | 按非空段统计 |
段落统计依赖空行,因此连续换行是关键分隔符。
八、阅读时间估算
8.1 _readingTimeMinutes
阅读时间通过单词数除以 200。
double get _readingTimeMinutes {
return _wordCount / 200;
}
源码注释说明平均阅读速度为 200 words per minute。
8.2 展示格式
阅读时间显示一位小数。
'${_readingTimeMinutes.toStringAsFixed(1)} min'
8.3 示例
| Words | Reading |
|---|---|
| 0 | 0.0 min |
| 100 | 0.5 min |
| 200 | 1.0 min |
| 450 | 2.3 min |
这是估算值,不是精确阅读耗时。
九、高频词统计
9.1 _wordFrequency
高频词通过 _wordFrequency getter 计算。
Map<String, int> get _wordFrequency {
final text = _controller.text.toLowerCase().trim();
if (text.isEmpty) return {};
final words = text.split(RegExp(r'\s+'));
final frequency = <String, int>{};
for (final word in words) {
final cleaned = word.replaceAll(RegExp(r'^[^\w]+|[^\w]+$'), '');
if (cleaned.isNotEmpty && cleaned.length > 2) {
frequency[cleaned] = (frequency[cleaned] ?? 0) + 1;
}
}
final sorted = frequency.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return Map.fromEntries(sorted.take(10));
}
9.2 处理流程
高频词统计分为 6 步:
- 文本转小写。
- 去掉首尾空白。
- 按空白切分单词。
- 清理单词首尾非单词字符。
- 过滤空词和长度不大于 2 的词。
- 统计次数、排序、取前 10。
9.3 清理规则
word.replaceAll(RegExp(r'^[^\w]+|[^\w]+$'), '')
该正则会移除单词首尾的非单词字符,例如英文逗号、句号和引号。
9.4 词频排序
final sorted = frequency.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return Map.fromEntries(sorted.take(10));
排序按出现次数从高到低,最终最多返回 10 个词。
十、统计卡片 UI
10.1 卡片结构
统计区域使用 Card 包裹两行指标。
Card(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('Words', _wordCount.toString(), Icons.text_fields),
_buildStatCard('Characters', _charCount.toString(), Icons.abc),
_buildStatCard('Sentences', _sentenceCount.toString(), Icons.short_text),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('No Spaces', _charCountNoSpaces.toString(), Icons.block),
_buildStatCard('Paragraphs', _paragraphCount.toString(), Icons.format_align_left),
_buildStatCard('Reading', '${_readingTimeMinutes.toStringAsFixed(1)} min', Icons.timer),
],
),
],
),
),
)
10.2 六个指标
| 指标 | 数据来源 | 图标 |
|---|---|---|
| Words | _wordCount |
Icons.text_fields |
| Characters | _charCount |
Icons.abc |
| Sentences | _sentenceCount |
Icons.short_text |
| No Spaces | _charCountNoSpaces |
Icons.block |
| Paragraphs | _paragraphCount |
Icons.format_align_left |
| Reading | _readingTimeMinutes |
Icons.timer |
10.3 _buildStatCard
统计项被封装为 _buildStatCard。
Widget _buildStatCard(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, color: Colors.teal),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
);
}
复用方法让 6 个统计项保持一致的视觉结构。
十一、Top Words 列表
11.1 标题与清空按钮
Top Words 区域上方有标题和 Clear 按钮。
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Top Words',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
TextButton.icon(
onPressed: () {
_controller.clear();
setState(() {});
},
icon: const Icon(Icons.clear),
label: const Text('Clear'),
),
],
)
点击 Clear 后,输入框清空并触发重建。
11.2 空状态
没有高频词时显示提示。
_wordFrequency.isEmpty
? const Center(
child: Text(
'Enter text to see word frequency',
style: TextStyle(color: Colors.grey),
),
)
: ListView(...)
空文本或没有长度大于 2 的词时,都会进入空状态。
11.3 高频词列表
有结果时使用 ListView 展示。
ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: _wordFrequency.entries.map((entry) {
return ListTile(
title: Text(entry.key),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.teal.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(entry.value.toString()),
),
);
}).toList(),
)
右侧胶囊样式展示出现次数。
十二、页面布局
12.1 Scaffold 结构
页面使用 Scaffold。
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column(
children: [
// 输入框
// 统计卡片
// Top Words 标题
// 高频词列表
],
),
);
12.2 主体区域
主体使用纵向 Column。
| 顺序 | 区域 | 作用 |
|---|---|---|
| 1 | 文本输入框 | 输入或粘贴文本 |
| 2 | 统计卡片 | 展示 6 个统计值 |
| 3 | Top Words 标题 | 标识高频词区域 |
| 4 | Expanded 列表 | 展示高频词或空状态 |
12.3 Expanded 的作用
Expanded(
child: _wordFrequency.isEmpty
? const Center(...)
: ListView(...),
)
Expanded 让高频词列表占据剩余空间,避免在不同屏幕高度下布局溢出。
十三、OpenHarmony 适配要点
13.1 基础组件验证
当前项目使用的 Flutter 组件包括:
| 组件 | 作用 | OpenHarmony 关注点 |
|---|---|---|
MaterialApp |
应用根组件 | 首屏加载 |
Scaffold |
页面骨架 | AppBar 与 Body |
TextField |
多行文本输入 | 输入、粘贴、软键盘 |
Card |
统计区域 | 圆角、间距 |
Row |
统计项排列 | 小屏显示 |
TextButton.icon |
清空按钮 | 点击响应 |
Expanded |
列表区域 | 高度分配 |
ListView |
高频词列表 | 滚动 |
ListTile |
高频词行 | 文本和计数 |
13.2 输入验证
OpenHarmony 上应重点验证:
- 输入框可以输入多行文本。
- 粘贴长文本后页面不崩溃。
- 输入后统计值实时变化。
- 软键盘弹出时页面仍可访问。
- 点击 Clear 后输入框和统计值归零。
13.3 统计验证
可使用如下英文文本验证:
Hello world. Hello Flutter!
Flutter makes UI fast.
预期现象:
- Words 大于 0。
- Characters 包含空格和换行。
- No Spaces 小于 Characters。
- Paragraphs 为 2。
- Top Words 中
hello和flutter的计数更高。
13.4 边界验证
需要注意以下边界:
- 中文连续文本不会按词语自然分词。
- emoji 和复杂 Unicode 字符的长度统计可能和用户感知不同。
- 缩写、连字符和撇号词的处理比较简单。
- 句子统计主要看
. ! ?。 - 段落统计依赖空行。
文本工具的适配重点是规则一致性。只要统计规则明确,结果就能解释;如果要支持中文分词,需要引入更专业的分词方案。
十四、测试与验证
14.1 初始页面测试
Widget 测试可以验证首屏结构。
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('word counter shows initial widgets', (tester) async {
await tester.pumpWidget(const WordCounterApp());
expect(find.text('Word Counter'), findsWidgets);
expect(find.text('Words'), findsOneWidget);
expect(find.text('Characters'), findsOneWidget);
expect(find.text('Sentences'), findsOneWidget);
expect(find.text('Top Words'), findsOneWidget);
});
}
14.2 输入统计测试
可以输入简单英文文本后验证单词。
testWidgets('word count updates after input', (tester) async {
await tester.pumpWidget(const WordCounterApp());
await tester.enterText(find.byType(TextField), 'hello world');
await tester.pump();
expect(find.text('2'), findsWidgets);
});
实际测试时可以收窄查找范围,避免不同统计项数字重复造成误判。
14.3 清空按钮测试
testWidgets('clear button resets input', (tester) async {
await tester.pumpWidget(const WordCounterApp());
await tester.enterText(find.byType(TextField), 'hello world');
await tester.pump();
await tester.tap(find.text('Clear'));
await tester.pump();
final field = tester.widget<TextField>(find.byType(TextField));
expect(field.controller?.text, '');
});
14.4 手工验证矩阵
| 场景 | 操作 | 预期 |
|---|---|---|
| 首次打开 | 启动应用 | 所有统计为 0 |
| 输入英文 | 输入 hello world |
Words 为 2 |
| 输入句子 | 输入 Hi. OK? |
Sentences 为 2 |
| 输入段落 | 两段之间空一行 | Paragraphs 为 2 |
| 高频词 | 重复输入同一词 | Top Words 计数增加 |
| 清空 | 点击 Clear | 输入框清空,统计归零 |
| 中文文本 | 输入连续中文 | 单词统计不会自然分词 |
十五、常见问题与优化建议
15.1 为什么所有统计都用 getter
统计值全部从 _controller.text 派生。
int get _charCount => _controller.text.length;
这样不用维护多份状态,输入变化后重建页面即可得到最新结果。
15.2 为什么单词统计对中文不友好
当前单词统计按空白字符切分。
text.split(RegExp(r'\s+'))
中文文本通常没有空格分词,因此不能得到自然语言意义上的词数。
15.3 为什么高频词过滤长度小于等于 2 的词
源码中有这一条件:
if (cleaned.isNotEmpty && cleaned.length > 2)
这会过滤掉 a、an、to、of、in 等短词,也会过滤很多有意义的短词。它适合简单英文高频词统计,但不是完整 NLP 规则。
15.4 为什么阅读时间是估算
阅读速度因语言、内容难度和读者不同而变化。当前固定用 200 words/min。
return _wordCount / 200;
如果要更灵活,可以把阅读速度做成可配置项。
15.5 如何支持中文分词
需要引入更专业的中文分词能力,或在输入规则上要求用空格分词。
abstract class Tokenizer {
List<String> tokenize(String text);
}
然后为英文、中文分别实现不同 tokenizer。
15.6 如何优化超长文本性能
当前每次输入都会重新计算所有 getter。对于超长文本,可以增加防抖或缓存。
class TextStats {
final int words;
final int characters;
final int sentences;
const TextStats({
required this.words,
required this.characters,
required this.sentences,
});
}
把统计结果集中计算一次,再传给 UI。
十六、工程扩展方向
16.1 抽取统计函数
可以把统计逻辑抽成纯函数。
int countWords(String text) {
final trimmed = text.trim();
if (trimmed.isEmpty) return 0;
return trimmed.split(RegExp(r'\s+')).where((w) => w.isNotEmpty).length;
}
纯函数更容易测试。
16.2 建模统计结果
可以定义统一统计结果对象。
class WordStats {
final int words;
final int characters;
final int charactersNoSpaces;
final int sentences;
final int paragraphs;
final double readingMinutes;
const WordStats({
required this.words,
required this.characters,
required this.charactersNoSpaces,
required this.sentences,
required this.paragraphs,
required this.readingMinutes,
});
}
页面只负责展示 WordStats。
16.3 增加导出功能
可以把统计结果导出为文本。
String exportStats(WordStats stats) {
return '''
Words: ${stats.words}
Characters: ${stats.characters}
Sentences: ${stats.sentences}
Paragraphs: ${stats.paragraphs}
''';
}
导出功能适合写作工具或内容审核辅助场景。
16.4 增加停用词过滤
高频词统计可以排除常见停用词。
const stopWords = {'the', 'and', 'for', 'with', 'that'};
统计时跳过这些词,结果会更接近主题词。
十七、相关链接与延伸阅读
17.1 Flutter 官方资料
| 资料 | 链接 |
|---|---|
| Flutter 官方文档 | https://docs.flutter.dev/ |
| Flutter API 文档 | https://api.flutter.dev/ |
| TextField | https://api.flutter.dev/flutter/material/TextField-class.html |
| TextEditingController | https://api.flutter.dev/flutter/widgets/TextEditingController-class.html |
| Card | https://api.flutter.dev/flutter/material/Card-class.html |
| ListView | https://api.flutter.dev/flutter/widgets/ListView-class.html |
| ListTile | https://api.flutter.dev/flutter/material/ListTile-class.html |
| TextButton | https://api.flutter.dev/flutter/material/TextButton-class.html |
17.2 Dart 与 OpenHarmony 资料
| 资料 | 链接 |
|---|---|
| Dart 官方文档 | https://dart.dev/ |
| Dart API 文档 | https://api.dart.dev/ |
| RegExp API | https://api.dart.dev/stable/dart-core/RegExp-class.html |
| String API | https://api.dart.dev/stable/dart-core/String-class.html |
| Map API | https://api.dart.dev/stable/dart-core/Map-class.html |
| Iterable API | https://api.dart.dev/stable/dart-core/Iterable-class.html |
| pub.dev | https://pub.dev/ |
| OpenHarmony docs | https://github.com/openharmony/docs |
| 开源鸿蒙跨平台社区 | https://openharmonycrossplatform.csdn.net |
总结
word_counter 是一个非常适合学习 Flutter 文本输入和派生统计的工具项目。它用 TextEditingController 作为唯一文本来源,通过多个 getter 实时计算单词数、字符数、无空格字符数、句子数、段落数、阅读时间和高频词,再用统计卡片和 Top Words 列表展示结果。项目结构简单,但覆盖了正则、Map、排序、空状态、清空按钮和实时刷新这些实用能力。
从 OpenHarmony 适配角度看,这个项目适合验证 Flutter 多行输入、粘贴文本、软键盘、实时 setState、卡片布局、列表滚动、清空按钮和长文本展示。排查路径也很明确:单词数不对看空白切分规则,句子数不对看标点正则,段落数不对看空行规则,高频词不对看清洗和长度过滤。
掌握这个项目后,可以继续扩展中文分词、停用词过滤、统计结果导出、可配置阅读速度、关键词高亮和更完整的文本分析模型,让文本统计工具从简单 Demo 演进为更实用的跨平台写作辅助应用。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)