1. 需求拆解与技术选型:这个APP到底做什么
1.1 演讲稿生成器的核心功能画像
事情得从我们项目组接到的一个需求说起。当时几个部门都在准备季度汇报,领导层希望我们做一个内部工具,让员工输入演讲主题、目标时长、场合类型,就能自动生成一份结构完整、口吻合适的演讲稿。说白了就是一个“演讲稿生成器”。
早期我们直接把模型接口封装成一个网页,但很快发现几个问题:网页版在手机上的体验一般,离线用不了,而且大家写稿子经常是在通勤路上、会议室开场前这种碎片时间,需要打开即用、写完即走。于是“做一个能用手机直接打开的轻量工具”就成了刚需。
再看这个工具的核心功能,其实比想象中清晰:
- 输入主题词、演讲时长、演讲场合(比如“工作汇报”“新人自我介绍”“产品发布会”),系统自动生成演讲稿初稿;
- 提供结构化编辑能力,用户可以在生成的稿子上改词、换段落、调整口吻,而不是只给一坨不可改的纯文本;
- 稿子生成后能按字数估算演讲用时,方便控制时长;
- 离线或弱网场景下,历史稿子能正常查看和编辑;
- 最好能有语音朗读辅助排练功能,毕竟演讲稿不是用来读的,是用来“讲”的。
把这些需求列完,团队里很自然就出现了一个分歧:是三个端各写一套原生,还是统一用跨平台方案。
1.2 为什么选Flutter做鸿蒙端
当时我们手里的目标平台是三个:iOS、Android,还有刚被划入交付范围的鸿蒙设备。如果走原生路线,意味着同一套功能逻辑至少维护三份代码,再加上后续迭代时的联调成本,小团队根本吃不消。所以跨平台方案几乎是唯一合理的选择。
跨平台方案里,我们对比过Web容器套壳、React Native、Flutter三路。Web套壳开发快但性能和原生交互差距大,特别是语音朗读、文本选中这类高频操作,体验会明显打折。React Native在鸿蒙生态上的适配成熟度当时还参差不齐,遇到问题容易卡住。而Flutter因为自身渲染引擎不依赖系统控件,理论上只要能跑起来,UI在各端的一致性是最高的。
用一句大白话解释:Flutter把“界面怎么画”这件事完全握在自己手里,视图不是调用系统原生的控件去拼,而是自己用渲染引擎把每一个像素画出来。所以同一套Flutter界面,在iOS、Android、鸿蒙上呈现出的视觉效果几乎一模一样的。对我们这种文本编辑、阅读、预览为主的工具型APP来说,这种一致性太重要了。
鸿蒙端的适配,现在已经有较完整的社区方案。基于这套方案,我们最终锁定了技术路线:
- UI层统一用Flutter编写,覆盖iOS、Android、鸿蒙三个平台;
- 鸿蒙端的系统能力(比如系统分享、文件存储)通过专门的能力桥接层调用;
- 演讲稿生成、文本处理、字数统计等核心逻辑全部放在Dart层,不碰任何平台私有API。
选型做完后我心里其实有底:这个项目最难的从来不是“怎么写演讲稿”,而是“怎么让一套代码在多端都跑得顺、用得好”。Flutter在这里的价值,往大了说是降低了多端交付的复杂度,往小了说就是让我们几个人在有限时间内真的能把东西做出来。
2. 鸿蒙适配环境搭建与工程改造
2.1 开发环境准备
鸿蒙开发第一关不是写代码,而是把环境配到“能跑真机”的状态。我们踩了一轮坑之后,把标准流程固定成了下面这几步,照着走基本不会卡太久。
第一步,安装DevEco Studio,这是鸿蒙应用开发的官方IDE,支持创建工程、管理签名、连接真机调试。注意安装时把SDK组件选全,尤其是HarmonyOS SDK和Toolchain相关的部分,后面编译才不会被缺失组件卡住。
第二步,准备Flutter SDK和Dart SDK。这里建议直接用稳定版渠道,别追新,因为新版本和鸿蒙适配层的兼容性需要额外验证,没必要在开发期给自己添乱。
第三步,在终端执行环境检查命令,确认Flutter能识别到鸿蒙设备:
flutter doctor如果输出里没有报错,说明基础环境没问题。接着把手机开启开发者模式、打开USB调试,用数据线连上电脑,执行:
flutter devices能看到设备列表里出现鸿蒙设备名称,就说明连接成功了。
第四步,配置适配层依赖。在Flutter工程里,需要引入社区提供的鸿蒙适配依赖,然后同步一下工程依赖,让构建系统自动拉取相关的适配代码。这个过程需要耐心,首次同步可能要下载不少依赖包,网络不好时容易卡住,建议提前配好镜像源。
上面这套流程跑完之后,你其实已经有一个能跑的Flutter鸿蒙壳子了。很多新手在这一步会反复失败,原因多半出在SDK版本不匹配或依赖没同步干净,修复思路很直接:先执行flutter clean,再把pubspec.lock删掉重新拉依赖,能解决大部分玄学问题。
2.2 工程结构适配要点
环境跑通只是开始,真正头疼的是把工程结构改成“三端友好”。我们一开始的工程是纯Flutter模板,后来为了接入鸿蒙适配层,做了这么几件事。
第一件事,确认目录里是否包含鸿蒙宿主工程目录。Flutter标准的工程结构里,原本只有android、ios、web这些宿主目录,接入鸿蒙后需要多一个鸿蒙宿主目录。创建好之后,打开其中的配置文件,把应用名称、包名、版本号等信息对齐到和其他平台一致。
第二件事,配置权限声明。演讲稿生成器涉及存储、网络、语音合成这几类能力,在鸿蒙端的权限配置文件里需要声明对应的权限项。这里有个容易踩的坑:权限声明只能说明“你需要这个能力”,真正弹窗询问用户是运行时系统自动处理的,但如果你连声明都没写,那运行到相关功能时会直接静默失败,而且log里往往只给一个很模糊的错误,排查起来相当费时。所以我们当时的做法是:先把需要的能力全部梳理成清单,集中核对一遍权限声明,再开始写业务代码。
第三件事,签名配置。鸿蒙真机安装APP必须有签名文件,调试阶段可以用自动生成的自签名证书,但要注意包名必须和证书里的信息一致,否则安装时直接报错。我们团队第一次遇到这个问题时,检查了半天代码,最后发现只是包名大小写写错了,这个细节建议提前留意。
第四件事,检查构建产物路径是否正常。鸿蒙适配层和Flutter标准的Android构建走的是两条打包链路,所以脚本里的输出目录、JAR包和SO文件的引用方式都要做对应调整。我们在这一步卡过两次,后来总结出一个稳的办法:构建完成后,去产物目录里检查一下是否生成了对应的Flutter资源文件和SO库文件,只要这两样在,安装到手机上基本就能正常跑起来。
等到这些基础工作做完,工程才算真正具备“跨平台”的雏形。后面写业务代码时,你就不会再想“我是在给鸿蒙写APP”还是“我在给安卓写APP”,而是老老实实回到Flutter的世界里,一门心思敲Dart就行。
3. 演讲稿生成器核心模块怎么落地
3.1 结构化生成引擎的设计思路
演讲稿生成器最核心的部分,不是UI做得多炫,而是“生成”这一下到底能不能用。我们用了一个很朴素的方案:不依赖单一的大模型接口去生成长文,而是先用模板引擎搭好骨架,再逐段调用生成能力来填充内容。
为什么这么做?因为纯靠模型直接给全文,容易出现两个毛病:一是结构飘忽,有时写得像散文、有时像议论文;二是字数不好控制,让写800字,结果给了1800字,讲都讲不完。我们想要的是一个结构稳定的稿子,开头、三个分论点、两个案例、一个收尾,说好五个段落就是五个段落,用户拿到手上能直接改,而不是从头开始返工。
具体做法是预置一套“演讲稿结构模板”,里面定义了段落类型、顺序、每段的功能说明。生成的时候,先把用户输入的主题和场合类型映射到模板上,确定每一段该写什么角度,例如:
- 第一段:开场白,点题,说明今天讲这个话题的原因;
- 第二段:分论点一,结合用户所属领域的痛点展开;
- 第三段:分论点二,给出解决思路或案例;
- 第四段:分论点三,做观点升华或行动号召;
- 第五段:收尾,总结全文,致谢。
骨架确定后,系统再为每一段生成具体内容。这样做的好处是,即使某一段生成质量稍有波动,整体文章仍然从头到尾都“像一篇演讲稿”,用户可以针对某一段单独重新生成,而不是点一次按钮就推翻全稿。
模板的核心逻辑用Dart写出来其实不长,核心就是定义一个段落结构列表,然后逐一生成填充分数到对应的内容槽里:
class SpeechSection { final String title; final String purpose; final String? content; SpeechSection({required this.title, required this.purpose, this.content}); } SpeechDraft buildOutline(String topic, SpeechType type) { return SpeechDraft(sections: [ SpeechSection(title: '开场白', purpose: '点题并建立联系'), SpeechSection(title: '分论点一', purpose: '提出核心问题'), SpeechSection(title: '分论点二', purpose: '给出解决思路'), SpeechSection(title: '分论点三', purpose: '案例佐证'), SpeechSection(title: '结尾', purpose: '总结激励'), ]); }生成每一段时,我们会把当前段的“目的”、前后段的摘要、用户提供的主题词一起作为输入条件,这样段与段之间是有上下文关联的,不会出现上一段还在聊成本、下一段突然跳到员工福利这种割裂感。
3.2 字数控制与时长的换算逻辑
演讲稿生成器还有个容易被忽视的点:字数管理。很多人写稿时根本不知道“讲10分钟需要多少字”,如果工具生成的稿子不讲时长,那这个工具就废了一半。
计算逻辑并不复杂,正常中文演讲语速大约每分钟220到260字,比较从容的讲解语速约每分钟180到200字。为了避免用户念得太赶,我们按每分钟200字作为基准值来估算目标字数:
int estimateTargetWordCount(int minutes) => minutes * 200;假设用户想讲10分钟,目标字数就是2000字左右。我们会把这个目标字数按权重分配到各个段落上:开场白占15%,分论点一占25%,分论点二占25%,分论点三占20%,结尾占15%。这样每段都有独立的字数上限,生成时模型就知道“这段别写太满”,字数控制准确率会高很多。
当然,真实世界里用户的语速差异很大,有人讲话天生快,有人喜欢停顿。所以APP里专门做了语速调节,用户可以选“慢速”“标准”“快速”三档,系统会根据选项调整每分钟基准字数,重新估算目标篇幅。实测下来,这个设计非常实用,至少帮我们砍掉了一半“稿子太长讲不完”的抱怨。
3.3 页面流转与多端自适应
演讲稿生成器的界面布局不算复杂,核心就三个页面:
- 生成参数页:放标题输入框、场合选择器、时长滑动条、语速选择器,底部一个大的“生成稿件”按钮;
- 稿件预览页:生成的稿子分段落展示,每段旁边有“重新生成”“编辑”“朗读”三个操作入口;
- 稿件编辑页:点击任意段落进入,可以自由修改文字,改完自动保存回预览页。
三个页面放在手机上正好一屏一页,不需要复杂导航。但在平板上,一页一屏就显得太浪费空间了,我们用了响应式布局:屏幕宽度超过一定阈值时,左边是段落列表,右边是详细编辑区,一次可以看稿、选段、改字,效率比手机端高不少。
字体大小和行距也没少花功夫。演讲稿和普通阅读文本不同,用户是要“看着念”的,所以每段的行高必须宽松,段间距要大,关键句要能加粗或标色。我们在预览页做了“提词模式”,开启后每行只显示几个词,根据设定的语速自动滚动,像提词器一样。这个功能一开始觉得是锦上添花,结果内部试用时好几个同事点名夸,成了亮点功能。
3.4 本地存稿与离线可用
演讲稿生成器这种工具,最怕的就是“网络一断,稿子全没了”。我们做了两级存储:正在编辑的当前稿子实时写入本地数据库,历史稿子也全部存本地,只把“生成”这一动作放到云端处理。
本地存储用的是一种轻量级的键值数据库方案,读写速度快,适合存文本数据。每次用户停止输入后做一次防抖保存,把当前稿子的标题、内容、生成参数、修改时间存下来。历史列表按修改时间倒序排列,支持搜索标题,支持删除,删除前弹确认框以免误操作。
离线状态下,用户打开APP能看到所有历史稿子,能正常编辑、复制、朗读。唯一不能做的是“生成新稿”。我们在生成按钮上做了状态判断,如果检测到网络不可用,直接提示“暂不支持离线生成,请联网后再试”,而不是让用户点了一下没反应干着急。
这个“先保证已有内容可靠,再追求在线能力”的思路,后来被证明特别正确。因为我们有不少用户是在飞机上、地库里用APP,网络条件很差,最终还是顺利完成了排练准备。
4. 语音朗读与导出分享怎么做好用
4.1 朗读功能的异步处理
演讲稿生成器如果只有“生成文字”,其实和记事本没什么区别。真正让它像个演讲稿工具的,是把“写”和“讲”串起来。语音朗读在这个链条上就是桥梁。
朗读功能实现上并不复杂,核心是调用系统的语音合成能力,把当前段落按字符串传给底层即可。但有几个细节会影响体验,值得单独说。
第一,异步合成。长文本一次性丢给合成接口,经常会出现内存飙升甚至卡死的情况,尤其是演讲稿动辄一两千字。我们的做法是分段合成:把当前段落按句号、逗号拆成句子列表,一句一句地合成音频,合成完一份就播放一份,播放完再合成下一份。这样内存占用始终在一个很小范围,连续朗读十分钟也不会卡顿。
第二,朗读中的高亮跟随。当正在读某一句时,页面里对应文字会加底色,方便用户跟着看。实现方式也不难,维护一个“当前朗读的句子序号”,句子切换时更新状态,UI层用AnimatedContainer做背景色过渡。这个功能对排练特别有用,眼睛可以一直盯着屏幕找节奏。
第三,语速和音色切换。由于演讲场景经常是“正式”“轻松”“激情”三种语气来回切,我们在设置里放了音色选项,用户可以给不同段落的朗读选择不同音色,这在排练时能明显帮助找到自己该用的语调。
4.2 导出文本与提词卡片
稿子写好了,总要带出去。导出功能我们做了三条路径,覆盖最常见的实用场景。
第一条路径是复制全文。点按钮后整篇稿子以纯文本格式进剪贴板,用户可以粘贴到任意聊天工具、笔记软件里,最原始也最通用。
第二条路径是导出提词卡片。这是我最推荐的一个功能,做法是把每段文字按“句子”或“短句”重新排版成一张窄长的卡片,字体大、行距宽,适合放在手机屏幕上近距离看。说实话,正式演讲的时候,大多数人根本不会照着全稿念,而是扫一眼关键词就能想起来接下来讲什么。提词卡片恰好就是干这个用的,卡片上每个短句对应一个要点,提示性极强。
第三条路径是保存为本地文件。生成文本后以指定格式保存到手机存储目录,这样用户可以把稿子拷到电脑上继续排版,或者发给别人协作修改。导出时要注意给文件起一个有辨识度的名字,我们用的是“演讲稿_主题_日期”这个命名格式,否则用户导出几篇稿子后,文件列表会乱成一锅粥。
4.3 分享能力与系统集成
分享这块,在鸿蒙端走的是系统分享能力,通过桥接层调用系统分享面板,把文本或文件内容交给用户选中的目标APP。这里有个值得说的桥接设计:Flutter侧只定义一个通用的“分享”方法,传入文本内容和文件路径,具体的分享面板展示、回调处理全部由鸿蒙原生侧负责。这样Dart代码完全不用关心系统差异,只要约定好接口就行。
调试分享时我们踩过一个典型的坑:分享面板能弹出,但选完微信或备忘录后,发现分享出去的文本是空的。排查了很久,最后发现问题出在“异步结果返回的顺序”上——Flutter侧在原生面板还没处理完时就读取了返回值,读到了一个空内容。解决办法是在原生侧把分享结果通过回调通道异步通知Flutter侧,Flutter拿到回调后再去更新UI,而不是同步等待结果。
这个坑很隐蔽,但也很有代表性。跨平台开发里,凡是涉及原生和Dart层频繁往返的场景,都要下意识养成分步回调的习惯,不要假设“我调你、你马上给我结果”这种同步关系成立。异步处理虽然要多写几行代码,但稳定性和可维护性都会好很多。
5. 常见问题与排错实录
5.1 构建期问题
跨平台开发的经典难题,构建失败能占掉开发期三分之一的时间。我们遇到过的问题主要集中在下面几种:
- 首次拉依赖超时。解决办法是指定国内镜像源,并把超时时间调长。如果还不行,删除
pubspec.lock再重新拉,往往能解决。 - 内存不足导致构建崩溃。鸿蒙和Android的构建工具都很吃内存,构建时把其他大型软件关掉,或者在编译配置里把并行任务数调低,能显著减少崩溃概率。
- 构建产物缺失Flutter资源文件。原因一般是宿主工程的资源配置不完整,检查一下是否把Flutter的编译产物路径正确挂载进了宿主工程,必要时先执行一次
flutter clean全量重编。
这里还有一个容易踩的坑:如果你既开过Android模拟器,又连着鸿蒙真机,构建时可能出现“平台冲突”——构建脚本分不清到底该把产物打包给哪个平台。我们在开发中规整出一套习惯:改需求时就只开真机调试,做UI验证时才开模拟器,避免误触发渠道混淆。
5.2 运行期问题与权限相关坑
构建通过只是第一步,上了真机才是真正的考验。我们遇到的第一个运行期问题是“文字渲染正常但图标全消失”。排查下来发现是系统字体加载路径在鸿蒙端不一样,有些自定义字体文件没有被正确加载。解决办法是把字体文件放到宿主工程的资源目录里,明确指定字体路径,而不是依赖默认查找。
第二个典型问题是“权限弹窗弹了但功能没生效”。原因也很典型:我们在代码里申请了权限,也在配置文件里做了声明,但没注意权限申请的时机必须在用户触发对应功能的前一刻。如果一进APP就申请权限,用户往往会拒绝,后面再想用就麻烦了。好的做法是:用户第一次点“朗读”按钮时,再弹窗请求“语音合成”权限,用户此时明确知道“我按了朗读,所以它在问我要权限”,同意率会高很多。
第三个问题是“界面被系统手势挡住”。演讲稿的编辑页有左右滑动手势,但鸿蒙系统的侧边返回手势优先级更高,结果用户本来想拖动编辑光标,却被系统判定为返回操作,直接退出页面。解决办法是把编辑区设计成按段落编辑,避免在页面整体上做复杂的横向滑动,同时也照顾了手掌误触的问题。
5.3 鸿蒙特有适配细节清单
鸿蒙设备在屏幕比例、系统导航习惯上和安卓、iOS有一些差异,如果不专门适配,用户会明显感觉“哪里不对劲”。我们整理过一张速查清单:
| 场景 | 问题表现 | 解决办法 |
|---|---|---|
| 安全区域 | 页面底部被系统导航条遮挡 | 使用安全区域API适配内边距 |
| 屏幕比例 | 字体被拉伸或显示不全 | 用动态文本缩放,不用固定像素值 |
| 横竖屏切换 | 页面布局错乱 | 锁定竖屏,或按屏幕宽度重新计算布局 |
| 返回手势 | 与编辑手势冲突 | 简化编辑交互,不用长距离横向滑动 |
| 状态栏颜色 | 深浅色模式下文字看不清 | 根据主题色动态调整状态栏图标样式 |
| 分享面板 | 文本内容为空 | 异步回调后再读取分享结果 |
这张表里的每一项,都是在真机上一遍遍试出来的。说实话,跨平台开发的“跨”字,最难的不是把代码写到多端能跑,而是让多端用户都觉得“这APP天生就该长在我这台设备上”。那些系统级的交互差异,才是真正决定体验高低的地方。
5.4 快速排错流程建议
如果读者朋友也在做多端开发,我建议从一开始就建立一套固定的排错流程,能省下大量时间:
- 先看代码还是先查环境?在多端开发里,90%的诡异问题都是环境或依赖问题,不是代码问题,所以先跑一遍
flutter doctor和flutter clean基本没坏处; - 报错信息不一定准确。很多跨端框架把底层错误包装成了高层错误,真正的根因藏在更底层,所以看到报错别急着改代码,先把完整错误日志翻到末尾几行,往往能找到关键线索;
- 真机优先。模拟器上跑得通,不代表真机没问题,权限、性能、字体渲染这些差异只有真机能暴露;
- 保存每个稳定版本的构建产物。哪天改了代码把工程弄崩了,还能回滚到能跑的版本,这个习惯救了我们好几次。
开发演讲稿生成器APP这个项目,前后我们花了大概两个月。真正写业务代码的时间可能只占一半,另一半全耗在了环境配置、多端适配、异步调试这些看似“不产生价值”但实际上决定成败的事情上。我个人的体会是,跨平台开发一定要有一个心理预期:多端的坑是躲不掉的,但每踩一个坑,后面所有同类项目都受益。
最后再分享一个产品层面的小技巧:演讲稿生成器这类工具,预设模板远比算法参数重要。我们内置了“工作总结”“新人介绍”“产品宣讲”“活动致辞”几套不同口吻的模板,用户即使不指定任何参数,选一个模板点一下生成,出来的稿子也已经有模有样。产品很多时候是“效果好”胜于“功能多”,能把一个核心场景做透,就比堆一堆花哨的按钮有价值得多。