ARTICLE DETAIL

资讯详情

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

Flutter双端开发实战:一套代码搞定iOS与Android上架全流程

Flutter双端开发实战:一套代码搞定iOS与Android上架全流程 Flutter 双端开发实战一套代码搞定 iOS Android从开发到上架全流程做移动端开发的这几年我先后用原生写过 Android也用 Objective-C 维护过 iOS 项目后面又接触了 React Native 和 uniapp。说实话跨端方案换来换去总有各种别扭的地方。直到我把一个完整的商业项目用 Flutter 从零写到双端上架才真正体会到“一套代码跑两端”这件事可以做到多彻底。这篇文章我会完整记录这次 Flutter 双端开发的整个过程包括项目结构怎么组织、Dio 请求怎么封装、本地数据库怎么做、Isolate 怎么用来优化性能以及最后 iOS 和 Android 两边的构建打包、上架审核全流程。还会把我实际踩过的坑——比如 VS Code 里的 Visual Studio toolchain 报错、Gradle 插件应用方式的问题、抓包调试的配置——全部整理出来。不管你是刚接触 Flutter 的新手还是从原生转跨端的开发者按着这篇文章的思路走基本能避开大部分常见的坑。1. 双端开发的核心思路为什么一套代码能同时搞定 iOS 和 Android1.1 从“两套代码两拨人”到“一套代码一个团队”做过原生开发的人都清楚同样的功能在 iOS 和 Android 上要实现两遍。业务流程一样但 UI 代码是两套网络层是两套存储方案是两套连崩溃日志的收集平台都要各接一遍。小公司养不起两个原生团队大公司又面临沟通协调成本。Flutter 的思路完全不一样它自带的渲染引擎不依赖 iOS 的 UIKit也不依赖 Android 的 View 体系而是直接用 Skia 在 Canvas 上绘制每一个像素。开发语言统一用 Dart业务代码写一遍UI 也是写一遍。我之前也担心过流畅度的问题。实际测下来在中等配置的 Android 千元机上Flutter 的列表滚动和页面转场依然能保持 60 帧这是因为 Flutter 的渲染管线是直接跟 GPU 交互的。相比 WebView 套壳方案体验上的差距是肉眼可见的。我做完这个项目后最大的感受就是Flutter 不是“能用”而是“真的好用”。它的状态管理、组件复用、热重载机制都在为“一个人维护双端”这件事服务。1.2 项目整体架构与目录规划动手写代码之前一定要先规划好目录结构。我自己长期用的是 feature-first 的分层方式也就是按业务模块划分而不是按技术类型划分。大概是这样的结构lib/ core/ // 核心基础能力 network/ // Dio 请求封装 database/ // sqflite 数据库封装 utils/ // 工具类 constants/ // 常量配置 features/ // 业务模块 login/ home/ order/ mine/ shared/ // 跨模块共享的组件和模型 widgets/ models/ app.dart // 根组件 main.dart // 入口这种结构的好处是当你需要新增一个业务模块时直接在 features 目录下建一个子目录这个模块自己的页面、控制器、数据模型都放在一起。多人协作时每个人负责各自独立的 feature 目录几乎不会产生代码冲突。我当时把登录、首页、订单、个人中心四个模块拆开写每个模块大概几千行代码整个项目维护起来非常清爽。另外提一点Flutter 的包管理用的是 pubspec.yaml类似前端的 package.json。依赖一旦定下来就尽量不要频繁升级大版本尤其是 Flutter SDK 本身的版本我见过太多项目因为升级 Flutter 版本导致第三方库不兼容最后被迫回滚。我的做法是项目初期就把 Stable 版本固定下来pubspec.lock 文件纳入版本控制团队所有人的依赖保持一致。2. 环境搭建与工程初始化从零到项目跑起来2.1 Flutter SDK 安装与编辑器选型Flutter 的安装本身并不复杂它的 SDK 就是个压缩包下载后解压然后把 bin 目录加到系统环境变量里终端里执行 flutter doctor 就能检查环境状态。这里我强烈建议能用 git 拉取 SDK 就用 git 拉取因为 flutter 命令后续需要自己管理版本用 git 方式切换版本会方便很多。编辑器方面我试过 Android Studio 和 VS Code 两种。Android Studio 适合做 Android 原生相关的调试但占用内存比较大。VS Code 搭配 Flutter 插件和 Dart 插件启动速度快日常写代码完全够用而且对目录结构的展示更轻量。我个人的选择是用 VS Code 写 Flutter 代码、用 Android Studio 做 Android 打包签名相关操作两边互补。有一点需要特别注意如果你电脑上只装了 VS Code没有完整的 Visual Studio 或 Windows SDK在编译 Android 项目时大概率会遇到“unable to find suitable Visual Studio toolchain”的报错。这个问题的本质是 Flutter 在编译原生插件时某些 C/C 依赖需要用到 Visual Studio 的 C 生成工具。解决办法是下载 Visual Studio Build Tools勾选“使用 C 的桌面开发”工作负载。我一开始也以为这是 Flutter 配置出问题了折腾了半天才发现是缺了 C 编译环境。2.2 新建项目后马上要改的三个配置跑通 flutter create 之后先别急着写业务代码有几处默认配置需要立刻改掉。第一应用包名。默认生成的包名是 com.example.xxx上架时必须改成自己公司的域名反写。在 Android 里修改 build.gradle 里的 applicationId 和 namespaceiOS 里修改 Xcode 项目的 Bundle Identifier。这里注意包名一旦在应用商店审核通过后再修改会被当成新应用处理所以一开始就要定好。第二App 显示名称。Android 在 AndroidManifest.xml 里改 android:labeliOS 在 Info.plist 里改 CFBundleDisplayName。如果不改安装到手机后显示的会是 Flutter 默认的项目名很影响专业感。第三隐私权限声明。很多 Flutter 项目会用到网络、存储、相机等权限。我遇到过应用因为权限描述不清晰被 Android 应用市场驳回的情况比如只写了“访问网络”这种含糊描述审核方要求写明用途。所以项目初始就把 AndroidManifest.xml 里的权限说明写得清楚一些加上 uses-permission 的 tools:targetApi 属性避免被第三方 SDK 自动注入多余权限。2.3 我踩过的环境坑Gradle 插件应用方式和依赖仓库Flutter 的 Android 工程会自动生成 Gradle 配置。早期版本的 Flutter 项目里主工程的 build.gradle 顶部会有这么一行apply flutter: $flutterRoot/packages/flutter_tools/gradle/flutter.gradle如果你用的是较新的 Flutter 版本并且看到控制台报 “You are applying Flutters main Gradle plugin imperatively using the apply script method, which is deprecated” 的警告那就说明需要迁移到新的插件配置方式。现在的做法是在 settings.gradle 里用 pluginManagement 声明 Flutter 插件plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 id com.android.application version 8.3.2 apply false id org.jetbrains.kotlin.android version 1.9.22 apply false }这个警告初期不影响编译但如果你后续要集成 Android 原生代码或者升级依赖版本旧方式可能会报一些很难排查的构建错误。我的建议是新建项目时直接使用最新稳定版 Flutter它默认就是新配置方式如果是老项目尽早迁移越晚改越麻烦。另外国内开发者集成第三方库时经常遇到从 Maven Central 下载依赖超时的问题。这是网络延迟导致的不是配置错误。我通常先在项目的 settings.gradle 里把仓库源配好增加国内可用的镜像源这样依赖下载会稳定很多。需要注意不能为了图方便把所有仓库都换成镜像因为有些镜像的同步周期慢会导致拿不到某个库的最新版本遇到这种情况就把镜像源作为备用仓库优先走官方源才是正解。3. 核心业务开发实操请求封装、本地存储与性能优化3.1 网络请求封装Dio 的拦截器与统一异常处理Flutter 里做网络请求我几乎不用原生 HttpClient 直接撸而是用 Dio 这个库。Dio 的生态比较完善支持拦截器、取消请求、FormData 上传下载、Cookie 管理这些正好是业务开发里最常用的功能。我封装的思路是先定义统一的响应模型再封装请求工具类。基本骨架大概是这样的class ApiClient { ApiClient._() { dio Dio(BaseOptions( baseUrl: ApiConfig.baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); dio.interceptors.add(LogInterceptor(responseBody: true)); dio.interceptors.add(TokenInterceptor()); } FutureT getT(String path, {MapString, dynamic? params}) async { final response await dio.get(path, queryParameters: params); return _unwrapT(response.data); } FutureT postT(String path, {Object? data}) async { final response await dio.post(path, data: data); return _unwrapT(response.data); } }TokenInterceptor 会读取本地存储的 token每次请求自动带上 Authorization 头。服务端返回 401 时拦截器负责拉取刷新 token 并重试原请求。这里有一个关键细节重试请求时要避免死循环一般设置最多重试一次否则 token 完全失效后会导致请求无限重发白白消耗用户流量和服务器资源。统一异常处理我放在了 _unwrap 方法里做接口返回 code 不为 0 时抛出业务异常然后由页面层的状态管理统一捕获弹 toast 提示。如果不做这层封装每个页面都要写 try-catch代码会大量重复而且很容易漏掉一些边界情况比如超时、断网、接口 500 这类一旦漏掉用户看到的就是界面卡死或者白屏。3.2 抓包调试Dio 请求怎么在 Charles 里看到Flutter 项目调试网络请求时有个常见的痛点——默认情况下 Flutter 发起的 HTTP 请求不经过系统的代理端口你在 Charles 或者 Fiddler 里看不到。很多人一开始以为是自己代理配置错了其实是因为 Flutter 的 Dart VM 默认不走系统代理设置。我的做法是在开发环境的 Dio 配置里显式设置代理地址if (kDebugMode) { (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate (client) { client.findProxy (uri) { return HttpClient.findProxyFromEnvironment(uri, environment: {http_proxy: http://127.0.0.1:8888}); }; client.badCertificateCallback (cert, host, port) true; }; }这里 8888 是 Charles 的默认端口。最重要的是badCertificateCallback 要返回 true否则 Charles 抓取 HTTPS 请求时会因为证书校验失败直接报错。这段配置只放在 debug 模式里release 包千万不要带上不然等于废掉了官方证书校验逻辑会有严重的安全风险。另外我建议团队在开发阶段统一使用同一个测试环境并且后端接口要保留足够长的日志时间。Flutter 端 LogInterceptor 会把请求头、请求体、响应体全部打到控制台调试接口对齐时非常方便。真机调试时VS Code 的控制台就能直接看到这些日志基本不需要再额外接一套日志系统。3.3 本地数据库与后端同步sqflite 的正确打开姿势Flutter 里做本地数据库首选 sqflite它是 SQLite 在 Flutter 上的封装。如果你的业务只是存少量键值对用 shared_preferences 就够了但如果涉及到列表数据、复杂查询、离线缓存一定要上数据库。我这里分享一下我常用的“本地数据库 后端同步”方案第一数据库表结构的设计要带同步状态字段。我通常会加一个 sync_status 字段0 表示未同步1 表示已同步2 表示同步失败。这样后端接口暂时不通的时候前端先把用户操作写入本地标记为未同步之后网络恢复再统一补交。第二批量操作要使用事务。sqflite 的 transaction 可以保证多条 SQL 要么全部执行成功要么全部回滚。我之前做过一个订单模块用户一次操作会同时更新订单记录、库存记录和日志表如果不放在事务里中途一旦异常退出数据库就会出现数据不一致的情况。这种问题很难被用户直接描述清楚但它会导致后续逻辑错乱排查成本非常高。第三数据库版本要谨慎管理。sqflite 的 onUpgrade 回调里每增加一个版本就要执行对应的 ALTER TABLE 语句。这里我强烈建议在正式发布前就把表结构定好千万不要在线上版本里反复修改。否则你会在升级时遇到“字段重复添加”“旧表数据迁移失败”之类的坑处理起来非常痛苦。3.4 用 Isolate 处理耗时任务避免掉帧Flutter 的 UI 线程也叫 root isolate平时我们写的 Dart 代码大部分都跑在这上面。如果你在 UI 线程里做 JSON 解析、图片压缩、大列表数据拼接都会导致界面掉帧甚至卡死。解决办法就是把这些耗时操作放到单独创建的 isolate 里去跑完成后通过 SendPort 把结果传回 UI 线程。Dart 里创建 isolate 最简单的做法是用 compute 函数适用于一次性任务final result await compute(parseLargeData, jsonString);但如果你的任务是需要长期共存的比如从数据库不断读取数据做分页用 compute 就不合适了因为它每次调用都会创建新 isolate。这时候更合理的做法是创建一个常驻 isolate用 ReceivePort 接收任务请求用 SendPort 返回结果。我这里踩过一个血泪坑在 Android 低端机上实时计算某个大列表的筛选结果直接在 UI 线程跑了三秒钟用户下滑列表时帧率掉到 10 帧以下。后来把计算逻辑丢到 isolate 里界面瞬间流畅了。做移动端开发这么久性能优化的第一个原则就是别让 UI 线程做脏活累活。3.5 内存优化从“能跑”到“不崩”Flutter 应用的内存问题不像 Android 原生那样有明确的 Activity 生命周期可以去释放但同样需要注意。我遇到最多的内存问题是三块一个是图片缓存。加载网络大图时如果不做尺寸压缩很容易撑爆内存。我的做法是配合 cached_network_image 库设置合理的内存缓存宽度和高度让图片只保留显示所需的分辨率避免原图直接加载到内存。第二个是 StreamSubscription 和 Timer 忘记取消。在 StatefulWidget 的 dispose 方法里凡是有监听的地方都要取消监听。很多人开发时只顾着写业务完全没意识到页面关了之后 Timer 还在跑结果导致页面对象无法被垃圾回收积少成多就产生了内存泄漏。第三个是频繁创建重复对象。比方说列表项里每次都 new 一个 EdgeInsets 或者 TextStyle虽然单次开销不大但滚动列表时成千上万次创建就会造成不必要的 GC 压力。正确的做法是把这些不依赖状态的对象提取为 static final整个列表共用。Flutter 的性能工具里我推荐多用 DevTools 的 Memory 面板它可以直接看到当前还存活的 isolate、所有对象的数量能够比较直观地定位内存泄漏点。我每做完一个模块都会用 DevTools 看一轮内存曲线确保页面退出后内存有明显的回落才敢放心的提交代码。4. 双端打包构建与上架从开发完到商店可下载4.1 Android 打包签名、加固与多渠道配置Android 打包的第一步是生成签名文件。用 Android Studio 自带的 Generate Signed Bundle or APK 向导就能生成 keystore 文件也可以直接用命令行工具 keytool 生成。签名文件一定要妥善保存并且要习惯性地把密码记录到公司的密码管理库里。我就见过有同事把 keystore 存在个人电脑的临时目录里结果电脑重装后一切归零应用后续想升级都升不了只能重新换签名上架后果非常严重。打包方式的建议是上架 Google Play 用 Android App Bundle 格式也就是 .aab 文件这样可以按设备配置分发包体更小。国内大部分应用市场虽然也支持 aab但我实测下来很多市场的上传配置对 aab 的支持还不算好所以国内的渠道包我仍然选择 APK 格式。多渠道打包方面如果单纯是区分不同市场的包名和渠道用 Flutter 的 --dart-define 参数会比较方便flutter build apk --release --dart-defineCHANNELmyapp然后在 Dart 代码里用 String.fromEnvironment(CHANNEL) 读取渠道名做运营统计之类的会用得到。如果你需要每个市场都有不同的图标、应用名、某些 SDK 配置那就要在 Android 工程里配置 flavor相对复杂一些但思路是一样的。上架前的加固也不是可选项。国内 Android 应用市场普遍要求过安全检测直接用未加固的包上传经常会被提示“存在风险”。市面上主流的加固方案都可以在打包后手动执行加固流程但要注意加固完成之后必须重新签名这个顺序不能反。另外加固后最好在真机上把登录、支付、分享这些核心链路都过一遍因为加固工具有时会对一些反射调用或者动态加载代码造成影响。4.2 iOS 打包开发者模式、签名证书与 TestFlightiOS 打包的核心前提是苹果开发者账号也就是 Apple Developer Program 的年度订阅。没有这个账号你只能在模拟器上运行无法在真机安装也无法上架 App Store。真机调试时会遇到“开发者模式未开启”的提示。iOS 16 之后苹果加了一道开关设备上需要到“设置 → 隐私与安全性 → 开发者模式”里手动开启否则 Xcode 或 Flutter 安装的开发版应用无法运行。这个不是报错是系统机制我的经验是提前在测试机上打开不然在真机调试时第一次跑起来会突然卡住。iOS 打包的整体流程是在 Xcode 里配置好 Team 和 Bundle Identifier然后通过 flutter build ipa 构建出 .ipa 文件。签名由 Xcode 自动管理Certificate 和 Provisioning Profile 都是账号体系内自动生成和维护的。这里面最容易出错的地方是 Bundle Identifier 不匹配。比如你在 Flutter 项目里改了 iOS 的 bundle id但 App Store Connect 里创建的 App 记录用的却不是同一个 id那么上传构建版本时会直接报错。建 App 记录之前一定要先确定好终版 bundle id。TestFlight 是上架前的必要环节。它本质上是一个内部的灰度分发平台你用 Archive 上传之后先在 App Store Connect 里添加测试员然后 TestFlight 会给测试员的手机推送邀请。我建议在所有测试设备上系统地跑一遍核心流程尤其是第三方登录和支付回调。因为这两块东西在沙盒环境下的表现和线上环境还是有差异的提前发现问题可以避免上架后才被用户吐槽。4.3 上架审核的流程与注意事项先说 Android 国内市场的上架。各主流市场的审核要求不尽相同但有几项材料是共通的应用版权证明、软件著作权证书、隐私政策页面、ICP 备案号部分市场要求。如果你是以公司名义发布还需要营业执照。这里我特别提醒隐私政策不是随便写一两句话就行的要具体说明收集了哪些个人信息、用途是什么、怎么注销账号和删除数据。我用过一次“收集设备信息用于统计”这种笼统描述结果在某个市场直接被驳回后来把每一项权限的使用场景写清楚才通过。App Store 的审核和 Android 市场不太一样它对 App 的功能完整度、UI 规范、用户隐私的提示要求都比较严。最容易踩的坑有这么几个审核员会要求“登录按钮旁边必须有可用的注册方式”如果你做了第三方登录但没给游客提供任何体验路径有被拒的风险。如果你的 App 包含用户生成内容也就是 UGC 模块必须有内容举报和屏蔽机制否则很容易被驳回。审核期间如果打开 App 崩溃基本就凉了所以上传前请务必在真机上把整个主流程完整走一遍特别是从冷启动到主页出现的阶段。App Store 审核一般需要 1 到 3 天如果被拒拒绝理由里通常会附上截图和日志。千万别慌按反馈逐条修改重新提交一般都会过的。我见过有人被拒绝一次就反复改代码结果越改越偏其实很多时候只需要改一下元数据描述或者补一句话的说明就够了。5. 开发中最常遇到的问题与排错记录5.1 常见报错速查表我整理了这段时间开发里遇到频率最高的几个问题做成了一张速查表方便你遇到时快速定位问题表现根本原因解决方法VS Code 编译报 unable to find suitable Visual Studio toolchain缺少 C 桌面开发组件安装 Visual Studio Build Tools勾选“使用 C 的桌面开发”apply flutter gradle 插件方式相关警告或报错Flutter Gradle 插件从 apply 脚本迁移到 plugins DSL在 settings.gradle 和根 build.gradle 中按新方式声明插件HTTPS 请求在抓包工具中看不到Flutter Dart VM 不走系统代理在 debug 模式 Dio 中显式设置代理地址并放开证书校验打包后安装到手机闪退多数是签名不一致或加固后未重签重新生成签名并确认打包流程签名顺序iOS 真机调试提示开发者模式未开启iOS 16 以后新增安全开关到系统设置里手动开启开发者模式升级 Flutter 版本后第三方库编译失败依赖不兼容或 Dart SDK 版本不匹配锁定 Flutter 版本不要随意升级必要时降级回原版本实际上至少一半的开发时间都会花在排错上。我个人的建议是遇到报错先看完整日志不要只盯最后几行。Flutter 的编译日志是分层的最底层的异常原因往往在最后面但真正触发问题的代码位置往往要靠中间部分的提示去定位。多按几次向上滚动把日志完整看完再动手胜过盲目去搜索错误码。5.2 几个值得长期保留的调试习惯第一真机调试永远优先于模拟器。Flutter 的热重载在模拟器上体验很好但像内存、网络、推送、相机这些能力模拟器跟真机的表现差异很大。我一般会在开发中后期开始只使用真机调试模拟器仅仅是验证布局用。第二正式发版前一定要打一个 release 包完整过一遍。很多 Flutter 应用 debug 模式一切正常release 模式却崩溃或者白屏常见原因有两个一个是混淆规则没有适配导致反射调用失败另一个是 debug 模式放宽了某些检查而 release 模式严格要求比如某些未处理的空安全类型。所以不要直接拿 debug 包去上架越早上手 release 包问题就能越早暴露。第三善用 Flutter 自带的诊断能力。比如 debug 模式下在终端执行 flutter run --profile 可以查看性能开销Maintain 的 DevTools 里能看 UI 帧率、CPU 占用和内存曲线。正常情况下Flutter 渲染的帧率应该稳定在 55 到 60 帧如果某段操作掉帧严重多半是主 isolate 被耗时工作阻塞了优先检查那块逻辑的隔离情况。第四团队协作时约定好统一的代码格式化规范。dart format 和 flutter analyze 是配合使用的高频工具。flutter analyze 能找出很多潜在的警告和错误提示我要求自己提交代码前保证分析结果无 error。这个习惯让我少踩了很多低级坑尤其是未处理的异步异常和资源泄漏类型的问题analyze 能提前发现相当一部分。第五关于 Flutter 逆向与安全方面我不建议在商业项目里存储敏感密钥在客户端代码中。Dart 代码的 AOT 编译虽然能提高逆向难度但客户端的密钥、加密逻辑只要运行在用户设备上就不可能绝对安全。凡是涉及支付回调验签、高权限接口的调用务必把关键逻辑放到服务端处理。这个意识得从头建立起而不是等 App 被人分析之后才补漏洞那代价就太大了。写在最后的个人体会从环境搭建、基础架构设计到网络请求封装、本地数据库、内存优化再到双端打包上架整个流程走下来最大的感受是Flutter 的核心价值不在于它是 GitHub 上星数最多的跨端框架而在于它让移动端开发回到了“一个需求只写一次”的状态。团队可以更专注于业务逻辑而不是在 Android 和 iOS 两端之间反复横跳。我在实际开发过程中也总结出一条经验遇到任何疑难问题先试着把问题拆小。比如构建出错了就先跑 flutter clean 再跑 flutter pub get界面卡顿了就先用 DevTools 分析帧率曲线再动手改代码。很多时候问题不是玄学而是某个环节的配置没有对齐而已。最后再分享一个小技巧发版前准备一份详细的发版自查清单把签名、版本号、隐私政策、广告标识、支付回调地址、崩溃上报开关这些项目列出来逐项确认。这套清单会让双端上架这件事变得可控、可重复也是我从这次项目里收获到的最大财富。
返回列表