
做 Web 开发助手的时候,"文本统计"是很常见的一个小工具:复制一段日志、粘贴一段文案,就能马上看到字符数、行数、段落数,顺手还能做一些清洗。
这一节我把实现拆成UI + 统计逻辑 两块:UI 负责交互和展示,统计逻辑单独放到 utils 里,后续无论你要做"关键词高亮""敏感词检测"都比较好接。
目标与口径
这里先把统计口径说清楚,不然后面很容易对不上:
- 字符数 :
text.length,包含空格、换行。 - 无空白字符数:把所有空白(空格/换行/制表符等)去掉再计数。
- 单词数:以连续空白分隔(适合英文/代码)。中文严格意义没有"单词",这里不做分词,保持工具定位简单。
- 行数 :按
\n统计。 - 段落数:按"空行分隔"统计(两个换行或换行+空白+换行)。
1)先把统计逻辑抽出来
lib/utils/text_stats.dart
dart
class TextStats {
final int charCount;
final int charNoSpaceCount;
final int wordCount;
final int lineCount;
final int paragraphCount;
const TextStats({
required this.charCount,
required this.charNoSpaceCount,
required this.wordCount,
required this.lineCount,
required this.paragraphCount,
});
}
说明
把统计结果做成一个不可变的 TextStats,好处挺实在:
- 状态更干净 :页面只需要保存一个对象,不用维护一堆
int。 - 更容易测试:后面你可以单独写测试用例喂各种字符串。
- 扩展简单:要加"中文标点数""数字个数",只是在这个类里多一个字段。
lib/utils/text_stats.dart
dart
class TextStatsCalculator {
static TextStats calc(String text) {
final trimmed = text.trim();
final charCount = text.length;
final charNoSpaceCount = text.replaceAll(RegExp(r'\s'), '').length;
final wordCount = trimmed.isEmpty
? 0
: trimmed.split(RegExp(r'\s+')).where((e) => e.isNotEmpty).length;
final lineCount = text.isEmpty ? 0 : text.split('\n').length;
final paragraphCount = trimmed.isEmpty
? 0
: trimmed.split(RegExp(r'\n\s*\n')).where((e) => e.trim().isNotEmpty).length;
return TextStats(
charCount: charCount,
charNoSpaceCount: charNoSpaceCount,
wordCount: wordCount,
lineCount: lineCount,
paragraphCount: paragraphCount,
);
}
}
说明
这里有几个小细节是实战里经常踩的:
- 先保存
trimmed:避免反复trim(),也把"只有空格"的情况统一处理。 where((e) => e.isNotEmpty):有时候文本里会出现多个空格/制表符,直接split可能会产生空字符串。- 段落统计用
\n\s*\n:空行中间可能夹着空格,不处理会导致段落数偏大。
2)页面骨架:输入区 + 结果区
lib/pages/text_stats_page.dart
dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../utils/text_stats.dart';
class TextStatsPage extends StatefulWidget {
const TextStatsPage({super.key});
@override
State<TextStatsPage> createState() => _TextStatsPageState();
}
说明
入口这里我只保留最关键的东西:
ScreenUtil:做 Web/多端时字体、间距适配更省心。utils引入:统计逻辑不写在页面里,后面改规则不会影响 UI。
lib/pages/text_stats_page.dart
dart
class _TextStatsPageState extends State<TextStatsPage> {
final _controller = TextEditingController();
TextStats _stats = const TextStats(
charCount: 0,
charNoSpaceCount: 0,
wordCount: 0,
lineCount: 0,
paragraphCount: 0,
);
@override
void initState() {
super.initState();
_controller.text = 'Flutter 是 Google 开发的开源 UI 框架。\n\n粘贴一段文本试试。';
_recalculate();
}
void _recalculate() {
setState(() => _stats = TextStatsCalculator.calc(_controller.text));
}
}
说明
我一般会把页面状态收敛成一个对象:
_stats初始值明确 :避免 build 过程中出现null或 "闪一下"。_recalculate()只做一件事 :把当前文本丢给TextStatsCalculator,拿结果更新 UI。
3)输入框:不花哨,但要好用
lib/pages/text_stats_page.dart
dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('文本统计')),
body: Column(
children: [
Expanded(
child: TextField(
controller: _controller,
maxLines: null,
expands: true,
style: TextStyle(fontSize: 14.sp),
decoration: InputDecoration(
hintText: '输入或粘贴文本...',
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.w),
),
onChanged: (_) => _recalculate(),
),
),
_StatsPanel(stats: _stats),
],
),
);
}
说明
这个输入区用的是我在工具类 App 里最常用的写法:
maxLines: null + expands: true:输入框自动撑满剩余空间,体验比滚动小框好很多。onChanged实时更新:这种统计类工具就应该"边输边变"。- 把结果面板抽出去 :页面
build不会越来越臃肿。
4)统计面板:展示要清晰
lib/pages/text_stats_page.dart
dart
class _StatsPanel extends StatelessWidget {
final TextStats stats;
const _StatsPanel({required this.stats});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.w),
color: Colors.grey[100],
child: Column(
children: [
Row(children: [
_StatItem(label: '字符数', value: stats.charCount, icon: Icons.text_fields),
_StatItem(label: '无空白', value: stats.charNoSpaceCount, icon: Icons.space_bar),
]),
SizedBox(height: 12.h),
Row(children: [
_StatItem(label: '单词数', value: stats.wordCount, icon: Icons.article),
_StatItem(label: '行数', value: stats.lineCount, icon: Icons.format_list_numbered),
]),
],
),
);
}
}
说明
这里看起来"就是摆控件",但抽一层 _StatsPanel 有两个现实收益:
- 方便复用:你后面要做"统计结果悬浮显示""侧边栏显示",直接复用这个组件。
- 避免在页面里堆 UI:维护起来轻松很多。
lib/pages/text_stats_page.dart
dart
class _StatItem extends StatelessWidget {
final String label;
final int value;
final IconData icon;
const _StatItem({required this.label, required this.value, required this.icon});
@override
Widget build(BuildContext context) {
return Expanded(
child: Card(
child: Padding(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
Icon(icon, color: Colors.blue),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey[600])),
Text('$value', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
],
),
],
),
),
),
);
}
}
说明
_StatItem 这种组件不要嫌麻烦,真实项目里几乎都会这么拆:
- 视觉统一:所有统计项样式一致,不用每次手写一遍。
- 后续改动集中:比如想把数字换成等宽字体、加一个点击复制,都只改这一个地方。
5)一些你可能会用到的小补充
如果你准备把它放进"Web 开发助手"这种工具集合里,我建议顺手加两点(不复杂,但很实用):
- 清空按钮 :
_controller.clear()后调用_recalculate()。 - 粘贴优化:Web 端用户常常是 Ctrl+V 连续粘贴多次,统计逻辑抽出来之后,你也可以很自然地做防抖(debounce)。
这两点我没有在上面的代码里硬塞进去,原因也很简单:文章里保持核心逻辑清晰,后面你扩展的时候不容易"牵一发动全身"。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net