全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-07-06_Flutter混编杂谈[Android]

您的位置:首页 >> 新闻 >> 行业资讯

Flutter混编杂谈[Android] 点击关注公众号,“技术干货”及时达!?本文为稀土掘金技术社区首发签约文章 ?1. 引言?? 好一阵子没更 「Flutter」 的文章了,主要是在忙公司项目,?? 团队练习Flutter大半年??,一直都是各写各的 Demo。?? 恰逢本季度APP业务需求不多,决定练练兵,把其中一个核心但功能简单的「「数据表单录入」」模块用 Flutter 重构一波。?? 踩坑不少,趁着本期开发接近尾声进入提测,花点时间梳理下「「混编-Android」」相关的知识点,以及实际开发中遇到的问题,自己沉淀知识之余,也希望对各位读者有所启发?? 2. 混编方案2.1.1. 三端分离???♀? 大部分产品都是有历史包(??)袱(??) 的,用 「Flutter」 完全重写不太现实,大多数情况都是作为 「库或模块」 集成到现有的应用程序中,一般会采用「「三端分离」」的模式进行混编开发: 即:「不改变App原生项目的管理方式,把Flutter项目作为它的子项目」。 ?? 对此,官方有详细的集成文档 → 《Add Flutter to an existing app》(https://docs.flutter.dev/add-to-app),照着操作就行好了。当然,实际集成过程大概率会些幺蛾子的??,这里把「「Android集成Flutter子项目的两种方式」」拎出来讲一哈: 「AAR集成」:将Flutter模块打包成 「AAR文件」,可以执行 「flutter build aar」 命令 或者在 「Android Studio」 中依次点击 「Build → Flutter → Build AAR」 进行打包。这种集成方式的好处:「不需要安装Flutter SDK」,坏处是:每次修改Flutter模块都需要重新编译打包上传,而且如果Flutter用到其它三方库或插件,可能需要处理 「将多个AAR合并成一个」 的问题。「源码集成」:将Flutter模块作为 「子项目」,添加到原生项目中,这种集成方式的好处:开发调试方便,支持Hot reload,当然,「需要安装Flutter SDK」。2.1.2. Android-源码集成「??」 「开发阶段」,改动频繁,自然是采取「「源码集成」」的方式,在原生项目的 「settings.gradle」 中添加下 「Flutter项目」 的配置: //创建一个新的Binding,并将当前的Gradle对象绑定到变量gradle上 setBinding(newBinding([gradle:this])) //执行include_flutter.groovy脚本,它可以访问到当前脚本中所有变量 // //注:这里是假设flutter项目和app主项目处于同一目录/层级 //settingsDir→获取settings.gradle文件的所在目录,parentFile→获得父目录 //可以按需调整,比如flutter项目和原生项目处于同一层级,可以这样写:settingsDir.parentFile.parentFile evaluate(newFile( settingsDir.parentFile, 'flutter模块/.android/include_flutter.groovy' )) ?? 就执行下 「include_flutter.groovy」 脚本,打开看看做了啥: //①获取脚本所在目录与Flutter项目的根目录 defscriptFile=getClass().protectionDomain.codeSource.location.toURI() defflutterProjectRoot=newFile(scriptFile).parentFile.parentFile //②在gradle中包含名为flutter的项目,路径为根目录下的:.android/Flutter gradle.include":flutter" gradle.project(":flutter").projectDir=newFile(flutterProjectRoot,".android/Flutter") //③读取.android/local.properties文件,获得fluttersdk的路径 deflocalPropertiesFile=newFile(flutterProjectRoot,".android/local.properties") defproperties=newProperties() assertlocalPropertiesFile.exists(),"??TheFluttermoduledoesn'thavea`$localPropertiesFile`file."+ "\nYoumustrun`flutterpubget`in`$flutterProjectRoot`." localPropertiesFile.withReader("UTF-8"){reader-properties.load(reader)} defflutterSdkPath=properties.getProperty("flutter.sdk") assertflutterSdkPath!=null,"flutter.sdknotsetinlocal.properties" //④应用其中的module_plugin_loader.gradle脚本,完成Flutter插件的加载 gradle.applyfrom:"$flutterSdkPath/packages/flutter_tools/gradle/module_plugin_loader.gradle" ?? include包含Flutter项目,读取 「.android/local.properties」 获取flutter sdk路径,应用其中的 「module_plugin_loader.gradle」 脚本。代码懒得贴了,直接描述下这个脚本的大概逻辑: 读取 「.flutter-plugins-dependencie」 文件,此文件包含了项目中用到的所有Flutter插件信息。 对于支持Android平台的插件执行下述操作: 断言保证插件名称和路径是String类型。如果插件不需要本地构建(如:只有Dart实现的插件),跳过该插件。创建新的文件对象 「pluginDirectory」 作为插件的Android子目录,断言保证目录存在。include包含到Gradle项目中,并设置插件的项目目录。在项目加载后,根项目 「beforeEvaluate」 时,对每个 「subproject」 进行配置,如果为插件,创建一个新目录 「androidPluginBuildOutputDir」 作为子项目的构建目录。 如果存在主模块名称 「mainModuleName」,则将其设置为项目的扩展属性。 在根项目 「afterEvaluate」 时确保所有子项目都在 「:flutter」 项目配置后进行。 ?? 简单点说就是:「管理和构建Flutter向后中的Android插件」,接着原生项目的 「app/build.gradle」 需要添加下 Flutter模块的依赖: dependencies{ implementationproject(':flutter') } ?? 另外,新版Android Studio创建的Android项目默认使用 「gradle.kts」 作为构建语言,没法直接添加上述的Flutter项目配置,要么删掉它改为 「settings.gradle」。要么新建一个脚本文件,如:「flutter_settings.gradle」,把配置内容丢里头,然后 「settings.gradle.kts」 使用 apply 进行引入: apply{from("flutter_settings.gradle")} ?? 还有,如果编译过程报下下述错误: Causedby:org.gradle.api.InvalidUserCodeException:Buildwasconfiguredtoprefersettingsrepositoriesoverprojectrepositoriesbutrepository‘maven’wasaddedbypluginclass‘FlutterPlugin’ Causedby:org.gradle.api.internal.plugins.PluginApplicationException:Failedtoapplypluginclass‘FlutterPlugin’. 打开 「settings.gradle」 (或kts),把:「RepositoriesMode.FAIL_ON_PROJECT_REPOS」 改为 「RepositoriesMode.PREFER_PROJECT」 就好了~ 前者仅从 「settings.gradle」 中定义的仓库解析依赖项,如果项目级别的 「build.gradle」 中定义任何仓库,构建会失败并抛出错误。后者会先尝试从项目级别的 build.gradle 中定义的仓库解析依赖项,找不到才使用 settings.gradle 中定义的仓库。 dependencyResolutionManagement{ repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) repositories{ google() mavenCentral() } } 然后 「项目级别的build.gradle」 加下仓库配置 (??阿里云镜像源是可选的哈~): allprojects{ maven(uri("https://maven.aliyun.com/repository/public")) maven(uri("https://maven.aliyun.com/repository/google")) maven(uri("https://maven.aliyun.com/repository/gradle-plugin")) maven(uri("https://maven.aliyun.com/nexus/content/groups/public/")) maven(uri("https://maven.aliyun.com/nexus/content/repositories/jcenter")) google() mavenCentral() } } 搞完,「Gradle Sync」 没报错,就可以在原生中使用Flutter啦,打开 「AndroidManifest.xml」 注册下 「FlutterActivity」: activity android:name="io.flutter.embedding.android.FlutterActivity" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"/ 接着整个Button点击跳 「FlutterActivity」: classMainActivity:AppCompatActivity(){ overridefunonCreate(savedInstanceState:Bundle?){ super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewByIdButton(R.id.bt_open_flutter_activity).setOnClickListener{ startActivity(FlutterActivity.createDefaultIntent(this)) } } } ?? 然后你会发现,点击按钮后,得等一会儿才跳转FlutterActivity,这是因为: 默认情况下,每个FlutterActivity都会创建自己的 「Flutter Engine」,涉及到 「Dart VM 的启动」 和 「Flutter 框架的初始化」,所以需要一点时间。 一种解法是 → 使用 「预热的FlutterEngine缓存」,在应用启动时 (如 「自定义的Application类」) 创建并启动一个 「FlutterEngine」,将其缓存起来,然后在启动FlutterActivity时,获取并使用这个已预热的FlutterEngine实例: classMyApp:Application(){ lateinitvarflutterEngine:FlutterEngine overridefunonCreate(){ super.onCreate() //①创建Flutter引擎实例 flutterEngine=FlutterEngine(this) //启动Flutter的代码执行,从默认的入口点(通常是main()函数)开始。 flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) //将预热好的FlutterEngine实例放入缓存,使用my_engine_id作为其标识符 FlutterEngineCache.getInstance().put("my_engine_id",flutterEngine) } } 接着调用处 「withCachedEngine()」 使用 "my_engine_id" 对应的已预热Flutter引擎实例: startActivity(FlutterActivity.withCachedEngine("my_engine_id").build(this)) 编译运行,再次启动 「FlutterActivity」,页面打开速度快了不少。另外,「withNewEngine()」 使用新引擎,可以通过 「initialRoute()」 设置 「初始路由 (Flutter应用启动时显示哪个页面)」 : startActivity( FlutterActivity .withNewEngine() .initialRoute("/my_route")//设置初始路由 .build(this) ) 改为 「withCachedEngine()」 使用缓存引擎后,就不能设置初始路由了,因为已经在引擎预热时设置过了,默认为 「"/"」 。如果Flutter项目中没有显式设置路由表 (使用 「MaterialApp」 的 「routes」 或 「onGenerateRoute」 参数),将会加载 「MaterialApp」 的 「home」 参数所指定的页面,如:「MaterialApp(home: MyHomePage())」 ,运行后会加载 「MyHomePage」。可以通过下述代码来设置初始路由: flutterEngine.navigationChannel.setInitialRoute("your/route/here"); 然后是 「FlutterFragment」,需要添加到 「宿主Activity」 中才能使用: classMyActivity:FragmentActivity(){ companionobject{ privateconstvalTAG_FLUTTER_FRAGMENT="flutter_fragment" } privatevarflutterFragment:FlutterFragment?=null overridefunonCreate(savedInstanceState:Bundle?){ super.onCreate(savedInstanceState) setContentView(R.layout.my_activity_layout) valfragmentManager:FragmentManager=supportFragmentManager flutterFragment=fragmentManager .findFragmentByTag(TAG_FLUTTER_FRAGMENT)asFlutterFragment? if(flutterFragment==null){ //创建FlutterFragment实例 varnewFlutterFragment=FlutterFragment.createDefault() flutterFragment=newFlutterFragment fragmentManager .beginTransaction() .add( R.id.fragment_container, newFlutterFragment, TAG_FLUTTER_FRAGMENT ) .commit() } } //有时可能需要对宿主Activity转发一些信号,如回退、权限等。 overridefunonBackPressed(){ flutterFragment!!.onBackPressed() } overridefunonRequestPermissionsResult( requestCode:Int, permissions:ArrayString?, grantResults:IntArray ){ flutterFragment!!.onRequestPermissionsResult( requestCode, permissions, grantResults ) } } 新建 「FlutterFragment」 实例默认会创建新的引擎实例,同样可以调用 「withCachedEngine()」 使用缓存引擎。默认使用 「SurfaceView」 来渲染Flutter内容,也可以切换为 「TextureView」 进行渲染 (前者性能更优)。注:「SurfaceView」 不能交错再View层次结构的中间,要么最底部,要么最顶部,不然会导致视觉的异常,如遮挡问题或渲染顺序问题。 valflutterFragment=FlutterFragment.withNewEngine() .renderMode(FlutterView.RenderMode.texture)//使用TextureView渲染 .build() 最后,还有一个 「FlutterView」,相比 「FlutterActivity」 和 「FlutterFragment」 的用法要复杂多了,得手动创建一系列的自定义绑定,如: 「Activity」:确保能收到宿主Activity的生命周期事件,实现 「FlutterActivityAndFragmentDelegate」 转发,特定生命周期处理,如:Activity可见调用appIsResumed(),不可见调用appIsInactive()或 appIsPaused(),销毁时调用detachFromFlutterEngine()等。「关联FlutterEngine」:这样Dart代码才能与本地平台代码进行交互,通常通过调用FlutterView.attachToFlutterEngine(flutterEngine) 和FlutterEngine.getLifecycleChannel().appIsResumed() 等方法来完成。「其它自定义交互」:剪贴板、系统 UI 覆盖、插件等其他交互。???♀? 感兴趣的可以看下官方Demo:flutter/samples/add_to_app/android_view(https://github.com/flutter/samples/tree/main/add_to_app/android_view),关于源码集成方式就说到这~ 2.1.3. Android-AAR集成?? 直接执行命令 「flutter build aar」 来生成AAR文件,不过会打三个包: 「debug」:会打开所有断言,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。等价于:「flutter run」。「release」:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。等价于:「flutter run --release」「profile」:和release基本一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西 (如:可以连接observatory到进程)。等价于:「flutter run --profile」可以添加参数限制打包产物,比如:只打release、只支持arm和arm64平台,构建版本号为0.01: flutterbuildaar--no-debug--no-profile--target-platformandroid-arm,android-arm64--build-number0.0.1 打包完成,控制台会输出AAR的路径信息,以及如何集成: 「build.gradle」 照着复制粘贴就行了,如果是 「build.gradle.kts」 的话,要稍微改改: valstorageUrl:String=System.getenv("FLUTTER_STORAGE_BASE_URL")?:"https://storage.googleapis.com" repositories{ maven(uri("E:\Code\Android\hybrid_flutter\build\host\outputs\repo")) maven(uri("$storageUrl/download.flutter.io")) } 添加完aar依赖,Sync Projct不报错,就可以正常运行了~ 3. 混合栈管理说完混编方案,接着说下「「混合栈管理」」,即:如何处理交替出现的 「Native页面」 和 「Flutter页面」,市面上的常见方案主要分为两类: 「单引擎」:App中只创建和维护一个Flutter Engine实例,所有Flutter页面都共享这个引擎实例,「多引擎」:为每个Flutter页面都创建一个独立的引擎实例。每个 「Flutter Engine」 都运行在自己的 「Dart VM」 中,拥有自己的 「主Isolate」 (或者叫UI Isolate,它负责运行Dart代码,包括UI渲染和事件处理)。「每个Isolate都有自己的内存堆和事件循环」,即 Isolate间 「不共享内存」,它们需要通过 「消息传递」 来进行通信。对 Isolate 不了解的同学可以先看下我之前写的???♂?《八、进阶-异步编程速通??》(https://juejin.cn/post/7321910136616763430#heading-3)。 ?? 经过前面的学习,我们知道Flutter项目实际上是绘制在一个 「SurfaceView」 上的,FlutterActivity 和 FlutterFragment 只是「「承载SurfaceView的容器」」,Flutter页面间的跳转,本质上只是「「切换Surface渲染显示」」。?? 问:那 「Flutter页面怎么跳原生页面」?答:通过 「平台通道」。写个简单示例: //①Flutter端→创建MethodChannel,定义一个函数传递方法调用 import'package:flutter/services.dart'; classNativeCodeRunner{ //创建一个MethodChannel,通道名称需要与原生端匹配 staticconstMethodChannel_channel=MethodChannel('cn.coderpig.channel/native'); //定义一个函数用于打开原生Activity staticFuturevoidopenNativeActivity()async{ try{ finalStringresult=await_channel.invokeMethod('openActivity'); print(result); }onPlatformExceptioncatch(e){ print("Failedtoopennativeactivity:'${e.message}'."); } } } //②Android原生→自定义FlutterActivity,重写configureFlutterEngine处理Flutter发送过来的方法调用 importio.flutter.embedding.android.FlutterActivity importio.flutter.embedding.engine.FlutterEngine importio.flutter.plugin.common.MethodChannel classMainActivity:FlutterActivity(){ privatevalCHANNEL="cn.coderpig.channel/native" overridefunconfigureFlutterEngine(flutterEngine:FlutterEngine){ super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger,CHANNEL) .setMethodCallHandler{call,result- if(call.method=="openActivity"){ //在这里启动你的Activity valintent=Intent(this,YourNativeActivity::class.java) startActivity(intent) //可选:向Flutter返回结果 result.success("Activityopened") }else{ result.notImplemented() } } } } //③Flutter端调用 ElevatedButton( onPressed:(){ NativeCodeRunner.openNativeActivity(); }, child:Text('OpenNativeActivity'), ) ?? 了解完页面怎么互跳,接着捋下市面上的常见混合栈管理方案~ 3.1. 官方 FlutterEngineGroup (多引擎方案)《Multiple Flutter screens or views》(https://docs.flutter.dev/add-to-app/multiple-flutters)中提到在Android和iOS中添加多个Flutter实例,主要使用API「「FlutterEngineGroup」」来构造Flutter引擎实例,而非前面使用的 「FlutterEngine的构造函数」。原因: ?使用 FlutterEngineGroup 时,多个FlutterEngine 实例可以共享一些底层资源和配置 (如 GPU 上下文、字体度量(font mertics)、隔离的现场快照),性能更佳,更快的首次渲染速度,更低的内存占用。 ?官方给了一个Demo → multiple_flutters(https://github.com/flutter/samples/tree/main/add_to_app/multiple_flutters) 打开看看具体的玩法,看下Android原生端,自定义Application类初始化了一个 「FlutterEngineGroup」 实例: importandroid.app.Application importio.flutter.embedding.engine.FlutterEngineGroup classApp:Application(){ lateinitvarengines:FlutterEngineGroup overridefunonCreate(){ super.onCreate() engines=FlutterEngineGroup(this) } } 接着定义了一个 「Flutter和Android共享」 的单例/可观察的DataModel: interfaceDataModelObserver{funonCountUpdate(newCount:Int)} classDataModel{ companionobject{valinstance=DataModel()} privatevalobservers=mutableListOfWeakReferenceDataModelObserver() publicvarcounter=0 set(value){ field=value for(observerinobservers){ observer.get()?.onCountUpdate(value) } } funaddObserver(observer:DataModelObserver){observers.add(WeakReference(observer))} funremoveObserver(observer:DataModelObserver){ observers.removeIf{ if(it.get()!=null)it.get()==observerelsetrue } } } 然后是最核心的 「EngineBindings」 ,在Android和Flutter端搭一个条"「桥」",使得两端能够互相通信和数据同步: importandroid.app.Activity importio.flutter.FlutterInjector importio.flutter.embedding.engine.FlutterEngine importio.flutter.embedding.engine.dart.DartExecutor importio.flutter.plugin.common.MethodChannel interfaceEngineBindingsDelegate{ funonNext() } classEngineBindings(activity:Activity,delegate:EngineBindingsDelegate,entrypoint:String): DataModelObserver{ valchannel:MethodChannel valengine:FlutterEngine valdelegate:EngineBindingsDelegate init{ valapp=activity.applicationContextasApp //①创建DartEntrypoint实例,需要延迟加载,避免在FlutterEngineGroup创建前就创建它 valdartEntrypoint= DartExecutor.DartEntrypoint( FlutterInjector.instance().flutterLoader().findAppBundlePath(),entrypoint ) //②使用App中的FlutterEngineGroup实例-engines创建并运行一个FlutterEngine实例 engine=app.engines.createAndRunEngine(activity,dartEntrypoint) this.delegate=delegate //③初始化MethodChannel实例,通道名称为:multiple-flutters channel=MethodChannel(engine.dartExecutor.binaryMessenger,"multiple-flutters") } //设置平台通道和DataModel的消息连接 funattach(){ DataModel.instance.addObserver(this) channel.invokeMethod("setCount",DataModel.instance.counter) channel.setMethodCallHandler{call,result- when(call.method){ "incrementCount"-{ DataModel.instance.counter=DataModel.instance.counter+1 result.success(null) } "next"-{ this.delegate.onNext() result.success(null) } else-{ result.notImplemented() } } } } //移除平台通道和DataModel的消息连接 fundetach(){ engine.destroy(); DataModel.instance.removeObserver(this) channel.setMethodCallHandler(null) } //DataModel中的计数更新时,通过MethodChannel发送新的计数值,就通知Flutter端 overridefunonCountUpdate(newCount:Int){ channel.invokeMethod("setCount",newCount) } } 接着是使用 「FlutterActivity」 来展示 Fluttre页面 → 「SingleFlutterActivity」: classSingleFlutterActivity:FlutterActivity(),EngineBindingsDelegate{ privatevalengineBindings:EngineBindingsbylazy{ EngineBindings(activity=this,delegate=this,entrypoint="main") } //创建时建立与Flutter引擎的链接 overridefunonCreate(savedInstanceState:Bundle?){ super.onCreate(savedInstanceState) engineBindings.attach() } //销毁时断开与Flutter引擎的链接 overridefunonDestroy(){ super.onDestroy() engineBindings.detach() } //重写此方法确使用的是EngineBindings创建的FlutterEngine实例 overridefunprovideFlutterEngine(context:Context):FlutterEngine?{ returnengineBindings.engine } //接收Flutter端命令,原生端执行的操作,比如这里是打开MainActivity overridefunonNext(){ valflutterIntent=Intent(this,MainActivity::class.java) startActivity(flutterIntent) } } 接着是使用两个垂直显示的 「FlutterFragment」 来展示Flutter页面 → 「DoubleFlutterActivity」 classDoubleFlutterActivity:FragmentActivity(),EngineBindingsDelegate{ //①定义两个EngineBindings来管理两个不同的Flutter引擎,懒加载初始化 //并指定定了不同的入口点(加载并运行不同的Dart代码) privatevaltopBindings:EngineBindingsbylazy{ EngineBindings(activity=this,delegate=this,entrypoint="topMain") } privatevalbottomBindings:EngineBindingsbylazy{ EngineBindings(activity=this,delegate=this,entrypoint="bottomMain") } privatevalnumberOfFlutters=2//显示Flutter视图的数量 privatevalengineCountStart:Int//当前Activity的引擎计数起始值 privatecompanionobject{ varengineCounter=0 } init{ engineCountStart=engineCounter engineCounter+=numberOfFlutters } overridefunonCreate(savedInstanceState:Bundle?){ super.onCreate(savedInstanceState) valroot=LinearLayout(this) root.layoutParams=LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT ) root.orientation=LinearLayout.VERTICAL root.weightSum=numberOfFlutters.toFloat() valfragmentManager:FragmentManager=supportFragmentManager setContentView(root) for(iin0untilnumberOfFlutters){ valengineId=engineCountStart+i valcontainerId=12345+engineId valflutterContainer=FrameLayout(this) root.addView(flutterContainer) flutterContainer.id=containerId flutterContainer.layoutParams=LinearLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, 1.0f ) //②根据不同的索引,把不同的引擎实例存储到FlutterEngineCache中,以便通过ID访问 valengine=if(i==0)topBindings.engineelsebottomBindings.engine FlutterEngineCache.getInstance().put(engineId.toString(),engine) //③使用缓存ID创建一个FlutterFragment实例 valflutterFragment= FlutterFragment.withCachedEngine(engineId.toString()).buildFlutterFragment() fragmentManager .beginTransaction() .add(containerId,flutterFragment) .commit() } //④与两个Flutter引擎建立连接 topBindings.attach() bottomBindings.attach() } overridefunonDestroy(){ //⑤循环,通过引擎ID,从FlutterEngineCache移除引擎,调用deatch()断开与引擎的连接。 for(iin0untilnumberOfFlutters){ valengineId=engineCountStart+i FlutterEngineCache.getInstance().remove(engineId.toString()) } super.onDestroy() bottomBindings.detach() topBindings.detach() } //接收Flutter端命令,原生端执行的操作,比如这里是打开MainActivity overridefunonNext(){ valflutterIntent=Intent(this,MainActivity::class.java) startActivity(flutterIntent) } } MainActivity启动这两个Activity就不用说了,看下Flutter端打开Activity的相关代码: //①三个不同的入口点 voidmain()=runApp(constMyApp(color:Colors.red)); @pragma('vm:entry-point') voidtopMain()=runApp(constMyApp(color:Colors.green)); @pragma('vm:entry-point') voidbottomMain()=runApp(constMyApp(color:Colors.blue)); // class_MyHomePageStateextendsStateMyHomePage{ int?_counter=0; lateMethodChannel_channel; @override voidinitState(){ super.initState(); //①初始化MethodChannel实例,设置原生调Flutter方法的具体实现,比如这里刷新值 _channel=constMethodChannel('multiple-flutters'); _channel.setMethodCallHandler((call)async{ if(call.method=="setCount"){ setState((){ _counter=call.argumentsasint?; }else{ throwException('notimplemented${call.method}'); } } //②值+1,刷新原生的计数器(执行方法调用) void_incrementCounter(){ _channel.invokeMethodvoid("incrementCount",_counter); } @override Widgetbuild(BuildContextcontext){ //... TextButton(onPressed:_incrementCounter,child:constText('Add')), //③调用原生跳页面的方法 TextButton( onPressed:(){ _channel.invokeMethodvoid("next",_counter); }, child:constText('Next'), ), } //... } ?? 然后有个问题:Flutter通过Channel与原生通信,每个端都需要维护一套 「协议规范」,多端协作容易出问题,比如某个MethodCall,Android实现了,iOS没实现,Flutter端调用就会报平台方法未定义的异常。对此,官方发布了 pigoen 库来帮我们解决这个问题 → 「通过一套协议生成多端协议代码」。 ?? 集成方法也很简单,键入命令:「dart pub add --dev pigeon」 装下库,或者打开 「pubspec.yaml」 添加库依赖: dev_dependencies: pigeon:^20.0.2 然后创建一个 「桥配置文件」,如 「pigeons/messages.dart」: import'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( ///Dart端 dartOut:'lib/pigeons/pigeon.dart', dartOptions:DartOptions(), //dart文件包名 //dartPackageName:'pigeon_example_package', //文件头 //copyrightHeader:'pigeons/copyright.txt', ///Android端 kotlinOut:'./android/app/src/main/kotlin/cn/coderpig/plugins/CPFlutterBridget.kt', kotlinOptions:KotlinOptions(), //javaOut:'android/app/src/main/java/cn/coderpig/plugins/CPFlutterBridget.java', //javaOptions:JavaOptions(), ///iOS端 //objcHeaderOut:'../xxx/Flutter/CPFlutterBridget.h', //objcSourceOut:'../xxx/Flutter/CPFlutterBridget.m', //objcOptions:ObjcOptions(), //swiftOut:'ios/Runner/CPFlutterBridget.g.swift', //swiftOptions:SwiftOptions(), ///Windows端 ///cppOptions:CppOptions(namespace:'pigeon_example'), //cppHeaderOut:'windows/runner/messages.g.h', //cppSourceOut:'windows/runner/messages.g.cpp', )) ///传递参数类型 classCommonParams{ String?pageName; MapString?,Object??arguments; } classApiParams{ String? MapString?,Object??arguments; } ///原生端提供的方法 @HostApi() abstractclassMessageHostApi{ ///push至原生页面,参数:页面名称、参数 voidpushNativePage(CommonParamsparams); ///pop出当前页面,预留参数,可通过params.pageNamepop到指定页面 voidpopPage(CommonParams?params); ///通过Key获取本地化文本数据(同步) StringgetLocalizedText(String?key); ///Flutter通过URL和arguments调用原生端接口,异步返回数据给Flutter端 @async MaprequestNativeApi(ApiParamsapiParams); ///是否允许开启Native页面的原生手势返回效果 voidenablePopRecognizer(boolenable); } ///Flutter端提供的方法 @FlutterApi() abstractclassMessageFlutterApi{ StringflutterMethod(String?aString); } 执行 「dart run pigeon --input pigeons/message_api.dart」 生成相关文件,比如上面配置的Kotlin文件: 打开看下Android原生端,自动生成了一个接口: flutter端: pigeon自动帮我们实现了桥接方法,Android端实现MessageFlutterApi接口,按需重写对应方法即可,具体使用可以自行查阅下 《Pigeon Examples》。 ?? 我们并没有采用官方的多引擎方案,主要是网上关于它的实践文章不多,怕踩坑,小团队写业务的人都不够用,哪还敢给自己挖坑啊。在掘金看到这篇踩坑记录的文章《Flutter 多引擎渲染,在稿定 App 的实践(三):躺坑篇》,感兴趣可以看看~ 3.2. 闲鱼 flutter_boost (单引擎方案)?? flutter_boost 是我们最终采用的混合栈管理方案,原因如下: 组内之前写的Flutter项目,就是用 「flutter_boost」,有踩过下小坑,但问题不大。此次混编业务不复杂:「Flutter只用做渲染UI和处理交互逻辑,数据都来源于Native端(MethodChannel)」 。「flutter_boost」 代码开源透明,用户基数大,网上相关资料比较多,虽然有点issues,但一直有在迭代更新。???♀? 集成直接撸官方文档《各平台安装》,路由跳转得用它这套《基本路由API部分》,后续有时间扒下源码,贴个网上摘录的原理片段: ?在页面切换时,Flutter View 与 Flutter Engine 进行attach和detach操作。页面导航由Native端驱动,根据其生命周期事件,通过 「Channel」 通知Flutter端响应页面上屏等逻辑。对于每个Flutter页面,Native端都会有一个 「FlutterViewContainer」 实例与之对应,Dart端则对应一个 「BoostContainer」 实例,两者由 「FlutterContainerManager」 进行管理,通过通信机制保持生命周期一致。哪个页面需要显示,Native端就将对应的 「FlutterViewContainer」 Push进导航栈,同时将Flutter引擎attach上。 ??? 说个自己在实际开发中踩的坑吧,也是弄了大半天才定位到问题... 就我们的表单录入,需要一个 「定时保存」 的功能,每隔5s,保存下用户录入的数据。?? 这不简单: ?「MethodChannel」 写个调原生读写文件的方法,定时器定时执行就好了 ??? 不用五分钟就把代码写出来了: ///保存草稿 staticFutureboolsaveDraft(intcategory,MapString,dynamicdraftJson)async= await_channel.invokeMethod('saveDraft',{'category':category,'draftJson':draftJson}); ///读取草稿 staticFutureStringreadDraft(intcategory)async= await_channel.invokeMethod('readDraft',{'category':category}); ///定时器混入类 mixinTimerMixinTextendsStatefulWidgetonState{ Timer?_timer; //Widget移除时取消定时器 @override voiddispose(){ _timer?.cancel(); super.dispose(); } voidstartTimer(FuturevoidFunction()callback){ if(_timer!=null){ hblog("定时器初始化过了"); return; } _timer=Timer.periodic(constDuration(seconds:10),(Timert)async{ try{ hblog("【${identityHashCode(_timer)}】执行异步任务"); awaitcallback(); }catch(e){ hblog("定时任务执行异常:$e"); } } } ///定时器混入类 mixinTimerMixinTextendsStatefulWidgetonState{ Timer?_timer; //当Widget移除时取消定时器 @override voiddispose(){ _timer?.cancel(); _timer=null; super.dispose(); } voidstartTimer(FuturevoidFunction()callback){ if(_timer!=null)return; _timer=Timer.periodic(constDuration(seconds:5),(Timert)async{ try{ awaitcallback(); }catch(e){ print("定时任务执行异常:$e"); t.cancel(); } } } ///调用处 class_TestWidgetStateextendsStateTextWidgetwithTimerMixin{ @override initState(){ super.initState(); startTimer(()=saveData()); } } ?? 然后 「主项目」 一运行,BUG就来了: 刚打开APP,定时器就开始计时,TM,Flutter页面都还没打开啊?????而且有 「多个定时器」 实例在那里计时。?? em... 尝试下写个 「单例」? ///单例计时器 classSingletonTimer{ staticSingletonTimer?_instance; Timer?_timer; SingletonTimer._(); staticSingletonTimergetinstance=_instance??=SingletonTimer._(); voidstartTimer(FuturevoidFunction()callback){ _timer?.cancel(); _timer=Timer.periodic(constDuration(seconds:10),(Timert)async{ try{ awaitcallback(); }catch(e){ print("Errorinsingletontimer:$e"); } } voidcancelTimer(){ _timer?.cancel(); } } ???♀? 再次运行,还是同样的结果,然后单独运行 「Flutter模块」,定时器又能正常工作,em... 那大概率就是 「flutter_boost」 的坑了,在它的Github仓库搜了下issues,关键词:timer、定时器等,没有找到相关的话题,自己又改Flutter代码、断点、打Log,折腾了好一阵子都没定位到原因。 ?? 此时脑海突然想过一个念头??,会不会是flutter_boost的使用方法不对,意外创建了多个对象?于是我又看回主项目的 「自定义Application」 类,集成方式和官方文档一模一样啊... 然后看到了云信SDK的初始化代码: ?? 判断主进程才初始化?卧槽,难不成是因为 「多进程导致onCreate()执行多次」,间接导致 「FlutterBoost.instance().setup()」 执行了多次?直接AS打开apk文件,定位到AndroidManifest.xml,搜索: 「android:process="」 擦,给flutter_boost初始化部分的代码加上 「是否处于主进程的判断」 试试看: //判断当前进程是否为主进程 funContext.isMainProcess()=this.packageName==this.getCurrentProcessName() //获取当前进程名称 funContext.getCurrentProcessName():String?{ valpid=android.os.Process.myPid() valactivityManager=this.getSystemService(Context.ACTIVITY_SERVICE)asActivityManager for(appProcessinactivityManager.runningAppProcesses){ if(appProcess.pid==pid){ returnappProcess.processName } } returnnull } //判断处于主进程才执行flutter_boost的初始化 if(this.isMainProcess()){ //执行flutter_boost的初始化 } ?? 这样改,「创建多个定时器实例」 的问题是解决了,但 「刚打开App计时」 的问题依旧存在。一时也没啥方向,于是翻起了官方issues,然后看到了这个:为什么应用初始化的时候默认会先生成一个空路由呢: ?????? 立马看下Flutter项目里路由出计划部分的代码,卧槽,在这里就创建了? 在这里打个Log,运行看日志,果然是App启动后就创建了,return改为返回另一个页面,运行再试试。?? 打开App时定时器没有开始计时,打开数据录入页面才开始计时,?? 就是这里的锅,真的坑 ?? 后面同事遇到一个页面退出Riverpod的Provider依旧存在的BUG也是这个原因... 3.3. flutter_thrio (单/多引擎)foxsofter/flutter_thrio,亮点是 「支持Flutter混合栈跨栈路由」,与 「flutter_boost」 每次 「页面切换」 Native端都会创建一个新的页面放入导航栈不同,「flutter_thrio」 的 Flutter页面内部的切换由 「Flutter」 自带的Navigator 来管理,Native 端导航栈不创建对应的页面容器,这样做的好处是节省部分内存。「flutter_thrio」 的 「三端的页面切换」 逻辑非常统一,均采用基于url进行页面跳转。工作模式既支持单引擎,也支持多引擎,而且不存在对引擎带代码的侵入式更改。目前有再更新,???♂? 库的优劣,README.md 已经说得很详细了,感兴趣的读者可自行测试~ 3.4. 其它《即将开源 | 让Flutter真正支持View级别的混合开发》?? 2019的文章,现在也没看到开源...《Flutter混合栈路由实践与优化》?? 腾讯心悦的TRouter方案,同样没开源...gtbluesky/fusion:单引擎,亮点是:应用在后台被系统回收,所有Flutter页面均可正常恢复,而且适配了HarmonyOS 5.0(12)+??。wangkunhui/min_stack_manager:单引擎,混合栈的最小实现,代码稍微简单点,想自己折腾可以借鉴~4. 架构 & 状态管理?? 个人感觉,「Flutter」 天然适合 「MVVM」 架构,Flutter 的架构设计就强调了组件(Widget)的 「声明式UI」 和 「响应式编程模型」,这与 MVVM 中的 「数据绑定」 和 「UI自动更新」 非常契合。 「Model」:数据层,「负责数据/状态的管理」 (如数据的获取、存储、修改等操作)。「View」:视图层,Flutter中由一系列的Widget组成,负责展示应用UI,并接收用户操作,但不直接处理业务逻辑,而是将用户操作转发给ViewModel来处理。「ViewModel」:连接View和Model的桥梁,从Model层获取数据,处理业务逻辑,然后以合适的形式提供给View。通过 「数据绑定」,可以使得 Model 的变化自动反应到View上,同时也处理来自View的用户操作。Flutter中,通常是一个以 「ChangeNotifier」 或其它状态管理方案(如Riverpod、Bloc 等)实现的类。?? Talk is cheap, show you the code. 基于 「MVVM模式」 写个最简单的计数器例子,让大伙感受下 「业务逻辑」 和 「UI」 的分离,先是 「Model」 → 存储数据/状态和业务逻辑: classCounterModel{ int_counter=0; intgetcounter=_counter; voidincrement(){ _counter++; } } 接着是 「ViewModel」,这里不使用 「ChangeNotifier」,而是手动管理监听器: import'counter_model.dart'; classCounterViewModel{ finalCounterModel_model=CounterModel(); Function()?_onChanged; intgetcounter=_model.counter; voidincrement(){ _model.increment(); _onChanged?.call(); } //添加监听器 voidaddListener(Function()listener){ _onChanged=listener; } //移除监听器 voidremoveListener(){ _onChanged=null; } } 最后是 「View」 → 用 StatefulWidget 来管理 ViewModel实例,并在合适的时机调用 「setState()」 来更新 UI: import'package:flutter/material.dart'; import'counter_view_model.dart'; voidmain(){ runApp(MyApp()); } classMyAppextendsStatelessWidget{ @override Widgetbuild(BuildContextcontext){ returnMaterialApp(home:CounterPage()); } } classCounterPageextendsStatefulWidget{ @override _CounterPageStatecreateState()=_CounterPageState(); } class_CounterPageStateextendsStateCounterPage{ finalCounterViewModel_viewModel=CounterViewModel(); @override voidinitState(){ super.initState(); _viewModel.addListener((){ //当ViewModel通知更新时,调用setState更新UI setState(() } @override voiddispose(){ _viewModel.removeListener(); super.dispose(); } @override Widgetbuild(BuildContextcontext){ returnScaffold( appBar:AppBar(title:Text('MVVMwithoutProvider')), body:Center( child:Column( mainAxisAlignment:MainAxisAlignment.center, children:WidGET@[ Text('您点击按钮的次数:'), Text( '${_viewModel.counter}', style:Theme.of(context).textTheme.headline4, ), ElevatedButton( onPressed:()=_viewModel.increment(), child:Text('增加'), ), ], ), ), } } 当然,上述代码只是用于演示,实际开发中妥妥得上 「状态管理库」,这里我们选的 Riverpod,主要还是组员更熟悉这个库??,虽然官方文档写得有点乱,但不妨碍这个库的好用,前提是你弄清楚具体怎么用???♀?。想了解这个状态管理库的童鞋,墙裂建议阅读下我之前些的《十五、玩转状态管理之——Riverpod使用详解》和 《十七、实战进阶-用 ViewModel 来分离 UI & 逻辑》。?? 分享两个遇到的UI问题,感觉读者也可能会遇到~ 4.1. Q1:Riverpod + ListView 数据改变UI刷新?? 数据改变可细分为 「列表长度」 和 「列表项内容」 的变化,随手写个简单Demo: ///test_list_model.dart classTestListModel{ ListListItemModellist; TestListModel({requiredthis.list}); } classListItemModel{ Stringtitle; StringsubTitle; ListItemModel({requiredthis.title,requiredthis.subTitle}); } ///test_list_vm.dart import'package:xxx/test/list/test_list_model.dart'; import'package:riverpod_annotation/riverpod_annotation.dart'; part'test_list_vm.g.dart'; @riverpod classTestListVMextends_$TestListVM{ @override TestListModelbuild()=TestListModel(list:[ ListItemModel(title:'title1',subTitle:'subTitle1'), ListItemModel(title:'title2',subTitle:'subTitle2'), ListItemModel(title:'title3',subTitle:'subTitle3'), ListItemModel(title:'title4',subTitle:'subTitle4'), ListItemModel(title:'title5',subTitle:'subTitle5'), ListItemModel(title:'title6',subTitle:'subTitle6'), ListItemModel(title:'title7',subTitle:'subTitle7'), ListItemModel(title:'title8',subTitle:'subTitle8'), ListItemModel(title:'title9',subTitle:'subTitle9'), ListItemModel(title:'title10',subTitle:'subTitle10') //移除列表项 voidremoveItem(intindex){ if(index0||index=state.list.length)return; state.list.removeAt(index); } //更新列表项内容 voidupdateItem(intindex){ if(index0||index=state.list.length)return; state.list[index].subTitle="${DateTime.now().millisecondsSinceEpoch}"; } } ///test_list_page.dart import'package:flutter/material.dart'; import'package:xxx/flutter_riverpod.dart'; import'package:xxx/test/list/test_list_model.dart'; import'package:xxx/test/list/test_list_vm.dart'; voidmain(){ runApp(constProviderScope(child:TestListRefreshPage())); } classTestListRefreshPageextendsConsumerStatefulWidget{ constTestListRefreshPage({super.key}); @override ConsumerStateConsumerStatefulWidgetcreateState()=_TestListRefreshState(); } class_TestListRefreshStateextendsConsumerStateTestListRefreshPage{ lateTestListModelmodel; latefinalvm=ref.read(testListVMProvider.notifier); @override Widgetbuild(BuildContextcontext){ model=ref.watch(testListVMProvider); returnMaterialApp( title:'TestListRefresh', theme:ThemeData( colorScheme:ColorScheme.fromSeed(seedColor:Colors.deepPurple), useMaterial3:true, ), home:Scaffold( appBar:AppBar( title:constText('TestListRefresh'), ), body:Column( children:[ Expanded( child:ListView.builder( itemCount:model.list.length+1, itemBuilder:(context,index){ if(index==0){ return_buildHeader(); }else{ return_buildItem(index-1); } }, ), ) ], ), } //构建表头 Widget_buildHeader(){ returnContainer(alignment:Alignment.center,child:constText("表头")); } //构建列表项 Widget_buildItem(index){ returnListTile( title:Text(model.list[index].title), subtitle:Text(model.list[index].subTitle), trailing:Row( mainAxisSize:MainAxisSize.min, children:WidGET@[ IconButton( icon:constIcon(Icons.edit), onPressed:(){ //更新列表项 vm.updateItem(index); }, ), IconButton( icon:constIcon(Icons.delete), onPressed:(){ //移除列表项 vm.removeItem(index); }, ), ], ), } } 运行效果如下: 此时点击列表项的 「编辑和删除按钮」 却没任何变化,因为没有赋予 「.state属性」 一个 「新值」,Riverpod 通过 「等值比较」 来判断 「新旧值是否相等」,从而决定 「是否通知监听器并触发UI重建」。「基本数据类型」 int、double、String等),==比较的是 「值的相等」,而 「自定义对象」,默认比较的是 「两个对象是否为同一个实例」。当然,你可以 「重写==操作符和hashCode属性」 来定制自定义对象的等值比较。所以,修改下上述代码,给 「.state」 赋一个新值即可解决: ?? OK了,点击删除和编辑都能正常刷新列表,不过这种直接创建新对象的方法,你需要指定所有字段的值,即便大多数字段的值没发生变化。一种常规解法是,定义 「copyWith()」 方法创建当前对象的一个副本,并修改需要变化的属性。 classTestListModel{ ListListItemModellist; TestListModel({requiredthis.list}); //定义copyWith() copyWith({ListListItemModel?list})=TestListModel(list:list??this.list); } //调用处 state=state.copyWith(list:state.list); ?? 当对象属性很多时,手写copyWith()同样会写到头皮发麻??,建议搭配 freezed 库来简化Model类的定义,它可以自动生成==、hashCode()、toString()、copyWith() 等方法,极大减少了样板代码的数量。改改Model类: 执行 「flutter pub run build_runner build --delete-conflicting-outputs」 生成相关代码,回到 「TestListVM」,代码却报错了: 错误信息: 点进去发现freezed只生成了属性的get方法,并没有生成set方法: ???♀? 因为 「freezed」 的核心设计理念是 「帮助开发者创建不可变的数据模型」,强制使用 「copyWith()」 来更新对象,以增强代码的安全性和可维护性。?? 如果希望使用 「freezed」 生成相关代码,「属性是可变的」,可以使用 @unfreezed 注解,对于不可变的属性可以标记为 「final」,生成的代码不会重写==和hashCode。修改后的代码: 修改完运行,编辑可以,删除又报错了: 原因很清楚:「试图从一个不可修改的列表中删除元素」,state.list 是一个不可修改的列表,我们可以新建一个包含原列表所有元素的列表,然后在新列表上进行修改操作。修改后的代码: ?? 然后点击删除也能正常刷新啦,功能虽然实现了,但并不是最优,在构建列表项的 「_buildItem()」 加个打印日志,可以发现对 「单个列表项」 的编辑操作,触发了整个ListView的重建: 其中一种解法是为 「每个列表项定义一个Provider」: @riverpod classListItemVMextends_$ListItemVM{ @override ListItemModelbuild(Stringtitle,String?subTitle)=ListItemModel(title:title,subTitle:subTitle??''); voidupdateItem(){ state=state.copyWith(subTitle:"${DateTime.now().millisecondsSinceEpoch}"); } } 修改下构建列表项处的代码: ?? 运行后,点击编辑,只有对应的列表项会触发刷新,在长列表的场景有助于提高性能。 4.2. Q2:ListView中的同类型Widget错误关联State「场景」:页面ListView中有多个相同类型的自定义多选组件 (每行最多三个),根据状态变化,需要控制控制显示隐藏。没有使用Visibilty组件包裹,直接 if(xxx) 条件成立,创建对应多选组件。 「结果」:A组件有3个选项,B组件有6个选项,状态改变列表刷新,条件不成立,不创建A组件,创建B组件。然后发现,B组件只有前3个选项能点击,后面3个选项没法点击。 ???♂? 《十、进阶-玩转各种Key??》已经说过这个问题,给组件定义一个Key即可解决~ 简述下 「Widget树重建」 涉及到的方法调用流程: 「更新Element」:Flutter框架会遍历Widget树,对于每个Widget,通过调用 「Element.update()」 来决定是否需要更新该Widget对应的Element。「Widget比较」:「Element.update()」 中会调用 「Widget.canUpdate()」 来比较新旧Widget是否相同,判断依据是新旧Widget的 「key和runtimeType」,两者都相同,「canUpdate()」 返回true,表示 「复用旧的Element」,只更新下关联的Widget。5. 调试5.1. Flutter模块-热重载「关闭应用 (需要杀进程)」 ,在「「Flutter模块」」终端输入命令「「flutter attach」」出现如下输出: 打开应用,稍等片刻会出现下述内容: ?? 修改完flutter代码,输入r就能使用flutter的热重载啦: 5.2. Flutter模块-断点调试Android Studio 打开「「Flutter模块」」的代码,下断点,然后点击「「Flutter Attach」」的按钮: 执行到断点代码就会弹出调试相关的信息啦~ 6. 打包APK6.1. 源码集成?? 这种集成方式,如果能在手机上运行,「本地手动打包」 基本是没问题的,就是麻烦,可以安排下 「CI(持续集成)」 自动打包,在编译主项目前,先拉取下最新的 「flutter模块代码」 执行相关进行构建,最后再编译主项目。大概的脚本如下: #获取当前脚本的绝对路径及父目录 SCRIPT_PATH="$(cd"$(dirname"${BASH_SOURCE[0]}")"pwd)" SCRIPT_PARENT_DIR=$(dirname"$SCRIPT_PATH") if[!-d"$SCRIPT_PARENT_DIR/flutter项目"then echo"Flutter子项目不存在,开始Clone" gitclonehttp://git.xxx.xxx.com/xxx/xxx/flutter项目.git"$SCRIPT_PARENT_DIR/flutter项目" echo"Fluter子项目Clone完毕" fi #CD到项目中,拉取最新代码 cd"$SCRIPT_PARENT_DIR/flutter项目" gitcheckout. gitcheckoutdevelop gitpull--rebaseorigindevelop #fluttersdk的路径 FLUTTER_CMD="/Users/xxx/flutter/bin/flutter" #执行flutter构建相关命令,后面是我用到的build_runner来生成.g.dart等文件 $FLUTTER_CMDpubget$FLUTTER_CMDpubrunbuild_runnerbuild--delete-conflicting-outputs #主项目构建 cd"$SCRIPT_PATH/主项目" bashgradlewclean bashgradlewassemble 6.2. AAR集成先明确一点: ?「Android Library」 依赖了其它三方库,对于 「project」 和 「远程依赖」 只会打包引用而不会打包源码和资源 ?? ?比如我们的项目执行 「flutter build aa」r 后就生成了4个AAR: 6.2.1. 多AAR依赖就是批量把生成的AAR都Push到Maven仓库,然后 「原生主项目」 再添加上第三方依赖(远程或本地),有需要的可以借鉴下这个脚本自由发挥,比如:变化的aar其实只有 「flutter模块」,其它aar版本号没变就不push~ flutterbuildaar #项目根目录 ROOT_PROJECT_PATH=$(cd"$(dirname"$0")";pwd) #repo目录 REPO_DIR=$ROOT_PROJECT_PATH/build/host/outputs/repo #初始化一个空数组来存储匹配的文件路径 aar_files=() #查找并处理匹配的文件 whileIFS=read-raar_file;do aar_files+=("$aar_file") done(find"$REPO_DIR"-typef-name"*.aar"|grep'release-[0-9.]*.aar$') #Maven仓库信息,可以单独为每个aar文件设置 GROUP_ID="cn.coderpig" ARTIFACT_ID="mylibrary" VERSION="1.0.0" LOCAL_REPO_PATH="$ROOT_PROJECT_PATH/repo" #遍历aar上传到Maven仓库 foraar_filein"${aar_files[@]}";do mvndeploy:deploy-file\ -Dfile="$aar_file"\ -DgroupId="$GROUP_ID"\ -DartifactId="$ARTIFACT_ID"\ -Dversion="$VERSION"\ -Dpackaging=aar\ -Durl=file://$LOCAL_REPO_PATH done 6.2.2. 把多个AAR包打成一个?? 不用搜了,全网都是教你用 kezong/fat-aar-android 来打一个完整的AAR: 作者已弃坑,最新一次commit也是2年前了,相关文章很多,就不赘述了,感兴趣可以参考下这几篇: 《Android 多个aar包合并成一个》《Android多设备多module打包(fat-aar)》《多个AAR打包成一个AAR》7. 小结?? 断断续续写了一周,总算把这篇文章写完了,看完应该会对想搞混编的童鞋有帮助。当然,实际开发中踩的远不止这些??,收集下比较典型的,后续再整理下跟大家分享吧,就酱,感谢??~ 「参考文献」: 《Flutter混编方案在起点客户端的实践之路》《Flutter 多引擎渲染,在稿定 App 的实践》《混合开发打包Android篇》《flutter和原生利用pigeon建立通道》《Flutter混合栈管理方案对比》《Flutter Boost3.0初探》《一款零侵入的高效Flutter混合栈管理方案,你值得拥有!》《Flutter混合栈管理》《{已开源} 阅文 Flutter 混合开发利器 MixStack》点击关注公众号,“技术干货”及时达! 阅读原文

上一篇:2025-03-09_收藏!来自数英编辑部的三八内容清单 下一篇:2025-07-21_【招聘】进来看看:围观、Carnivo、凡人广告

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价