ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter实战:AI对话App开发环境搭建与核心链路解析

Flutter实战:AI对话App开发环境搭建与核心链路解析 1. 立项复盘这个AI对话App为什么最终选了Flutter那周产品例会开了二十分钟需求就一句话我们要做一个AI对话App手机上能用先上Android和iOS。听完这句话我脑子里先闪过三个技术选型小程序、原生双端、再就是Flutter。其实大多数项目走到AI对话这个方向时核心逻辑都在服务端的大模型侧客户端要承担的只是对话入口、流式展示、会话管理和多端体验的一致性。所以这个选择没有想象中那么复杂。我们最后选定Flutter理由特别朴素团队只有两个人一个偏前端一个偏客户端单代码库能同时出双端产品那边UI改动非常频繁而Flutter的热重载在快速迭代项目里体验确实好再加上dio、flutter_bloc、connectivity_plus这些生态组件足够成熟不需要从零造轮子。这里先把结论放在这这个App最终顺利上了架验证了选型是对的。但过程中环境配置和构建链路的坑远比写业务代码要多这也是为什么我把环境两个字写进标题。1.1 需求真正落地前我们把三个问题想清楚了第一个是上下文管理放哪里。很多人一开始会把多轮对话的上下文拼在客户端然后把整段历史抛给AI接口这个方案在会话长度超过十几轮之后就会被token长度和请求延迟一起拖垮。我们的做法是让服务端维护会话记忆客户端只承担最近这轮的输入加前一轮的输出缓存既保证多轮对话的连续性又不会让手机在弱网下等待太久。第二个是流式输出。用户对AI的感知速度很大程度来自字一个一个蹦出来的打字机效果。如果客户端不做流式而是等整段响应结束再一次性渲染一个5秒的完整回答会被拉成13秒的等待用户直接认为App卡了。所以客户端必须从第一天就支持流式接口这不是性能优化是产品体验的底线。第三个是弱网和断线。移动网络不像办公室宽带电梯、地库、地铁隧道随时会断。数据没发完、响应没收全怎么重试、怎么提示、怎么保证消息不丢必须在架构里预留。这些约束直接决定了后面网络层和状态管理的设计方案。1.2 Flutter就赢在不用双头维护这套环境上下表是我做选型时列的对比项目结束后回看基本每条都踩中了对比项Flutter小程序双端原生双端人力1份代码1份代码但能力受限2套团队UI一致性自绘引擎一致性高受宿主平台限制需要多套实现系统能力平台通道可覆盖部分受限最全包体积较大含引擎约20-30MB小小构建链复杂度中高低中上线流程两端分别审核平台审核两端单独审核Flutter最大的代价是包体积和构建链复杂度。尤其是后者——Gradle、Xcode、CocoaPods、Android SDK这些词叠在一起新手很容易在环境这一关就被劝退。但对比一次双端原生团队的维护成本这笔账还是合算的。而且如果你本来就有Android开发基础熟悉Java和Gradle那Flutter的环境门槛其实比想象中低很多。2. 环境搭建完整链路从零到第一个能跑的页面刚把Flutter装好那两天我最大的感受是Flutter本身装起来不难难在它外面粘着一圈工具链——Java、Android SDK、Xcode、CocoaPods哪一环版本不对都会在第一次run时翻车。这一部分我按自己实际走的路径从头捋一遍尽量还原一个新环境从零到能跑的全过程。2.1 装Flutter SDK之前先检查四样东西先别急着下载Flutter。我列了一个检查清单按顺序做能省掉后面一大半报错Git是否安装并可用macOS自带Windows建议装Git for Windows否则后面拉插件依赖会很难受。Java 17是否已装Android Gradle PluginAGP8.x开始要求Java 17你如果只装了Java 8后面gradle编译会直接给你脸色看。Android Studio是否已装很多教程说用VSCode加Flutter插件就行但Android SDK和模拟器还是需要Android Studio来管理尤其Linux和Windows环境这一步不建议跳过。macOS做iOS开发要准备什么Xcode和Command Line Tools是必须的另外还有CocoaPods后面iOS构建会用到。这一步的逻辑是Flutter只是一个编译框架真正把它编译成App的还是底层的Android toolchain和Xcode toolchain。缺了哪一环flutter doctor都会如实汇报只是有些提示写得不够直白。2.2 flutter doctor逐项排查装完SDK配置好环境变量记得先跑一下flutter doctor。这条命令几乎是整个项目里最值钱的一条它一次性检查所有依赖项并给出修复建议。我当时的输出大概长这样[√] Flutter (Channel stable, 3.16.9, on macOS ...) [√] Android toolchain - develop for Android devices [√] Xcode - develop for iOS and macOS [√] Chrome - develop for the web [√] Android Studio (version 2023.1) [√] VS Code (version 1.85.0) [√] Connected device (2 available)但更多时候你看到的是一堆[!]甚至[x]。这里我把最有代表性的三种列出来doctor提示实际原因处理方式Android license status unknown没接受SDK许可按提示运行flutter doctor --android-licenses一路同意Xcode installation is incomplete没有完成安装或未签协议打开Xcode完成组件安装并运行xcodebuild -license acceptCocoaPods not installediOS依赖管理缺环安装CocoaPodsmacOS上常见brew install cocoapods或sudo gem install cocoapods这里我踩过一个印象很深的坑Android SDK装了但platform-tools路径没加进PATHdoctor显示的是Android工具链找不到adb看起来像是没装SDK实际只是环境变量少了。你看到这种奇怪的未检测到提示时先别急着重装先用echo $PATH确认路径。2.3 第一次run模拟器、真机和那些玄学报错环境通过之后我用flutter create ai_chat_app创建项目然后连上模拟器直接flutter run。这个第一次通常不会太顺利常见的三连第一个是卡在Gradle。Flutter项目里默认的Gradle版本和AGP是绑定的首次构建时Gradle会下载分发包如果下载源访问慢可能等十分钟还在转圈。解决办法是把gradle-wrapper.properties里的distributionUrl换成可访问的下载源或者手动下载好zip包放到指定路径。这不是什么hack而是社区里很常规的做法重点是提前准备不要等卡住了再查。第二个是CocoaPods报错。如果你按同一份代码跑双端的方式工作iOS首次构建会自动执行pod install报CocoaPods版本不兼容的情况经常出现。处理方式很直接升级到当前稳定版升级完记得把Podfile.lock删掉再跑一次。我记得第一次跑iOS工程时光这一个问题就耗了小半天后来才知道这是几乎所有Flutter新人都会撞的墙。第三个是编译器版本问题。我遇到的就是那个流媒体里频繁出现的提示the current configured flutter sdk is not known to be fully supported。这个我先卖个关子第五章讲打包时专门说。3. AI对话核心链路请求、流式响应与状态落盘环境通了项目创建好按理说可以写界面了。但AI对话App的核心链路和传统App差一大截普通App请求的是一段JSONAI对话App请求的是一段持续十几秒的文本流。从网络层到UI层每一环都要围绕流来设计这是全场工程的命门。3.1 接口对接的最小工程结构我先把客户端网络层的数据接口定义出来。为了兼容不同的大模型服务商我把远程接口抽象成发送一条用户消息返回一个文本流至于内部是SSE还是WebSocket由服务端统一抹平客户端不关心。代码里最简单的配置是这样final dio Dio(BaseOptions( baseUrl: https://api.example.com/, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), )); dio.options.headers[Authorization] Bearer $token;connectTimeout和receiveTimeout我分别设15秒。AI接口响应时间本来就比普通接口长设太短容易误杀正常的思考中设太长用户又会一直看到转圈。15秒是我测下来比较平衡的取值。如果走完全推理且不做流式这个值可能要拉到30秒以上。真正麻烦的是流式接口的处理。我建议不要试图用dio的普通响应解析一整段文本而是把ResponseType设为ResponseType.stream然后逐块读取按行解析出需要的文本增量。每个片段一到就立刻往UI层的流里灌一次。这样从服务端到界面是一条完整的活水不是等一桶水全接满再倒出来。3.2 流式输出在移动端的表现问题打字机效果是AI对话App的灵魂。同样一句话服务端一秒就给出来了客户端如果攒着不显示用户感知就是卡了3秒如果每收到一个字节就刷一次整棵树中低端手机会掉帧。所以流式渲染要抓住局部更新这个命门。我在Flutter里用的是StreamBuilder包住消息列表中最新的那条消息。每来一个token就把它追加到当前消息的content末尾再让消息列表自动滚到底部。滚动这里有个细节ScrollController.animateTo如果放在每个token回调里直接调用频率太高反而一卡一卡。实测有效的做法是加一个节流比如每80到120毫秒滚动一次人的眼睛根本分辨不出差距但UI的流畅度明显上去。还有一个容易被忽略的点是文本排版。我遇到过英文数字在部分Android版本上出现毛边的情况把字体配置显式指定为系统默认字体就好。Flutter的自绘渲染在文本排版上优势很大但初次接触时字体的坑确实比原生多一点。3.3 超时、断网、限流的兜底方案AI服务不是永远稳定的一个AI对话App至少要处理四类异常超时、断网、服务端限流、内容安全拦截。我建了一个错误码集中处理表方便测试和后续维护现象典型码用户侧表现处理请求超时无响应或超时一直转圈提示网络慢允许重发服务端过载429 / 503回答停止指数退避重试1次然后提示稍后再试认证失效401无法对话静默刷新token后重放请求内容被拦截非2xx回答中断展示安全提示不重试断网检测我用connectivity_plus监听网络状态。但要注意监听网络状态变化不等于监听是否有网有些场景Wi-Fi连着但出口不通所以还得配合请求失败回调。我在交互层做了个轻量提示条断网时顶部亮红条恢复后自动消失。真实项目里这个提示条比任何弹窗都好用。还有一条合规经验AI对话内容不能原样透传。我们接入了内容安全检测接口在消息发出前和流式结果返回后各过一次审核。一旦触发拦截立刻终止展示并给出友好提示。做这类产品内容安全能力是底线的工程储备不是可选项。4. 消息列表状态管理为什么cubit压过了bloc一头消息列表是整个AI对话App里最复杂的一块状态。用户发一句立刻要出现气泡响应回来时正在生成的那个气泡要逐字变化历史记录还要支持翻阅和删除。用setState在这个场景里会越写越痛苦所以我直接上了flutter_bloc。后来在bloc和cubit之间我选了cubit实测下来确实更省心。4.1 对话页面的状态特点对话页面的状态变化主要有两类一是整个列表的追加二是单条消息的局部更新。前者好办后者麻烦。如果用setState最直接的实现是State里放一个List 每次收到流式token就setState(() messages[lastIndex].content token)。这段逻辑跑起来倒也没错但setState会给整个页面打上脏标记重建范围可能涉及输入框、会话列表、滚动控制器的所有依赖旗舰手机测不出问题中低端机就明显掉帧。更关键的是页面里还有录音输入、键盘弹起逻辑setState一多维护成本成倍上升。所以状态管理不是要不要用的问题而是怎么分的问题。cubit这种模式让我把消息追加、流式更新、重试、删除这些操作全部收进一个独立的逻辑层页面只负责拿状态去渲染各司其职。调试的时候看一眼状态的变化过程就知道问题出在哪一层效率和体验完全是两回事。4.2 落地结构我落地时的代码骨架长这样class ChatCubit extends CubitChatState { final ChatRepository repository; final ListChatMessage _messages []; Futurevoid sendMessage(String text) async { _messages.add(ChatMessage(role: user, content: text)); final placeholder ChatMessage(role: assistant, content: ); _messages.add(placeholder); emit(ChatState.generate(_messages, loading: true)); await for (final delta in repository.streamChat(text)) { placeholder.content delta; emit(ChatState.generate(_messages, loading: true)); } emit(ChatState.generate(_messages, loading: false)); } }这个结构的好处是消息列表只有一个数据源UI层的StreamBuilder只做映射不负责拼数据。用户消息先插入列表再立刻渲染是一种乐观更新用户感受是秒回占位消息先出现再被流式内容填满打字机效果基于它展开。后面要加停止生成重新生成只需往cubit里加方法不会污染页面。4.3 我踩过的状态丢失和组件重建问题用cubit的第一周我就踩了一个坑应用在后台被系统回收回来时整个对话状态全空了。原因是cubit的状态只存在于内存App进程被杀后什么都没有。解决办法是给消息列表加了本地持久化每写一条消息和每次流式结束时都落一次库App重启后从库里恢复会话。这也正好呼应了前面第三部分说的状态落盘它不是一句口号而是一条真实的数据流水线。另一个小坑是热重载。flutter_bloc在热重载后会重新执行bloc的创建如果我在页面State里初始化cubit热重载后可能出现旧cubit的事件发到了新页面的尴尬。解决方式是让cubit的创建放在依赖注入层页面从provider里拿同一个实例而不是在initState里新建。这个建议可能有点超前但实际项目只要一涉及热重载调试你就会发现这个设计特别值。5. 打包上架前的最后100米签名、Gradle和那些倔强的提示环境验收的真正标准不是flutter run能跑起来而是能打出可上架的正式包。这一步我遇到的坑比开发期加起来还多而且大部分都和环境相关。5.1 Android签名与Gradle的纠缠发正式版第一件事就是签名。先用keytool生成keystore然后在android/app/build.gradle里配好签名信息。android { signingConfigs { release { storeFile file(release.jks) storePassword ****** keyAlias key0 keyPassword ****** } } buildTypes { release { signingConfig signingConfigs.release shrinkResources true minifyEnabled true } } }关于minifyEnabled我在这里提醒一句开启混淆后如果没有为JSON数据类配好keep规则运行release包时会出现本机跑得好好的线上包一打开就闪退。我们项目的解决办法是模型类统一放在指定包下并在proguard-rules.pro里保留这些类。每次打release包后先在自己手机装一遍再考虑分发。Gradle相关的经典报错是AGP和Gradle版本不匹配。Flutter官方模板给的是适配好的版本但很多人会在装第三方插件时被插件带偏。例如插件要求的compileSdk版本比项目高就会报Failed to transform或dependency requires ...。处理思路很明确不要让插件决定项目的SDK版本把compileSdk统一升到稳定版本再同步更新targetSdk和依赖版本。宁可多花一晚上对齐版本也不要让每个插件各自为政。5.2 iOS权限描述与ATSiOS这边的环境问题集中在Info.plist和签名。AI对话App通常要麦克风语音输入所以NSMicrophoneUsageDescription和NSSpeechRecognitionUsageDescription需要写好。不要小看这两行描述写得模糊被审核拒绝的概率很高这是看似无关紧要却能拖累发版进度的字符串。ATSApp Transport Security是另一个容易踩的坑。服务端如果用的是HTTPS基本不用动如果任何一个接口用了HTTPiOS会默认拦下来。有人图省事会把NSAllowsArbitraryLoads设为true这个做法我非常不建议。正规做法是把测试域名单独加进NSExceptionDomains或者直接推动服务端全量HTTPS。AI对话类App后面还涉及流式接口建议从第一天就用wss和https否则上架审核很容易被卡。真机调试iOS还有一层身份签名的门槛把iOS设备添加到开发者账号的设备列表在Xcode里选中Team否则build会报requires a development team。这个问题每换一台测试机都会遇到提前配置好能避免现场慌。5.3 is not known to be fully supported这类提示怎么判断前面提到的the current configured flutter sdk is not known to be fully supported我也遇到过。很多人第一次看到这个提示会以为是系统坏了其实是因为Flutter SDK和Dart SDK的组合不在官方完整支持列表里。这通常发生在三种情况用了过旧的Dart SDK、擅自升级了Flutter小版本、或者本地有多个Flutter版本导致PATH指向混乱。我的判断流程是先跑flutter --version看版本组合再去对应版本的changelog里确认支持的Dart版本。如果发现是flutter和dart不匹配执行flutter upgrade重新对齐如果项目对稳定性要求极高就反过来固定到某个patch版本并锁住pubspec.yaml里的依赖版本。这种提示一般都不会导致项目完全不能编译但会让人反复怀疑环境提前知道原因真的能省很多时间。另外如果你在配置Android构建时遇到Flutter 3.7版本之后Android默认启用Impeller渲染引擎而这个引擎又对部分老GPU支持不佳界面上出现异常渲染的情况可以在AndroidManifest.xml里临时关闭Impeller来验证但复测后要记得改回来。这类渲染层面的环境问题和SDK版本提示一样都属于配置型问题不是业务bug。6. 一些零碎但很救命的环境经验最后这部分不做总结只放几个在实际项目里反复救过我的小经验。它们未必是教科书内容但对做AI对话类App的Flutter开发者尤其是第一次上手的人优先级很高。6.1 跟着设备走的环境变量项目组两个人一个在Windows一个在macOSgit clone同一个仓库后有一段时间经常出现A这边跑得好好的B那边build失败。排查到最后大部分都是环境变量导致的Flutter SDK路径不同、Android SDK路径不同、Java版本不同。建议在README里写清楚最低版本要求或者用flutter config统一配置而不是让大家各自去改本地的bashrc或环境变量面板。6.2 热重载好用但AI界面有些情况必须冷启动Flutter热重载在改UI时确实快但AI对话界面有个坑流式生成过程中你改了某个组件的build逻辑热重载后状态流会断占位消息永远卡在正在生成。遇到这种情况先停止生成再冷重启。在正式的AI对话功能里我把停止生成的按钮做得足够醒目不单是为了用户交互也是给开发期自救用的。6.3 这个项目后续可以怎么扩展如果后面有机会继续做我想把会话记录做成多端同步让手机和Web共享同一份历史录音输入这块做成流式语音识别边录边出字再给对话加一点人设管理的能力用户可以自己定义AI聊天气质。Flutter在UI表现力上足够支撑这些扩展环境链也已经趟熟真正决定天花板的是服务端推理能力和内容安全策略而不是客户端本身。最后再补一条个人建议如果你的产品也是AI对话类记得在开发期就把内容审核链路接上这比任何客户端优化都优先。网上很多demo能把对话做出来但能不能跑得长久看的就是这些看不见的部分。希望这篇围绕Flutter、AI对话App和开发环境的实战记录能帮你把最容易被低估的环境关提前跨过去。
返回列表