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 技术目标

本文围绕真实源码拆解以下内容:

  1. Flutter 应用入口和青绿色 Material 3 主题。
  2. TextEditingController 如何作为文本来源。
  3. _wordCount 如何按空白字符切分单词。
  4. _charCount_charCountNoSpaces 的区别。
  5. _sentenceCount 如何按 . ! ? 统计句子。
  6. _paragraphCount 如何按空行统计段落。
  7. _readingTimeMinutes 如何按 200 words/min 估算。
  8. _wordFrequency 如何清洗、计数、排序和截取。
  9. _buildStatCard 如何复用统计展示。
  10. 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 提供 MaterialAppScaffoldAppBarTextFieldCardListTileTextButton 等组件。

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 实时统计链路

输入变化后的链路如下:

  1. 用户输入或粘贴文本。
  2. TextField.onChanged 触发。
  3. setState 让页面重建。
  4. 各个 getter 重新读取 _controller.text
  5. 统计卡片和高频词列表刷新。

当前实现没有防抖。短文本和中等文本体验直接;超长文本场景可以考虑延迟计算或增量统计。

五、单词统计

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 步:

  1. 文本转小写。
  2. 去掉首尾空白。
  3. 按空白切分单词。
  4. 清理单词首尾非单词字符。
  5. 过滤空词和长度不大于 2 的词。
  6. 统计次数、排序、取前 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 上应重点验证:

  1. 输入框可以输入多行文本。
  2. 粘贴长文本后页面不崩溃。
  3. 输入后统计值实时变化。
  4. 软键盘弹出时页面仍可访问。
  5. 点击 Clear 后输入框和统计值归零。

13.3 统计验证

可使用如下英文文本验证:

Hello world. Hello Flutter!

Flutter makes UI fast.

预期现象:

  • Words 大于 0。
  • Characters 包含空格和换行。
  • No Spaces 小于 Characters。
  • Paragraphs 为 2。
  • Top Words 中 helloflutter 的计数更高。

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 演进为更实用的跨平台写作辅助应用。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐