这段时间处理公司App接入Flutter的业务,前前后后把混合开发的几条路子都趟了一遍。从最开始的“在原生Android页面里嵌一个Flutter活动页”,到后来“从首页直接拉起独立的Flutter二级页”,再到原生和Flutter之间来回传数据,踩了不少坑,也积累了一些经验。今天把这套Flutter混合开发流程整理出来,主要围绕三块:怎么把Android工程和Flutter工程关联起来、怎么在安卓页面里嵌入Flutter页面、怎么在安卓中启动Flutter页面。如果你正在做同样的技术选型或者准备接入,这篇内容应该能帮你少走弯路。
1. 混合开发的整体思路:不是二选一,而是渐进式融合
1.1 为什么原生App要接Flutter
先说背景。当时我们团队面对的问题是:存量Android App是Java+Kotlin混着写的,业务模块非常多,登录、支付、IM、推送全是原生实现,而且稳定跑了三四年,不可能推倒重写。但是新来的运营活动、电商频道、个人中心改版这些需求变化太频繁,原生开发一周发一版的节奏根本跟不上,产品团队催得紧,开发团队也想提高迭代效率。引入Flutter成为最现实的选择——用Flutter承载新业务模块,同时保留原有Native页面。
混合开发的核心价值在于,它让你不用做“全有或全无”的决定。你不需要先把整个App迁到Flutter,而是可以先在一个页面试水,跑通了再逐步扩大范围。比如我们上线的第一个Flutter页面是运营活动页,前一周还在做原生,切到Flutter之后,UI还原效率明显提升,热重载对调试的帮助也很大。这种渐进式迁移策略,风险小,见效快,适合大多数已有Native产品的团队。
1.2 两种关联路线:Module方式与AAR方式
在真正写代码之前,你得先想清楚一个架构问题:Android工程和Flutter工程之间,以什么方式关联?根据实际场景,最常见的两种路线是Flutter Module方式和AAR产物方式。
Flutter Module方式,本质上是把Flutter工程作为一个子模块嵌入Android工程的构建体系,Android工程在编译时直接读取Flutter源码目录,参与整体Gradle构建。这种方式适合“原生和Flutter代码由同一批人维护、需要频繁联调”的团队。它的优点是修改Flutter代码后,Android工程重新编译就能生效,调试链路短,不需要来回打产物包;缺点是Android工程和Flutter工程的耦合度比较高,如果你想把Flutter模块完全交给另一个团队维护,这种耦合会带来协作上的摩擦。
AAR方式,则是先把Flutter工程用flutter build aar命令打包成一个Android依赖库,然后像引入普通Maven依赖一样,把AAR引入原生工程。这种方式的好处是解耦非常彻底:Flutter团队和Android团队可以各自独立发版,原生工程不需要关心Flutter源码长什么样。缺点是每次Flutter代码变更后,都要重新打AAR、传仓库、更新依赖版本,迭代节奏快的阶段会显得繁琐。
我们团队最终选了Module方式,原因是团队成员同时维护Flutter代码和原生代码,联调频率极高,追求的是改完就能跑的效率。如果你们团队是前后端分离、Flutter专门由一个小组维护,那AAR方案会更合适。
1.3 集成模式选型对比
为了方便决策,我把自己调研时整理的对比表放出来,大家可以参考:
| 维度 | Module方式 | AAR方式 | FlutterView直接嵌入 |
|---|---|---|---|
| 代码联调效率 | 高,改完直接编译 | 低,需要重新打包 | 中,适合单页面 |
| 工程耦合度 | 高 | 低 | 中 |
| 业务隔离 | 一般 | 好 | 一般 |
| 接入成本 | 中 | 低 | 高 |
| 适合场景 | 团队共同维护、频繁迭代 | 独立团队协作、稳定发版 | 临时页面、Demo演示 |
这个表格里FlutterView直接嵌入属于最底层的做法,灵活性最高,但你需要自己管理FlutterEngine的生命周期,接入成本不小。后面会详细说FlutterView的使用场景,但日常混合开发我更推荐以FlutterFragment和FlutterActivity为主。
2. 前期准备:Flutter环境与Android工程关联
2.1 Flutter SDK、Android环境与多版本管理
开始之前,先把环境准备好。Flutter SDK建议下载stable渠道的稳定版,解压后设置好PATH。Windows和macOS都建议用flutter doctor检查一遍依赖,Android SDK、Android Studio、JDK这些基础环境缺一不可。这里有个小建议:如果你们团队同时维护多个Flutter项目,不同项目可能依赖不同版本的Flutter SDK,千万别在公共环境里只装一个固定版本,我后面会单独讲FVM多版本管理方案。
Flutter SDK下载慢、网络不稳定这个问题,很多新手会卡住。解决办法是配置环境变量,把PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL指向国内可访问的镜像仓库,这是官方认可的加速方式,配置完重开终端,重新执行flutter doctor就能看出来。如果你需要同时管理多个版本的Flutter,直接用FVM(Flutter Version Management),一条fvm install装指定版本,用fvm use切换项目级SDK版本,这是当前比较主流的多版本管理思路。
2.2 Module方式:把Flutter工程作为Android工程的子模块
Module方式的关联是整个混合开发的第一步,也是很多教程讲不清楚的地方。我以实际命令为例,完整走一遍流程。
首先,创建Flutter模块工程。注意这里创建的不是普通的flutter create应用工程,而是flutter create -t module模块工程:
flutter create -t module --org com.example flutter_module执行完之后,你会发现在flutter_module目录下并没有生成完整的android应用工程,而是生成了一个隐藏的.android目录,以及一个lib/main.dart入口文件。main.dart就是你的Flutter业务代码入口,.android目录是给Gradle构建用的辅助工程。
接着,在Android工程里引入Flutter模块。打开Android工程根目录的settings.gradle,加入下面这段配置:
// settings.gradle def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies') if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> properties.load(reader) } } setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'flutter_module/.android/include_flutter.groovy' ))这段代码的核心作用是:让Android工程的Gradle构建过程知道Flutter模块在哪,并把Flutter模块作为一个子工程include进来。其中include_flutter.groovy是Flutter模块自动生成的脚本,不要手动改动它。
最后,在Android工程的app/build.gradle中依赖这个Flutter模块:
dependencies { implementation project(':flutter') }这里要注意,Flutter模块对Android工程依赖的是名为flutter的Project,不是flutter_module。因为include_flutter.groovy里默认include的是Flutter SDK的gradle子工程,这个细节第一次接触时很容易踩坑。
2.3 AAR方式:把Flutter工程打成原生依赖
如果你决定走AAR路线,操作会比Module方式更简单,也更适合跨团队协作。
第一步,在Flutter模块工程中执行打包命令:
flutter build aar这个命令会自动构建debug、profile、release三种模式的AAR产物,并在控制台输出本地仓库的路径。典型输出会在flutter_module/build/host/outputs/repo目录下生成对应的Maven仓库。
第二步,在Android工程根目录的settings.gradle中配置这个本地仓库:
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url 'flutter_module/build/host/outputs/repo' } } }第三步,在app/build.gradle中按构建模式引入依赖:
dependencies { debugImplementation 'com.example.flutter_module:flutter_debug:1.0' profileImplementation 'com.example.flutter_module:flutter_profile:1.0' releaseImplementation 'com.example.flutter_module:flutter_release:1.0' }这里debug、profile、release三种模式要分开依赖,因为Flutter的AAR产物是按模式区分的。如果你只引入了release版本,本地调试时就会因为找不到对应依赖而报错。每次Flutter代码更新后,都需要重新执行flutter build aar并同步版本号,这个流程放在CI里会更省心。
2.4 Flutter Gradle插件的新版配置要求
这部分是很多人升级Flutter版本后必踩的坑。如果你在构建时看到这样一条报错:You are applying Flutter's main Gradle plugin imperatively using the apply script method, which is deprecated and will be removed,说明你还在用老式的apply plugin方式去加载Flutter Gradle插件。
如果项目使用新版Flutter(3.16及以上),Gradle插件推荐使用声明式的方式配置。在Android工程的根目录settings.gradle中加入:
pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not found in local.properties" return flutterSdkPath }() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } }然后在项目根目录build.gradle中,用pluginsDSL替代原先的apply plugin:
plugins { id "com.android.application" version "8.1.0" apply false id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false }在app/build.gradle中再应用:
plugins { id "com.android.application" id "dev.flutter.flutter-gradle-plugin" }这样配置之后,Flutter插件就会通过flutter.sdk路径自动加载,不需要手动写apply脚本。注意local.properties里的flutter.sdk指向你本地Flutter SDK的解压路径,这个文件通常不会提交到Git,团队其他成员拉代码后需要自己配置。
3. 在安卓页面中嵌入Flutter页面:从Fragment入手
3.1 FlutterFragment嵌入的基本步骤
工程关联好之后,接下来就是实战环节:在现有的安卓页面里嵌入一个Flutter页面。常见的做法是通过FlutterFragment把Flutter页面作为一个Fragment加载到原生Activity的布局中,这样做的好处是保留了原生页面的导航框架,Flutter只负责页面内容渲染,不接管整个页面栈。
先看布局,假设你想在原生页面的上半部分放Flutter内容,下半部分还是原生控件,那么布局文件里先放一个FrameLayout作为Flutter容器的占位:
<FrameLayout android:id="@+id/flutter_container" android:layout_width="match_parent" android:layout_height="300dp" /> <Button android:id="@+id/native_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="我是原生按钮" />然后在原生Activity中,把FlutterFragment动态添加到这个容器里。注意这个Activity要继承自FragmentActivity或AppCompatActivity,因为FlutterFragment依赖AndroidX FragmentManager:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentTransaction tx = getSupportFragmentManager().beginTransaction(); FlutterFragment flutterFragment = FlutterFragment.withNewEngine() .initialRoute("activity_page") .build(); tx.add(R.id.flutter_container, flutterFragment); tx.commit(); } }这里withNewEngine()表示每次创建FlutterFragment时都新建一个FlutterEngine,适用于页面不复杂、偶尔出现的场景。initialRoute是Flutter端路由名的对应参数,Flutter里可以通过命名路由解析它。
对应Flutter侧的main.dart,用onGenerateRoute来分发不同的route:
void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( onGenerateRoute: (settings) { if (settings.name == 'activity_page') { return MaterialPageRoute(builder: (context) => const ActivityPage()); } return MaterialPageRoute(builder: (context) => const HomePage()); }, ); } }这样你就能在原生页面里看到一个局部的Flutter页面了,同时原生按钮还能继续点击,完全不影响原本的交互逻辑。
3.2 FlutterEngine的选择:新建、缓存、还是共享
刚才的代码用了withNewEngine(),它是创建FlutterFragment最简单的方式,但不是最高效的方式。FlutterEngine的创建和Dart isolate的初始化是混合开发启动耗时的主要来源,如果你对页面性能有要求,就需要深入了解FlutterEngine的三种使用策略。
第一种,直接用withNewEngine()。每次创建Fragment时都新建FlutterEngine,优点是无状态、互不干扰,缺点是启动慢、内存占用高,完全不适合频繁打开的页面。
第二种,使用FlutterEngineCache缓存预创建的Engine。你可以先在Application或其他合适的时机创建好一个FlutterEngine并预热:
FlutterEngine flutterEngine = new FlutterEngine(this); flutterEngine.getDartExecutor().executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ); FlutterEngineCache.getInstance().put("my_engine", flutterEngine);然后在创建FlutterFragment时直接使用这个缓存的Engine:
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine") .build();这种方案的好处是页面打开速度极快,因为Engine和Dart isolate已经提前创建好,跳过了最耗时的初始化阶段。缺点是所有复用这个Engine的页面共享同一个Dart isolate,路由栈是共用的,你要在Flutter侧处理好页面跳转逻辑。
第三种,使用FlutterEngineGroup创建共享引擎。如果你需要多个Flutter页面或Fragment,但又不想每个都重新走一遍Engine初始化,可以用FlutterEngineGroup创建共享同构的Engine。它内部会复用同一套Dart runtime和基础组件,内存占用比多个独立Engine低,官方也推荐这种方案用于多实例场景。不过FlutterEngineGroup的使用相对复杂,适合页面数量多、且页面间需要共享资源的复杂应用,初期可以先不碰。
3.3 生命周期同步与内存释放
用FlutterFragment嵌入页面时,Fragment的生命周期和FlutterEngine是自动绑定的,不需要你手动干预。但如果你直接使用FlutterView或者手动持有FlutterEngine,内存释放就得上心了。
一个常见误区是,Activity销毁后没有释放FlutterEngine。虽然Dart有垃圾回收,但FlutterEngine持有大量原生资源(Surface、纹理、平台通道),如果不主动销毁,很容易造成内存泄漏。如果你用withNewEngine()创建了FlutterFragment,可以在Fragment的onDestroy里考虑是否需要销毁对应的Engine:
@Override public void onDestroy() { super.onDestroy(); // 如果这个engine是只为当前页面创建的,销毁前先移除缓存 FlutterEngineCache.getInstance().remove("my_engine"); flutterEngine.destroy(); }但要注意,不要在任何地方随便destroy(),如果这个Engine还被其他页面引用,销毁后会直接导致空白页或崩溃。我一般遵循一个原则:如果Engine在Application里创建且全局复用,就不在某个页面销毁;如果Engine是页面自定义创建,则页面销毁时一并销毁。
4. 在安卓中启动独立的Flutter页面
4.1 FlutterActivity启动页面
嵌入Flutter页面的另一种常见需求,是希望从某个原生按钮直接跳转到一个独立的Flutter页面,也就是说整个页面全是Flutter渲染,不保留原生页面结构。这个场景用FlutterActivity是最标准的做法。
最简单的方式是这样:
Intent intent = FlutterActivity.createDefaultIntent(this); startActivity(intent);createDefaultIntent会启动默认的FlutterActivity,并加载Flutter工程里默认的Dart入口。如果你的Flutter工程有多个入口、多个路由,就需要用withNewEngine()的构建方式传入路由参数:
Intent intent = FlutterActivity .withNewEngine() .initialRoute("goods_detail?id=1001") .buildCurrentIntent(this); startActivity(intent);启动后,Flutter侧的MaterialApp会根据路由名加载对应页面。这里补充一个细节:如果你不希望启动时出现白屏或闪屏,记得在AndroidManifest.xml中为FlutterActivity配置主题,使用Launcher页的windowBackground或者直接设置一个不透明的背景色,能明显提升启动观感。
4.2 路由、参数传递与返回结果
路由参数除了放在initialRoute里,还有一种更优雅的方式是在启动FlutterActivity时通过Intent的Extra传递数据,尤其是那些不适合放在URL里的复杂对象。比如:
Intent intent = FlutterActivity .withNewEngine() .initialRoute("user_profile") .buildCurrentIntent(this); intent.putExtra("user_id", "u_12345"); startActivity(intent);Flutter侧在onGenerateRoute里,可以通过MaterialPageRoute的settings.arguments读取Intent携带的Extra数据:
onGenerateRoute: (settings) { if (settings.name == 'user_profile') { final args = settings.arguments as Map?; final userId = args?['user_id'] ?? ''; return MaterialPageRoute(builder: (context) => UserProfilePage(userId: userId)); } return MaterialPageRoute(builder: (context) => const HomePage()); }返回结果的处理也经常被问到。原生Activity通过startActivityForResult启动FlutterActivity,Flutter页面里通过Navigator.pop带数据返回,然后原生侧在onActivityResult中接收。这里关键是频道要统一,数据格式建议约定为String、int、bool这些基础类型,避免解析复杂对象。
4.3 多页面路由与Engine复用优化
当FlutterActivity内部存在多个页面层级时(比如从Flutter首页跳转到Flutter详情页再跳到支付页),你会发现这些页面共享同一个FlutterEngine,路由栈由Flutter侧管理,原生只需要启动一次FlutterActivity。这种模式是最高效的:原生到Flutter只启动一次,后续页面跳转完全在Flutter内部完成,不会反复创建Engine和Dart isolate,页面切换体验也流畅不少。
但相应地,你要在Flutter侧维护好页面栈。Navigator.push和Navigator.pop的使用要规范,尤其是在支付、登录这类需要返回结果的场景,不要出现页面栈越积越深的问题。我见过一个项目,用户在Flutter页面里反复跳转几十层,返回键一层层弹,体验非常差。后来我们在Flutter侧统一封装了路由管理器,所有二级页面都用pushReplacement或pushAndRemoveUntil,保证页面栈深度可控。
5. 原生与Flutter的通信:MethodChannel、EventChannel
5.1 MethodChannel双向调用
嵌入Flutter页面只是第一步,页面跑起来之后,原生和Flutter之间一定需要通信。最常见的通信方式是MethodChannel,它支持Flutter调用原生方法,也支持原生调用Flutter端方法。
先看Flutter调用原生的场景。比如Flutter页面上有个按钮,点击后需要读取Android设备型号并展示,Flutter侧这样写:
import 'package:flutter/services.dart'; class NativeBridge { static const MethodChannel _channel = MethodChannel('com.example.app/bridge'); static Future<String?> getDeviceInfo() async { return await _channel.invokeMethod('getDeviceInfo'); } }原生侧,在创建FlutterEngine的地方注册对应的MethodChannel:
new MethodChannel( flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.app/bridge" ).setMethodCallHandler((call, result) -> { if (call.method.equals("getDeviceInfo")) { result.success(Build.MANUFACTURER + " " + Build.MODEL); } else { result.notImplemented(); } });这里有几个注意点。result.success只能调用一次,不能多次回调。如果回调不能立刻给出结果,也一定要保存result并在异步任务结束后调用,否则Flutter侧的Future会一直挂起,造成页面卡住感。另外,原生回调的线程默认不是主线程,如果你更新UI必须切到主线程再操作。
原生调用Flutter侧方法的场景也很常见。比如原生监听到某个广播事件,需要通知Flutter页面刷新数据。原生侧通过MethodChannel的invokeMethod调用Flutter侧注册的方法:
MethodChannel channel = new MethodChannel( flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.app/bridge" ); channel.invokeMethod("onNativeEvent", "refresh_data");Flutter侧在initState里注册监听:
@override void initState() { super.initState(); _channel.setMethodCallHandler((call) async { if (call.method == "onNativeEvent") { // 刷新页面 } return null; }); }到这里,双向调用的基本闭环就跑通了。需要注意的是,MethodChannel的method名称在整个App内要尽量唯一并统一管理,避免不同页面重复命名产生冲突。
5.2 EventChannel事件订阅
有些场景更适合用事件流来通信,比如电量变化、网络状态变化、传感器数据这类持续性的数据推送。Flutter侧用EventChannel来订阅原生事件,原生侧通过EventSink持续向Flutter发送事件。
Flutter侧订阅:
EventChannel _eventChannel = const EventChannel('com.example.app/events'); StreamSubscription? _sub; void _startListen() { _sub = _eventChannel.receiveBroadcastStream().listen((event) { // 处理事件 }); }原生侧实现StreamHandler,在onListen时开始发送事件,在onCancel时停止:
new EventChannel( flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.app/events" ).setStreamHandler(new EventChannel.StreamHandler() { @Override public void onListen(Object arguments, EventChannel.EventSink events) { // 事件开始监听时,发送当前状态 events.success("current_battery_level: 80"); } @Override public void onCancel(Object arguments) { // 停止事件发送 } });EventChannel适合单向数据流,不适合作为请求/响应式调用。我一般会在需求评审阶段就判断好数据类型:如果是“问一句答一句”,用MethodChannel;如果是“一直往外推数据”,用EventChannel。选错了通信方式,后面维护成本会成倍增加。
5.3 通道命名规范与调试技巧
混合开发的通信通道多了以后,命名不规范非常痛苦。我建议采用com.company.product.feature的格式,比如com.example.app.user、com.example.app.payment。这样在Logcat里一眼能看出是哪个业务模块在通信,排查问题不需要翻代码。
调试平台通道时,有一个特别有用的技巧。Flutter端和Android端在通道名称相同、Method名相同但参数类型不一致时,报错信息往往比较模糊。遇到这种情况,先在Flutter侧打印MethodChannel的调用参数,再在Android侧打印收到的参数,对照一下两边类型和字段名,基本能定位问题。另一个高频坑是int和double类型在Java和Dart之间自动转换的细节,比如Java的int到Dart侧变成int,但如果传到Dart侧的是null,一定要在Dart侧做好判空,不然空指针异常会让你排查半天。
6. 实战避坑:3个高频报错与解决建议
6.1 VS Code运行Flutter时找不到Visual Studio工具链
在VS Code里开发Flutter,如果你跑flutter run,底部弹出设备选择列表时没有手动指定设备,Flutter默认会把windows桌面端当作目标平台执行。这时候如果本机没有安装Windows桌面开发工具链,就会报类似这样一段错误:unable to find suitable Visual Studio toolc...,很多人第一次看到这个报错会误以为是Flutter环境坏了,其实不是。
原因很简单:Flutter需要Visual Studio的C++工作负载来编译Windows桌面端。解决方式有两个。如果你只想开发Android,在VS Code底部点击设备选择器,把目标设备切换成Android模拟器或真机,问题直接消失。如果你想保留Windows桌面端调试能力,那就安装Visual Studio Build Tools,安装时勾选“使用C++的桌面开发”工作负载,这个组件比较大,需要几分钟。
我自己开发时习惯在项目的launch.json里锁定设备类型,避免每次运行还要手动选一次设备:
{ "name": "Flutter (Android)", "request": "launch", "type": "dart", "deviceId": "emulator-5554" }这样按F5就直接跑Android模拟器,不会再误启动Windows桌面构建。
6.2 Flutter Gradle插件apply方式报错
这个报错在前面配置模块时已经提过,但实际项目里它的出现场景比你想象的更多。团队里如果有一个老Android工程,原本用的是旧版Flutter插件,某天升级了Flutter或AGP版本,构建立马报红:
You are applying Flutter's main Gradle plugin imperatively using the apply script method, which is deprecated and will be removed.
我的处理流程是:先检查根目录settings.gradle是否配置了pluginManagement,如果没有,先把Flutter SDK路径读取逻辑加进去;然后把根目录和app/build.gradle里的apply plugin: ...改成pluginsDSL方式。改完之后重新Sync,基本都能解决。这里要特别提醒:local.properties中的flutter.sdk路径必须正确,否则一切配置都白搭。因为你是用这个路径去 include Flutter的gradle插件的,路径错了找不到插件,所有关联方案都会失败。
6.3 FileProvider路径异常与分区存储
Flutter混合开发里还有一个很诡异的问题,就是App启动时直接崩溃,Logcat里报FileProvider路径异常,类似content://com.某应用.fileprovider/external_path/android/data/...这样的Uri。这个问题多出现在Android 11及以上,原因是分区存储机制限制了应用对外部存储目录的访问,而部分三方SDK(比如通讯录、客服SDK、扫码SDK)通过FileProvider分享Android/data目录下的文件时就会触发限制。
处理思路分几步。先检查AndroidManifest里是否声明了多个FileProvider,若存在authorities冲突就合并或改掉其中一个的authorities。再检查是否需要兼容旧文件访问,在manifest的application节点加android:requestLegacyExternalStorage="true",这只对旧版应用有效,并不能根治新SDK的问题。最彻底的方式是升级到适配Android 11及以上分区存储的SDK版本,或者在拿到文件路径时先复制到App私有目录,再交给FileProvider处理。
遇到这类问题,不要被Logcat里长长的content Uri吓到,先定位是哪个SDK发出的Uri,再决定是升级SDK还是改造文件处理逻辑。
6.4 多版本Flutter SDK管理的小建议
最后说一个日常工作必不可少的工具:FVM。团队里不同Android项目可能依赖不同Flutter版本,A项目用3.13,B项目用3.19,如果只靠本地装一个版本,每次切换项目都得重新安装SDK,效率极低。
FVM的简单用法如下:
# 安装指定版本 fvm install 3.19.0 # 在项目目录内指定版本 fvm use 3.19.0 # 使用FVM包装的flutter命令 fvm flutter pub get fvm flutter run关键点在于:项目工程里生成的local.properties会记录SDK路径,用FVM管理版本后,每个项目的SDK路径由FVM的符号链接统一维护,不会互相覆盖。团队协作时,建议在项目根目录提交.fvmrc文件,锁定Flutter版本,其他人拉取代码后用fvm install一键还原环境,避免因SDK版本不一致导致的构建差异。
踩过几次版本不一致的坑之后,我现在给团队定的规范是:所有Flutter相关项目必须使用FVM,且在README里写明推荐版本和fvm还原命令。这一条能省掉团队内部大量环境兼容问题。
我在实际接入Flutter混合开发的过程中,最大的体会是:混合开发不是“把Flutter塞进Android”,而是“让Flutter和Android各自发挥长处”。原生页面稳定,Flutter页面灵活,两者通过规范化的模块关联和平台通信协作,才是长期可维护的状态。如果你准备在项目里启动混合开发,我建议先拿一个低风险页面(比如运营活动页或设置页)练手,把Module关联、FlutterFragment嵌入、MethodChannel通信这三条基础流程跑通,再考虑大规模推广。最后再补充一个小技巧:接入混合开发后,每次发布前在CI环境里执行一次干净的flutter build aar,保证产物和本地开发环境一致,能提前拦截掉很多只在某台机器上能编过的隐患。