javascript基础从小白到高手系列二十二:Request、Response 及Body 混入
字数统计工具技术解析文档
一、项目背景概述
本项目是一个基于 Flutter 框架开发的字数统计工具应用,专为 OpenHarmony 平台设计。应用的核心功能是对用户输入的文本进行实时字数统计,包括字符数和单词数两个维度的统计,并提供详细的统计信息展示。
从产品定位来看,这是一个轻量级文本分析工具,面向需要统计文本字数的各类用户,如写作者、编辑、学生、程序员等。在日常工作和学习中,经常需要统计文章、报告、作业的字数,本应用提供了一个便捷的实时统计方案,用户只需输入或粘贴文本,即可立即看到统计结果。
技术层面,应用采用 Flutter 的 Material Design 3 设计规范,使用深紫色作为主题色。应用界面分为输入区和统计结果区两大部分,统计实时更新,无需点击按钮。结果展示采用卡片式布局,直观清晰,点击卡片还可以展开查看更详细的统计信息。
应用的核心特点是实时统计,用户输入文本时统计数据会即时更新,无需手动触发。这种设计提供了流畅的用户体验,用户可以边输入边观察字数变化。统计功能包括字符数和单词数,还提供了文本密度等扩展统计指标。
二、架构分析
2.1 整体架构图
2.2 实时统计流程图
2.3 架构层次说明
应用采用简洁的两层组件架构,结构清晰,易于理解和维护:
第一层:应用根层级
负责应用的全局配置,包括主题、标题等。使用 MaterialApp 作为根组件,提供 Material Design 风格的视觉规范。首页使用 Scaffold 提供基础的页面结构,包括应用栏和可滚动的主体内容。这一层只负责框架搭建,不涉及具体的业务逻辑。
第二层:功能核心层
字数统计核心组件封装了全部的统计功能,包括文本输入、实时统计、结果展示等。这是一个自包含的状态型组件,内部管理文本控制器和统计数据。组件内部按照功能划分为三个区域:文本输入区域、统计结果区域和操作提示区域。
这种集中式的设计适合功能相对单一的工具类应用,所有相关逻辑都在一个组件内,便于理解和维护。实时统计的特性使得状态更新更加频繁,但得益于 Flutter 高效的重建机制,性能依然出色。
三、入口组件流程
3.1 应用启动流程
应用的入口函数 main 调用 runApp 方法启动应用。根组件是一个无状态组件,返回 MaterialApp 实例,配置了应用标题、深紫色主题和 Material 3 支持。根组件中保留了较多的注释说明,介绍了 Flutter 热重载等开发特性,体现了这是一个教学或示例性质的项目。
首页组件是一个状态型组件,但其状态类非常简洁,构建方法只返回一个包含字数统计组件的 Scaffold。应用栏标题为"字数统计工具",采用深紫色背景和白色前景色。
主体内容使用 SingleChildScrollView 包裹,确保在小屏幕设备上内容可以滚动查看,避免溢出问题。内部是一个垂直列布局,只包含一个字数统计核心组件。
3.2 组件职责划分
首页组件:负责页面框架的搭建,提供应用栏和基础布局结构。本身不持有业务状态,只作为容器存在。代码简洁,职责单一。
字数统计核心组件:负责全部的字数统计业务逻辑,包括文本输入管理、实时统计计算、结果展示等。是一个自包含的功能模块,几乎所有的业务代码都在这个组件中。
这种职责划分使得首页组件非常轻量,核心功能集中在一个组件中,对于简单的工具应用来说是合理的设计选择。
四、核心组件逐段解析
4.1 字数统计核心组件
字数统计核心组件是整个应用的主体,是一个状态型组件,封装了所有统计相关的功能。
状态变量:
- 文本控制器:管理输入文本框的内容,是一个 TextEditingController 对象
- 字符数统计:记录当前文本的字符总数
- 单词数统计:记录当前文本的单词总数
- 显示详情标志:控制详细统计信息的展开与收起
文本控制器用于管理用户输入,同时也是统计数据的来源。字符数和单词数是两个主要的统计指标,实时更新。显示详情标志是一个交互状态,控制详细面板的显示与隐藏。
核心方法:
统计更新方法是组件的核心方法,在文本发生变化时被调用:
- 从文本控制器获取当前文本内容
- 计算字符数,直接使用字符串的 length 属性
- 计算单词数,使用正则表达式按空白字符分割,然后过滤空字符串
- 调用 setState 更新字符数和单词数状态
清空方法用于清空文本和重置统计数据,将所有状态恢复到初始值。
切换详情方法用于切换详细统计面板的显示状态,点击统计卡片时触发。
生命周期管理:
组件在 dispose 方法中释放文本控制器的资源,确保内存安全。这是 Flutter 中使用控制器的标准最佳实践。
4.2 文本输入区域
文本输入区域包含输入文本框、实时统计标识和清空按钮三个部分。
输入文本框:
使用 TextField 组件,最多显示6行,内容超出时可滚动。没有边框,通过外部容器的边框来实现视觉边界。设置了占位提示文字"请输入要统计的文本…",引导用户输入。
关键的是,输入框配置了 onChanged 回调,每当文本发生变化时就调用统计更新方法。这是实现实时统计的核心机制,用户每输入一个字符都会触发一次统计更新。
实时统计标识:
显示在输入区域左下角,文字为"实时统计",颜色为灰色,字号较小。这个标识告诉用户统计是实时进行的,不需要手动点击按钮。
清空按钮:
使用 ElevatedButton.icon 图标按钮,浅灰色背景,深紫色文字。点击后清空文本内容和统计数据。清空按钮提供了快速重置的功能,方便用户开始新的统计任务。
4.3 统计结果区域
统计结果区域是应用的核心展示区域,使用浅紫色背景的容器包裹,内部包含两个统计卡片和一个可选的详细统计面板。
字符数卡片:
使用 GestureDetector 包裹,点击可以切换详情面板的显示。卡片为白色背景,带有圆角和轻微阴影。内部垂直排列,上方是"字符数"标签,下方是大号的数字。深紫色的标签和黑色的数字形成清晰的层级。
单词数卡片:
与字符数卡片结构相同,显示单词数统计。两个卡片并排排列,均匀分布在可用空间中。
卡片的可点击设计是一个很好的交互细节,用户可以通过点击卡片来展开或收起详细信息,增加了界面的交互深度。
详细统计面板:
当显示详情标志为 true 时显示,包含更详细的统计信息:
- 字符数(含空格):与卡片上的数字一致,再次确认
- 单词数:同样与卡片一致
- 文本密度:单词数占字符数的百分比,保留两位小数
详细面板提供了更丰富的统计信息,满足有进阶需求的用户。文本密度是一个有趣的指标,可以反映文本的"稀疏"程度。
4.4 操作提示区域
操作提示区域位于组件底部,使用浅蓝色背景的容器包裹,包含操作提示文字。提示以列表形式列出了4条操作说明,帮助用户了解工具的各项功能。
浅蓝色背景与深紫色主题形成对比,同时保持了柔和的视觉效果。提示内容涵盖了实时统计、详情查看、字符数统计规则和单词数统计规则等关键信息,对于新用户非常有帮助。
五、状态管理
5.1 状态架构
本应用采用组件内状态管理方案,所有状态都集中在字数统计核心组件内部管理。这种方案对于功能单一的工具类应用来说简单直接,不需要引入额外的状态管理库。
状态分类:
输入状态:
- 文本控制器:管理用户输入的文本内容
- 这是一个有状态的对象,内部维护着文本的变化
统计状态:
- 字符数:统计得到的字符总数
- 单词数:统计得到的单词总数
交互状态:
- 显示详情标志:控制详细面板的展开与收起
5.2 状态更新流程
状态更新主要发生在以下几个时机:
用户输入时:这是最频繁的状态更新时机。每当用户输入或删除文字时,onChanged 回调被触发,调用统计更新方法,重新计算字符数和单词数,并通过 setState 更新状态。由于 Flutter 的重建机制非常高效,即使每次按键都触发重建,界面依然流畅。
点击清空时:清空文本控制器的内容,同时将字符数和单词数重置为0。这是一个全量重置操作。
点击统计卡片时:切换显示详情标志的布尔值,控制详细面板的显示与隐藏。这是一个纯 UI 状态的切换。
5.3 实时更新机制
实时统计是本应用的核心特性,其实现依赖于 TextField 的 onChanged 回调:
- 用户在文本框中输入内容
- 文本控制器的内容发生变化
- TextField 触发 onChanged 回调
- 回调函数调用统计更新方法
- 统计方法重新计算字符数和单词数
- 调用 setState 更新状态
- Flutter 框架检测到状态变化,重新构建组件
- 界面显示最新的统计数据
这个过程在用户每次按键时都会发生,由于 Flutter 的渲染性能优秀,用户感觉不到任何延迟,统计数据始终与输入内容保持同步。
六、关键代码详解
6.1 字符数统计
字符数统计非常简单直接,使用字符串的 length 属性即可:
_characterCount = text.length;
Dart 中字符串的 length 属性返回字符串中 UTF-16 代码单元的数量。对于大多数常用字符(包括中文、英文、数字等),每个字符对应一个代码单元,所以 length 可以准确反映字符数量。但对于某些特殊字符(如 emoji、生僻汉字等),可能需要更多的代码单元,这时 length 可能不等于用户感知的"字符数"。不过对于大多数使用场景,这种计算方式已经足够准确。
字符数统计包括所有字符,即包括空格和标点符号。这也是为什么提示中特意说明"字符数包括空格和标点符号",让用户明确统计规则。
6.2 单词数统计
单词数统计相对复杂一些,涉及字符串分割和过滤:
final words = text.split(RegExp(r'\s+')).where((word) => word.isNotEmpty).toList();
_wordCount = words.length;
算法步骤解析:
- 使用正则表达式 \s+ 作为分隔符分割文本。\s 匹配任意空白字符(空格、制表符、换行符等),+ 表示匹配一个或多个。这样可以处理连续的多个空白字符。
- 使用 where 方法过滤掉空字符串。为什么会有空字符串呢?因为如果文本以空白字符开头或结尾,split 方法会在开头或结尾产生空字符串。
- 将过滤后的结果转换为列表,取其长度作为单词数。
这种单词统计方式是基于空格分隔的,对于英文等以空格分隔单词的语言比较准确。但对于中文等没有空格分隔的语言,这种统计方式就不太准确了,整个中文句子可能会被算作一个"单词"。这也是提示中说明"单词数按空格分隔统计"的原因。
6.3 文本密度计算
详细统计面板中有一个有趣的指标:文本密度。它的计算方式是单词数除以字符数再乘以100,以百分比形式表示,保留两位小数。
(_wordCount / _characterCount * 100).toStringAsFixed(2)
文本密度反映了文本中"单词"的密集程度。密度越高,说明平均每个单词的字符数越少;密度越低,说明平均每个单词越长。对于英文文本来说,这个指标有一定的参考价值。对于中文文本来说,由于单词统计方式的限制,这个指标的意义相对有限。
toStringAsFixed 方法用于将数字格式化为固定小数位数的字符串,这里保留两位小数,使显示更加规范。
6.4 手势交互与状态切换
统计卡片使用 GestureDetector 包裹,实现了点击切换详情的功能。GestureDetector 是 Flutter 中处理手势的强大组件,可以检测各种手势事件。
这里使用的是 onTap 回调,即点击事件。用户点击卡片时,调用 _toggleDetails 方法,该方法通过 setState 将 _showDetails 变量取反,实现详情面板的展开与收起。
这种交互模式很常见,也很直观。卡片看起来像是可以点击的,用户自然会尝试点击,点击后有内容展开,符合用户的预期。
七、技术总结
7.1 技术亮点
实时统计机制:通过 TextField 的 onChanged 回调实现实时统计,用户输入时统计数据即时更新,无需手动触发。这种设计提供了流畅自然的用户体验,是现代工具类应用的标配功能。
清晰的信息层级:统计结果分为概览卡片和详细面板两个层级。默认显示简洁的卡片统计,点击后展开更多详情。这种渐进式信息展示方式既满足了快速查看的需求,又提供了深入了解的途径。
卡片式交互设计:统计卡片不仅用于展示信息,还可以点击交互,切换详情面板的显示。这种设计增加了界面的交互深度,同时保持了界面的简洁性。
完善的用户引导:操作提示区域详细说明了各项功能的使用方法和统计规则,帮助用户正确理解和使用工具。特别是说明了字符数和单词数的统计规则,避免了用户的误解。
7.2 可优化点
更多统计维度:可以增加更多的统计指标,如行数、段落数、中文字符数、英文字符数、数字字符数、标点符号数等,丰富统计功能。
中文分词支持:当前的单词统计是基于空格分隔的,对中文不太友好。可以集成分词库,实现中文词汇的准确统计。
统计历史记录:保存用户的统计历史,方便对比和回顾。
导出功能:支持将统计结果导出为文本或图片,方便用户分享或保存。
性能优化:对于超长篇文本,可以考虑使用防抖(debounce)技术,避免每次按键都触发统计,提升性能。
多语言支持:针对不同语言提供更准确的统计方式,如中文按字/词统计、英文按词统计等。
7.3 技术价值
本项目展示了如何使用 Flutter 开发一个实时交互的文本统计工具,涉及文本输入处理、实时状态更新、手势交互、数据计算等多个方面。对于学习 Flutter 的开发者来说,这是一个很好的实战练习项目。
特别是实时统计的实现方式,展示了 Flutter 响应式编程的核心理念:状态驱动界面,状态变化自动反映到界面上。onChanged 回调配合 setState 的模式是 Flutter 中最基础也是最重要的模式之一,理解这个模式对于掌握 Flutter 开发至关重要。
项目针对 OpenHarmony 平台的适配也展示了 Flutter 跨平台开发的能力,一套代码可以在不同平台上运行,为开发者提供了极大的便利。在 OpenHarmony 生态快速发展的今天,Flutter 为应用开发者提供了一个高效的跨平台开发选择。

Flutter for OpenHarmony 实战:字数统计 - 统计字符、单词数量
目录
前言:跨生态开发的新机遇
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。
Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。
不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。
无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。
混合工程结构深度解析
项目目录架构
当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:
my_flutter_harmony_app/
├── lib/ # Flutter业务代码(基本不变)
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 首页
│ └── utils/
│ └── platform_utils.dart # 平台工具类
├── pubspec.yaml # Flutter依赖配置
├── ohos/ # 鸿蒙原生层(核心适配区)
│ ├── entry/ # 主模块
│ │ └── src/main/
│ │ ├── ets/ # ArkTS代码
│ │ │ ├── MainAbility/
│ │ │ │ ├── MainAbility.ts # 主Ability
│ │ │ │ └── MainAbilityContext.ts
│ │ │ └── pages/
│ │ │ ├── Index.ets # 主页面
│ │ │ └── Splash.ets # 启动页
│ │ ├── resources/ # 鸿蒙资源文件
│ │ │ ├── base/
│ │ │ │ ├── element/ # 字符串等
│ │ │ │ ├── media/ # 图片资源
│ │ │ │ └── profile/ # 配置文件
│ │ │ └── en_US/ # 英文资源
│ │ └── config.json # 应用核心配置
│ ├── ohos_test/ # 测试模块
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 鸿蒙依赖管理
└── README.md
展示效果图片
flutter 实时预览 效果展示
运行到鸿蒙虚拟设备中效果展示
功能代码实现
项目结构调整
在本次开发中,我们对项目结构进行了调整,创建了专门的组件目录来存放字数统计功能的实现:
lib/
├── main.dart # 应用入口
└── components/
└── text_counter.dart # 字数统计组件
TextCounter 组件开发
组件结构设计
TextCounter 是本次开发的核心组件,负责实现文本输入、字数统计和结果展示的完整流程。该组件采用了 StatefulWidget 来管理状态,确保用户输入能够实时反映到统计结果上。
核心功能实现
以下是 TextCounter 组件的核心代码实现:
import 'package:flutter/material.dart';
class TextCounter extends StatefulWidget {
const TextCounter({Key? key}) : super(key: key);
_TextCounterState createState() => _TextCounterState();
}
class _TextCounterState extends State<TextCounter> {
final TextEditingController _textController = TextEditingController();
int _characterCount = 0;
int _wordCount = 0;
bool _showDetails = false;
void _updateCounts() {
final text = _textController.text;
// 统计字符数(包括空格和标点)
setState(() {
_characterCount = text.length;
// 统计单词数(以空格分隔)
if (text.isEmpty) {
_wordCount = 0;
} else {
// 分割文本为单词,过滤空字符串
final words = text.split(RegExp(r'\s+')).where((word) => word.isNotEmpty).toList();
_wordCount = words.length;
}
});
}
void _clearText() {
_textController.clear();
setState(() {
_characterCount = 0;
_wordCount = 0;
});
}
void _toggleDetails() {
setState(() {
_showDetails = !_showDetails;
});
}
void dispose() {
_textController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'字数统计',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 16),
// 文本输入区域
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'输入文本',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withOpacity(0.3)),
borderRadius: BorderRadius.circular(6),
),
child: TextField(
controller: _textController,
onChanged: (_) => _updateCounts(),
maxLines: 6,
decoration: const InputDecoration(
hintText: '请输入要统计的文本...',
border: InputBorder.none,
contentPadding: EdgeInsets.all(12),
),
style: const TextStyle(fontSize: 14),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'实时统计',
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
ElevatedButton.icon(
onPressed: _clearText,
icon: const Icon(Icons.clear, size: 16),
label: const Text('清空'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[100],
foregroundColor: Colors.deepPurple,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
],
),
],
),
const SizedBox(height: 20),
// 统计结果区域
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.deepPurple[50],
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 字符数统计
GestureDetector(
onTap: _toggleDetails,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
const Text(
'字符数',
style: TextStyle(
color: Colors.deepPurple,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'$_characterCount',
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
// 单词数统计
GestureDetector(
onTap: _toggleDetails,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
const Text(
'单词数',
style: TextStyle(
color: Colors.deepPurple,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'$_wordCount',
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
// 详细信息
if (_showDetails)
Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'详细统计',
style: TextStyle(
color: Colors.deepPurple,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'字符数(含空格): $_characterCount',
style: const TextStyle(
color: Colors.black87,
fontSize: 13,
),
),
Text(
'单词数: $_wordCount',
style: const TextStyle(
color: Colors.black87,
fontSize: 13,
),
),
Text(
'文本密度: ${_characterCount > 0 ? (_wordCount / _characterCount * 100).toStringAsFixed(2) : 0}%',
style: const TextStyle(
color: Colors.black87,
fontSize: 13,
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// 操作提示
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'操作提示',
style: TextStyle(
color: Colors.blue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'• 输入文本时会自动统计字数',
style: const TextStyle(
color: Colors.blue,
fontSize: 12,
),
),
Text(
'• 点击统计卡片查看详细信息',
style: const TextStyle(
color: Colors.blue,
fontSize: 12,
),
),
Text(
'• 字符数包括空格和标点符号',
style: const TextStyle(
color: Colors.blue,
fontSize: 12,
),
),
Text(
'• 单词数按空格分隔统计',
style: const TextStyle(
color: Colors.blue,
fontSize: 12,
),
),
],
),
),
],
),
);
}
}
应用入口集成
在 main.dart 文件中,我们集成了 TextCounter 组件,构建了完整的应用界面:
import 'package:flutter/material.dart';
import 'components/text_counter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter for openHarmony',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyHomePage(title: 'Flutter for openHarmony'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('字数统计工具'),
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
TextCounter(),
],
),
),
);
}
}
核心功能说明
-
实时字数统计
- 输入文本时会自动统计字符数和单词数
- 字符数包括空格和标点符号
- 单词数按空格分隔统计
-
详细统计信息
- 点击统计卡片可以查看详细统计信息
- 包括字符数、单词数和文本密度
-
用户交互
- 提供清空文本功能
- 实时更新统计结果
- 响应式布局,适配不同屏幕尺寸
-
界面设计
- 采用卡片式设计,提供清晰的视觉层次
- 合理的间距和边框,提升用户体验
- 美观的颜色搭配,符合现代设计风格
使用方法
-
启动应用
- 运行应用后,会直接进入字数统计工具的主界面
-
输入文本
- 在"输入文本"区域输入要统计的文本
- 输入过程中会自动统计字数
-
查看统计结果
- 统计结果会实时显示在"字符数"和"单词数"卡片中
- 字符数包括空格和标点符号
- 单词数按空格分隔统计
-
查看详细信息
- 点击统计卡片,可以查看详细统计信息
- 包括字符数、单词数和文本密度
-
清空文本
- 点击"清空"按钮,可以清空所有输入和统计内容
-
重新输入
- 清空后可以重新输入文本进行统计
本次开发中容易遇到的问题
1. 正则表达式使用错误
问题描述:在开发过程中,使用正则表达式分割文本时可能会遇到语法错误,特别是在处理空格分割时。
解决方案:
- 使用正确的正则表达式语法,例如
RegExp(r'\s+')来匹配一个或多个空格 - 注意在 Dart 字符串中,反斜杠需要转义,因此需要使用原始字符串(r前缀)
示例代码:
// 正确的写法
final words = text.split(RegExp(r'\s+')).where((word) => word.isNotEmpty).toList();
// 错误的写法
final words = text.split('\s+').where((word) => word.isNotEmpty).toList();
2. 文本控制器管理
问题描述:TextEditingController 如果不及时释放,可能导致内存泄漏,特别是在频繁创建和销毁组件的场景中。
解决方案:
- 在
dispose()方法中调用controller.dispose()释放资源 - 确保所有控制器都被正确管理,避免内存泄漏
示例代码:
void dispose() {
_textController.dispose();
super.dispose();
}
3. 状态更新时机
问题描述:在更新统计结果时,如果状态更新时机不正确,可能导致界面显示不一致或更新不及时。
解决方案:
- 使用
setState()方法更新状态,确保界面能够及时反映最新的统计结果 - 在文本变化时立即更新统计结果,提供实时反馈
示例代码:
void _updateCounts() {
final text = _textController.text;
setState(() {
_characterCount = text.length;
// 其他统计逻辑...
});
}
// 在TextField中使用
TextField(
controller: _textController,
onChanged: (_) => _updateCounts(),
// 其他属性...
)
4. 空文本处理
问题描述:当文本为空时,单词数统计可能会出现错误,例如除以零或返回非预期结果。
解决方案:
- 在统计单词数之前,检查文本是否为空
- 为空时直接返回 0,避免后续计算出错
示例代码:
if (text.isEmpty) {
_wordCount = 0;
} else {
final words = text.split(RegExp(r'\s+')).where((word) => word.isNotEmpty).toList();
_wordCount = words.length;
}
5. 响应式布局适配
问题描述:在不同屏幕尺寸的设备上,界面布局可能会出现问题,例如统计卡片排列不合理。
解决方案:
- 使用
SingleChildScrollView包裹内容,确保在小屏幕设备上也能正常显示 - 使用
MainAxisAlignment.spaceEvenly等布局属性,确保统计卡片能够合理排列 - 避免使用固定宽度,尽量使用相对布局
示例代码:
body: SingleChildScrollView(
child: Column(
children: <Widget>[
TextCounter(),
],
),
),
// 统计卡片布局
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 统计卡片...
],
),
6. 鸿蒙平台适配
问题描述:在鸿蒙平台上,某些 Flutter 特性可能会有不同的表现,例如字体渲染或布局计算。
解决方案:
- 测试应用在鸿蒙平台上的表现,确保功能正常
- 针对鸿蒙平台的特性进行适当的适配
- 遵循 Flutter 的最佳实践,确保跨平台兼容性
7. 性能优化
问题描述:当处理大量文本时,实时统计可能会导致性能问题,例如输入卡顿。
解决方案:
- 优化统计算法,减少不必要的计算
- 考虑使用防抖技术,避免频繁更新状态
- 确保统计逻辑的时间复杂度合理,例如使用 O(n) 复杂度的算法
示例代码:
// 优化后的统计逻辑
void _updateCounts() {
final text = _textController.text;
setState(() {
_characterCount = text.length;
if (text.isEmpty) {
_wordCount = 0;
} else {
// 高效的单词统计
final words = text.split(RegExp(r'\s+')).where((word) => word.isNotEmpty).toList();
_wordCount = words.length;
}
});
}
总结本次开发中用到的技术点
1. Flutter 基础组件
- StatefulWidget:用于管理需要动态更新的界面状态,确保字数统计能够实时反映到界面上
- TextField:用于文本输入,支持多行文本和实时变化监听
- ElevatedButton:用于用户操作按钮,提供清空文本等功能
- Container:用于布局和样式设置,创建美观的卡片效果
- Row/Column:用于线性布局,合理排列统计卡片和其他元素
- GestureDetector:用于实现点击交互,支持显示/隐藏详细统计信息
- SingleChildScrollView:用于实现响应式布局,确保在小屏幕设备上也能正常显示
2. 状态管理
- setState():用于更新组件状态,触发界面重建,确保统计结果能够实时更新
- TextEditingController:用于管理文本输入和输出,提供文本操作的能力
- 生命周期管理:使用
dispose()方法释放资源,避免内存泄漏
3. 文本处理技术
- 正则表达式:使用
RegExp(r'\s+')匹配一个或多个空格,实现单词分割 - 字符串操作:使用
split()和where()方法处理文本,实现单词统计 - 条件判断:使用
if-else语句处理空文本情况,确保统计逻辑的健壮性 - 数值计算:使用
toStringAsFixed()方法格式化数值,提供更美观的统计结果
4. 用户界面设计
- 卡片式设计:使用
BoxDecoration创建卡片效果,提供清晰的视觉层次 - 响应式布局:使用
MainAxisAlignment.spaceEvenly等布局属性,确保统计卡片能够合理排列 - 视觉反馈:通过颜色变化和布局调整,为用户提供清晰的操作反馈
- 色彩搭配:使用紫色和蓝色作为主题色,创建现代、美观的界面
- 间距和边框:合理设置间距和边框,提升用户体验
5. 性能优化
- 高效算法:使用时间复杂度接近 O(n) 的算法进行文本统计,确保处理大量文本时的性能
- 实时更新:在文本变化时立即更新统计结果,提供实时反馈,提升用户体验
- 资源管理:及时释放
TextEditingController等资源,避免内存泄漏 - 防抖考虑:虽然本次开发中使用了实时更新,但在处理大量文本时可以考虑使用防抖技术
6. 鸿蒙平台适配
- 跨平台兼容性:遵循 Flutter 的最佳实践,确保应用在鸿蒙平台上能够正常运行
- 项目结构调整:按照鸿蒙项目要求组织代码结构,提高代码的可维护性
- 平台特性考虑:在开发过程中考虑鸿蒙平台的特性,进行适当的适配
7. 开发最佳实践
- 组件化开发:将功能封装为独立组件,提高代码复用性和可维护性
- 代码可读性:使用清晰的命名和注释,提高代码可读性
- 错误处理:对空文本等边界情况进行处理,提高应用的健壮性
- 用户体验:注重界面设计和交互体验,提供直观、易用的操作方式
- 文档编写:编写详细的功能说明和使用文档,方便后续维护和使用
8. 技术栈整合
- Flutter 框架:使用 Flutter 提供的丰富组件和 API,快速构建美观、功能完整的应用
- Dart 语言:利用 Dart 语言的特性,实现高效、简洁的代码
- 鸿蒙平台:将 Flutter 应用适配到鸿蒙平台,拓展应用的覆盖范围
通过本次开发,我们不仅实现了一个功能完整、界面美观的字数统计工具,还积累了在 Flutter for OpenHarmony 平台上开发应用的经验。这些技术点和最佳实践,对于后续开发类似应用或进行更复杂的项目,都具有参考价值。

flutter_openHarmony(简称 Flutter‑OH)
注意:不是Google官方产物,是OpenHarmony社区TPC组织维护的Flutter引擎移植版本。把Flutter的Dart/Skia引擎做底层改造,让Flutter应用可以直接编译输出 HAP包,跑在OpenHarmony/纯血鸿蒙设备上,不需要依赖Android兼容层。
简单讲:一套Dart/Flutter业务代码,可以同时编译 Android、iOS、OpenHarmony(HAP)。
核心原理
对Flutter Engine做Embedder嵌入适配,对接OpenHarmony Rosen图形管线、UIAbility生命周期,通过MethodChannel实现 Dart ↔ ArkTS双向通信,Flutter自绘UI渲染到鸿蒙Surface,复用方舟编译器、系统权限、分布式能力。
- Dart业务代码几乎不变
- 底层引擎适配鸿蒙图形、线程、生命周期
- 输出产物是标准HAP应用包,可上架鸿蒙应用市场
主要优势
-
存量Flutter项目低成本接入鸿蒙生态
纯Dart业务、纯Widget界面几乎不用改代码即可编译出鸿蒙HAP;只有带Android/iOS原生桥接的插件,才需要做鸿蒙适配替换。已经有成熟Flutter App,想快速覆盖鸿蒙设备,不用全部重写ArkTS。 -
多端UI高度一致性
Flutter自绘渲染,不受各平台控件差异影响,手机、平板、车机界面表现统一;滚动、动画、首页各类动效(轮播、吸顶、骨架屏、入场动画)跨平台表现一致,和你前面问的App首页各种效果可以一套代码全部实现。 -
继承Flutter完整开发体验
保留热重载、DevTools调试、完整Widget组件库;pub.dev海量纯Dart三方库直接复用,是鸿蒙跨端方案里三方库最丰富的方案。提供定制CLI,一条命令完成编译、真机调试、打包HAP。 -
可调用OpenHarmony原生系统能力
支持调用分布式软总线、分布式数据KV、原子化服务、鸿蒙权限体系、硬件能力;Flutter页面和ArkTS原生页面可以混合开发、互相跳转,复杂原生逻辑继续写ArkTS,UI业务交给Flutter实现。 -
全场景设备覆盖
支持OpenHarmony手机、平板、智慧屏、车机等设备,适合需要多终端统一UI的业务。引擎做了懒加载,跟随UIAbility生命周期启停,控制内存占用,减少后台资源消耗。
更多推荐

所有评论(0)