1. 技术栈选型从零到发布为什么这一步决定了后面80%的坑做独立开发最怕什么不是代码写不出来而是写到一半发现选错了技术栈推倒重来的成本高到让人想放弃。我见过太多人第一行代码还没敲光在“用Flutter还是React Native”“后端用Node还是Go”这些问题上纠结了两周最后项目还没开始就结束了。这篇内容就是把我自己从零到把APP推上应用商店的完整技术栈决策过程拆开讲清楚包括每一层选了什么、为什么这么选、当时还考虑过哪些方案、以及事后回头看哪些选对了哪些踩了坑。适合正在准备做第一个完整APP的独立开发者也适合已经写过Demo但没走过完整发布流程的朋友。核心关键词就一个技术栈。但技术栈不是一张清单而是一连串有先后依赖关系的决策链每一步都会影响下一步的选择空间。先说一个基本判断技术栈没有绝对的好坏只有跟你的约束条件匹不匹配。你的约束条件包括你已有的技术背景、项目的时间预算、目标平台iOS/Android/Web、是否需要后端、后期维护成本、以及最容易被忽略的——你愿不愿意为某些环节付费。把这几个约束想清楚技术栈的选择范围会迅速收窄纠结的时间能省掉一大半。我自己的约束条件是这样的前端只会JavaScript和一点Python后端经验有限预算控制在每月100块以内目标平台是iOS和Android双端项目类型是内容管理加用户系统的工具类APP。基于这些约束下面逐层拆解我的选择逻辑。2. 前端框架跨平台方案里我为什么最终选了Flutter2.1 跨平台 vs 原生先算一笔时间账独立开发者做双端APP第一个绕不开的决策就是写两套原生代码还是用跨平台框架写一套。我当时的算法很简单假设原生开发iOS需要X小时Android需要Y小时跨平台需要Z小时。根据社区里大量独立开发者的经验数据跨平台方案的Z大约是X的1.3到1.5倍而X加Y大约是X的1.8到2倍。也就是说跨平台能省下大约30%到40%的总开发时间。对于一个人做项目的情况这个时间差往往就是“能做完”和“做不完”的区别。但跨平台不是没有代价。性能敏感的场景比如高频动画、复杂手势、大量列表渲染跨平台框架确实会吃亏。不过对于我这种内容管理加用户系统的工具类APP性能瓶颈根本不在渲染层而在网络请求和数据缓存。所以跨平台方案完全够用。2.2 Flutter、React Native、原生三选一的决策过程当时我重点对比了三个方案对比维度FlutterReact Native原生双端学习曲线Dart语言需重新学但语法接近JS直接复用JS/TS经验Swift和Kotlin都要学UI一致性自绘引擎双端像素级一致依赖原生组件双端有差异各自平台最自然生态成熟度包管理pub.dev常用库齐全npm生态最大各自官方生态热重载体验极快状态保持好快但偶尔丢失状态各有方案发布流程一套代码分别打包一套代码分别打包两套独立流程长期维护Google主力维护Meta维护社区活跃各自平台官方最终选Flutter的决定性因素有三个。第一我受够了React Native里双端UI不一致带来的调试成本Flutter的自绘引擎意味着我在iOS上看到什么样Android上就是什么样省掉了大量“这个间距在Android上怎么多了2px”的排查时间。第二Flutter的热重载体验确实更好改完代码保存后状态不丢失对于频繁调整UI的项目来说效率提升明显。第三Dart语言的空安全机制在编译期就能挡掉大量空指针问题对于我这种经常忘记判空的人来说少写了很多防御性代码。注意如果你已经对React生态非常熟悉React Native的学习成本几乎为零这时候选React Native可能比Flutter更划算。技术栈选型的第一原则是复用已有知识而不是追求“最好”的方案。2.3 Flutter项目初始化时我踩过的坑初始化Flutter项目时我建议直接用命令行而不是IDE的向导flutter create --org com.yourdomain --project-name your_app --platforms ios,android your_app这里有几个细节值得注意。--org参数决定了iOS的Bundle ID和Android的包名一旦发布后就不能改了所以一定要想清楚。--platforms参数只生成你需要的平台目录避免项目里多出一堆用不到的文件夹。项目名用下划线命名因为Dart的包名规范要求如此。初始化完成后第一件事是锁定Flutter版本。在项目根目录的pubspec.yaml里加上environment: sdk: 3.2.0 4.0.0然后在CI或者本地开发机上用fvmFlutter Version Management来管理版本。我试过直接用系统全局的Flutter结果换电脑后版本不一致导致构建失败排查了半天。用fvm之后项目根目录放一个.fvmrc文件团队里所有人用的都是同一个版本省心很多。3. 状态管理与架构为什么我放弃了Provider转向Riverpod3.1 状态管理方案的选择逻辑Flutter的状态管理方案多到让人眼花缭乱setState、Provider、Riverpod、Bloc、GetX、MobX……我一开始用的是Provider因为官方文档推荐社区示例也多。但写到第三个页面的时候问题就来了多个Provider之间的依赖关系变得复杂某个Provider需要在另一个Provider更新后重新计算用Provider的ProxyProvider写起来很绕而且调试时很难追踪到底是哪个环节触发了重建。后来我换成了Riverpod核心原因是它的编译期安全。Riverpod的Provider是全局变量不依赖BuildContext这意味着我可以在任何地方读取状态包括在纯Dart函数里。而且Riverpod的ref.watch和ref.read语义清晰watch用于需要响应变化的场景read用于只需要读取一次的场景写代码时不容易搞混。3.2 我的项目分层架构最终我的项目采用了三层架构数据层Repository模式每个Repository负责一个领域的数据获取和缓存。比如UserRepository负责用户信息的读写ContentRepository负责内容列表的增删改查。状态层Riverpod的Notifier每个Notifier对应一个页面或一个功能模块的状态。Notifier里调用Repository的方法把数据转换成UI需要的格式。UI层Widget只负责渲染通过ref.watch监听Notifier的状态变化通过ref.read调用Notifier的方法。这个分层的好处是每一层都可以独立测试。Repository层可以用mock数据测试Notifier层可以用Riverpod的ProviderContainer测试UI层用Widget测试。我试过在项目中期重构时因为分层清晰只改了Repository层的实现上层代码一行没动。3.3 路由管理GoRouter的实际使用体验路由我选了GoRouter原因是它支持声明式路由和深度链接。声明式路由的好处是路由配置集中在一个地方不像Navigator 1.0那样路由逻辑散落在各个页面里。深度链接对于内容类APP很重要用户从浏览器点开一个链接应该直接跳到对应的内容详情页而不是打开APP首页再让用户自己找。GoRouter的配置大概长这样final router GoRouter( routes: [ GoRoute( path: /, builder: (context, state) const HomePage(), routes: [ GoRoute( path: content/:id, builder: (context, state) ContentDetailPage( id: state.pathParameters[id]!, ), ), ], ), ], );这里有个细节路径参数用:id声明在builder里通过state.pathParameters读取。我一开始忘了在路径里加冒号结果一直匹配不到排查了半小时才发现。4. 后端与数据存储从Firebase到自建服务的权衡4.1 后端方案的三条路线独立开发者的后端选择大概有三条路一是用BaaSBackend as a Service比如Firebase或Supabase二是用Serverless函数三是自己租服务器写后端。我三条路都试过最后选了第三条但这个过程值得展开说。Firebase的优势是上手极快认证、数据库、存储、推送全都现成的文档也全。但问题在于成本不可控。Firebase的Firestore按读写次数计费项目上线后如果某个查询没做好索引一次全表扫描就能烧掉几天的预算。我试过在测试环境跑了一个批量导入脚本第二天收到账单提醒虽然金额不大但那种“不知道钱花在哪”的感觉很难受。Supabase是开源的Firebase替代品基于PostgreSQL可以用SQL查询成本相对可控。但Supabase的免费层有项目暂停机制一段时间不访问数据库会自动暂停下次访问需要等待唤醒。对于个人项目来说这个体验不太好。Serverless函数比如Cloud Functions或Edge Functions适合处理轻量级的业务逻辑但不适合做主要的数据存储层。而且冷启动延迟在移动端场景下比较明显用户打开APP等两秒才看到数据体验很差。4.2 我最终的后端架构最后我选了一个折中方案用轻量级后端框架自己写API部署在一台低配云服务器上数据库用SQLite加定期备份。这个方案的优势是成本固定每月几十块数据完全自己掌控没有按量计费的不确定性。后端框架我选了FastAPI原因是Python写起来快自动生成OpenAPI文档而且异步支持好。数据库用SQLite是因为项目初期数据量小单文件数据库备份和迁移都方便。等数据量上来了再迁移到PostgreSQLSQL语法基本兼容迁移成本可控。API设计上我遵循了几个原则。第一所有接口返回统一的JSON结构{ code: 0, message: success, data: { ... } }code为0表示成功非0表示业务错误。这样客户端只需要判断一个字段就能知道请求是否成功。第二认证用JWTtoken放在请求头的Authorization字段里。第三所有时间字段用ISO 8601格式的UTC时间客户端负责转成本地时区显示。4.3 数据缓存策略移动端不能每次都从网络拉数据必须有本地缓存。我用了两层缓存内存缓存和磁盘缓存。内存缓存用Dart的Map实现存储当前会话中频繁访问的数据比如用户信息。磁盘缓存用shared_preferences存简单的键值对用sqflite存结构化的列表数据。缓存的更新策略是“先读缓存再请求网络网络返回后更新缓存并刷新UI”。这样用户打开APP时能立刻看到上次的数据同时后台静默请求最新数据。如果网络请求失败用户至少还能看到缓存的内容不会白屏。注意缓存一定要设过期时间。我试过因为缓存没设过期用户改了头像后自己看得到但别人看不到排查后发现是本地缓存一直没更新。后来所有缓存都加了时间戳超过一定时间就强制从网络拉取。5. 开发工具链与发布流程那些文档里不会写的细节5.1 代码管理与协作工具代码托管用Git这没什么好说的。但分支策略值得说一下。我一个人开发用的是简化版Git Flowmain分支保持可发布状态dev分支日常开发功能分支从dev切出完成后合并回dev发布时dev合并到main并打tag。CI/CD我用了GitHub Actions配置了三个工作流push到dev时跑测试和静态分析push到main时构建iOS和Android的发布包打tag时自动上传到应用商店的测试轨道。这样每次发布只需要打一个tag剩下的交给CI。5.2 iOS和Android发布流程的差异iOS和Android的发布流程差异很大这里展开说几个关键点。iOS方面证书和描述文件是最容易出问题的环节。我建议用Fastlane的match来管理证书把所有证书和描述文件存在一个私有的Git仓库里CI运行时自动拉取。这样换电脑或者换CI环境时不需要手动配置证书。App Store Connect的审核周期通常是1到3天首次提交可能更长。审核被拒最常见的原因是隐私政策不完整和截图不符合规范。Android方面Google Play的审核通常几小时内就能完成但首次发布需要完成开发者账号验证这个流程可能需要几天。Android的签名用keystore文件一定要备份好丢了就没办法更新已发布的应用了。我试过因为keystore密码记错折腾了一下午才找回来。对比项iOSAndroid开发者账号费用每年99美元一次性25美元审核周期1-3天几小时到1天签名方式证书描述文件keystore文件测试轨道TestFlight内部测试/封闭测试/开放测试截图要求严格尺寸必须匹配相对宽松5.3 版本号管理策略版本号我用的是major.minor.patchbuild的格式。major在有重大功能更新时递增minor在有小功能更新时递增patch在修bug时递增build每次提交到商店时自动递增。Flutter里版本号在pubspec.yaml里配置version: 1.2.345前面的部分用户能看到后面的build号是给商店和CI用的。我试过忘记递增build号结果商店拒绝上传说版本号已存在。后来在CI里加了一个脚本每次构建时自动从Git的commit数量生成build号再也不用手动改了。6. 常见问题与排查技巧实录6.1 技术栈相关的典型问题问题一Flutter在iOS上构建失败报错“Module not found”这个通常是因为CocoaPods的缓存出了问题。解决方法是进ios目录删掉Podfile.lock和Pods文件夹然后重新跑pod install。如果还不行试一下pod repo update更新本地的spec仓库。问题二Riverpod的Provider在页面销毁后仍然被监听这是因为ref.watch在Widget销毁时没有自动取消监听。解决方法是确保在ConsumerWidget或ConsumerStatefulWidget里使用ref.watch不要在其他地方用。如果需要在非Widget环境里监听用ref.listen并手动管理订阅。问题三后端API返回的数据在客户端解析失败最常见的原因是字段类型不匹配。比如后端返回的id是数字客户端定义的是字符串。解决方法是所有API接口都用OpenAPI文档定义清楚字段类型客户端用代码生成工具自动生成模型类不要手写解析逻辑。问题四应用商店审核被拒理由是“隐私政策不完整”隐私政策必须包含收集哪些数据、为什么收集、数据存储在哪里、是否与第三方共享、用户如何删除数据。我建议直接用隐私政策生成工具生成一份然后根据自己APP的实际情况修改。不要直接复制别人的因为每个APP收集的数据不一样。6.2 独家避坑技巧第一个技巧在项目初期就配置好错误监控。我用的是SentryFlutter和FastAPI都有对应的SDK。配置好之后线上出现的任何异常都会自动上报包括堆栈信息和用户操作路径。这个在排查线上问题时能省掉大量“用户说闪退了但我复现不出来”的时间。第二个技巧所有网络请求都加超时和重试。移动网络不稳定是常态没有超时设置的请求可能会一直挂着用户看到的就是转圈圈。我设的是连接超时5秒读取超时10秒失败后重试2次重试间隔指数递增。第三个技巧在APP里加一个隐藏的调试页面显示当前的环境配置、API地址、缓存状态、版本号等信息。触发方式可以是在设置页面连续点击版本号5次。这个页面在排查用户反馈的问题时非常有用可以让用户截图发过来一眼就能看出问题所在。第四个技巧发布前一定要在真机上测试。模拟器和真机的差异比想象中大尤其是涉及权限、推送、支付这些功能。我试过在模拟器上一切正常真机上推送收不到排查后发现是推送证书配置错了。6.3 技术栈决策的复盘回头看整个技术栈的选择有几个决策我认为是对的选Flutter省下了大量双端适配时间选Riverpod让状态管理清晰可控选自建后端让成本固定且数据自主。也有几个决策如果重来我会调整后端框架可能会选Go而不是Python因为部署更简单单二进制文件数据库可能会一开始就用PostgreSQL而不是SQLite省掉后期迁移的麻烦。但技术栈不是一成不变的。项目不同阶段可以用不同的技术栈。初期追求开发速度中期追求稳定性后期追求性能和成本优化。关键是每一步都知道自己为什么这么选以及如果选错了怎么切换。最怕的不是选错而是不知道为什么选出了问题也不知道往哪个方向排查。提示技术栈的决策文档一定要写下来哪怕只是几行字。记录当时的选择理由、考虑过的替代方案、以及切换成本。三个月后你回头看会感谢自己写了这些。