date:12.29,for flutter >v1.12.x
在Flutter的应用场景中,有时候一个APP只有部分页面是由Flutter实现的,比如:我们常用的闲鱼App,它宝贝详情页面是由Flutter实现的,这种开发模式被称为混合开发。
混合开发的一些其他应用场景:
在原有项目中加入Flutter页面,在Flutter项目中加入原生页面
原生页面中嵌入Flutter模块
Flutter页面中嵌入原生模块
以上这些都属于Flutter混合开发的范畴,那么如何进行Flutter混合开发呢?
在这篇文章中我将向大家介绍Flutter混合开发的流程,需要掌握的技术,以及一些经验技巧,与该文章配套的还有Flutter与Android 混合开发讲解的视频教程。
Flutter混合开发的教程我们分为上下两篇,上篇主要介绍如何在现有的Android应用上进行Flutter混合开发,下篇主要介绍如何在现有的iOS应用上进行Flutter混合开发。
将Flutter集成到现有的Android应用中需要如下几个主要步骤:
提示:为了能够顺利的进行Flutter混合开发,请确认你的项目结构是这样子的:
1 2 3 4 | -?flutter_hybrid ???? -?flutter_module ???? -?FlutterHybridAndroid? ???? -?FlutterHybridiOS |
1. 创建Flutter module
在做混合开发之前我们首先需要创建一个Flutter module。
假如你的Native项目是这样的:xxx/flutter_hybrid/Native项目:
1 2 3 4 5 | $? cd ?xxx /flutter_hybrid/ // 创建支持AndroidX的flutter_module $?flutter?create?--androidx?-t?module?flutter_module? // 创建不支持AndroidX的flutter_module $?flutter?create?-t?module?flutter_module |
注意上面命令中添加了 --androidx参数,该参数的作用是创建一个支持AndroidX的flutter模块
所以说,在创建flutter模块前首先要确定你的Android项目是不是支持Android X,通常是由最新Android Studio创建的Android项目都是默认支持Android X的,所以命令中需要添加--androidx参数
上面代码会切换到你的Android/iOS项目的上一级目录,并创建一个flutter模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //flutter_module/ .android .gitignore .idea .ios .metadata .packages build flutter_module_android.iml flutter_module.iml lib pubspec.lock pubspec.yaml README.md test |
上面是flutter_module中的文件结构,你会发现它里面包含.android与.ios,这两个文件夹是隐藏文件,也是这个flutter_module宿主工程:
-
.android?-?flutter_module的Android宿主工程; -
.ios?-?flutter_module的iOS宿主工程; -
lib?-?flutter_module的Dart部分的代码; -
pubspec.yaml?-?flutter_module的项目依赖配置文件; -
因为宿主工程的存在,我们这个flutter_module在不加额外的配置的情况下是可以独立运行的,通过安装了Flutter与Dart插件的AndroidStudio打开这个flutter_module项目,通过运行按钮是可以直接运行它的。
构建flutter aar(非必须)
如果你需要的话,可以通过如下命令来构建flutter aar:
1 2 | $? cd ?.android/ $?. /gradlew ?flutter:assembleDebug |
这会在.android/Flutter/build/outputs/aar/中生成一个flutter-debug.aar归档文件。
tag: part1 end
为已存在的Android应用添加Flutter module依赖
接下来我们需要配置我们Android项目的Flutter module依赖,打开我们Android项目的settings.gradle添加如下代码:
1 2 3 4 5 6 7 | //?FlutterHybridAndroid/settings.gradle include? ':app' ?????????????????????????????????????//?已存在的内容 setBinding( new ?Binding([gradle:? this ]))????????????????????????????????? //?new evaluate( new ?File(?????????????????????????????????????????????????????? //?new ? settingsDir.parentFile,??????????????????????????????????????????????? //?new ? 'my_flutter/.android/include_flutter.groovy' ??????????????????????????//?new ))?????????????????????????????????????????????????????????????????????? //?new |
setBinding与evaluate允许Flutter模块包括它自己在内的任何Flutter插件,在settings.gradle中以类似:?:flutter、package_info、:video_player的方式存在;
添加:flutter依赖
1 2 3 4 5 6 | //FlutterHybridAndroid/app/build .gradle ... dependencies?{ ? implementation?project( ':flutter' ) ... } |
在Java中调用Flutter module
至此,我们已经为我们的Android项目添加了Flutter所必须的依赖,接下来我们来看如何在Java中调用Flutter模块:
在Java中调用Flutter模块有两种方式:
直接启动一个FlutterActivity的方式
第一步:AndroidManifest.xml中声明FlutterActivity,添加如下代码:
1 2 3 4 5 6 7 | //AndroidManifest.xml <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" ??? /> |
可参考:课程源码
第二步:通过FlutterActivity开启Flutter页面:
1 2 3 4 5 6 7 8 9 10 11 | findViewById(R.id.jump).setOnClickListener( new ?View.OnClickListener()?{ ??? @Override ??? public ?void ?onClick(View?view)?{ ??????? startActivity( ??????????? FlutterActivity ??????????????? .withNewEngine() ??????????????? .initialRoute( "route1" ) ??????????????? .build(MainActivity. this ) ??????? ); ??? } }); |
可参考:课程源码
这种方式虽然简单,但是无法自定义插件,所以无法直接实现Dart和Native之间的通信,如果需要通信的话可以实现下面?介绍的方式。
使用复写FlutterActivity?的方式(推荐)
也可以创建一个Actvity然后继承FlutterActivity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | package ?org.devio.flutter.hybrid; import ?android.content.Context; import ?android.content.Intent; import ?android.os.Bundle; import ?android.util.Log; import ?androidx.annotation.NonNull; import ?io.flutter.embedding.android.FlutterActivity; public ?class ?FlutterAppActivity? extends ?FlutterActivity{ ??? public ?final ?static ?String?INIT_PARAMS?=? "initParams" ; ??? private ?String?initParams; ??? public ?static ?void ?start(Context?context,?String?initParams)?{ ??????? Intent?intent?=? new ?Intent(context,?FlutterAppActivity. class ); ??????? intent.putExtra(INIT_PARAMS,?initParams); ??????? context.startActivity(intent); ??? } ??? @Override ??? protected ?void ?onCreate(Bundle?savedInstanceState)?{ ??????? super .onCreate(savedInstanceState); ??????? initParams?=?getIntent().getStringExtra(INIT_PARAMS); ??? } ??? /** ???? *?重载该方法来传递初始化参数 ???? * ???? *?@return ???? */ ??? @NonNull ??? @Override ??? public ?String?getInitialRoute()?{ ??????? return ?initParams?==? null ??? super .getInitialRoute()?:?initParams; ??? } } |
可参考:课程源码
上面我们使用字符串“initParams”来告诉Dart代码在Flutter视图中显示哪个小部件。 Flutter模块项目的lib/main.dart文件需要通过window.defaultRouteName来获取Native指定要显示的路由名,以确定要创建哪个窗口小部件并传递给runApp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import? 'dart:ui' ; import? 'package:flutter/material.dart' ; void?main()?=>?runApp(_widgetForRoute(window.defaultRouteName)); Widget?_widgetForRoute(String?route)?{ ? switch ?(route)?{ ??? case ?'route1' : ????? return ?SomeWidget(...); ??? case ?'route2' : ????? return ?SomeOtherWidget(...); ??? default : ????? return ?Center( ??????? child:?Text( 'Unknown?route:?$route' ,?textDirection:?TextDirection.ltr), ????? ); ? } } |
调用Flutter module时传递数据
在上文中,我们无论是通过第一种方式还是通过第二种的方式,都允许我们在加载Flutter module时传递一个String类型的initialRoute参数,从参数名字它是用作路由名的,但是既然Flutter给我们开了这个口子,那我们是不是可以搞点事情啊,传递点我们想传的其他参数呢,比如:
1 2 3 4 5 6 | startActivity( ??? FlutterActivity ??????? .withNewEngine() ??????? .initialRoute( "{name:'devio',dataList:['aa','bb',''cc]}" ) ??????? .build(MainActivity. this ) ); |
然后在Flutter module通过如下方式获取:
1 2 3 4 5 | import ?'dart:ui' ; // 要使用window对象必须引入 String?initParams=window.defaultRouteName; // 序列化成Dart?obj?敢你想干的 ... |
通过上述方案的讲解是不是给大家分享了一个新的思路呢。
优化打开Flutter时的启动速度:
新版Flutter支持通过预加载Flutter引擎的方式来提升Flutter模块的打开速度,怎么做呢?
第一步:创建一个Application然后添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public ?class ?MyApplication? extends ?Application?{ ??? public ?static ?final ?String?CACHED_ENGINE_ID?=? "MY_CACHED_ENGINE_ID" ; ??? @Override ??? public ?void ?onCreate()?{ ??????? super .onCreate(); ??????? //在MyApplication中预先初始化Flutter引擎以提升Flutter页面打开速度 ??????? FlutterEngine?flutterEngine?=? new ?FlutterEngine( this ); ??????? //?Start?executing?Dart?code?to?pre-warm?the?FlutterEngine. ??????? flutterEngine.getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()); ??????? //?Cache?the?FlutterEngine?to?be?used?by?FlutterActivity. ??????? FlutterEngineCache.getInstance().put(CACHED_ENGINE_ID,?flutterEngine); ??? } } |
上述代码在MyApplication中初始化了一个id为CACHED_ENGINE_ID?的FlutterEngine,然后我们在需要打开Flutter模块时告诉Flutter我们缓存的id即可:
方式一:
1 2 3 4 5 | startActivity( ? FlutterActivity ??? .withCachedEngine( "my_engine_id" ) ??? .build(currentActivity) ? ); |
方式二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package ?org.devio.flutter.hybrid; import ?android.content.Context; import ?android.content.Intent; import ?android.os.Bundle; import ?androidx.annotation.NonNull; import ?io.flutter.embedding.android.FlutterActivity; import ?static ?org.devio.flutter.hybrid.MyApplication.CACHED_ENGINE_ID; public ?class ?FlutterAppActivity? extends ?FlutterActivity?{ ??? public ?final ?static ?String?INIT_PARAMS?=? "initParams" ; ??? private ?String?initParams; ??? public ?static ?void ?start(Context?context,?String?initParams)?{ ??????? Intent?intent?=? new ?Intent(context,?FlutterAppActivity. class ); ??????? intent.putExtra(INIT_PARAMS,?initParams); ??????? context.startActivity(intent); ??? } ??? @Override ??? protected ?void ?onCreate(Bundle?savedInstanceState)?{ ??????? super .onCreate(savedInstanceState); ??????? initParams?=?getIntent().getStringExtra(INIT_PARAMS); ??? } ??? //使用在MyApplication预先初始化好的Flutter引擎以提升Flutter页面打开速度,注意:在这种模式下回导致getInitialRoute?不被调用所以无法设置初始化参数 ??? @Override ??? public ?String?getCachedEngineId()?{ ??????? return ?CACHED_ENGINE_ID; ??? } ??? /** ???? *?重载该方法来传递初始化参数 ???? *?@return ???? */ ??? @NonNull ??? @Override ??? public ?String?getInitialRoute()?{ ??????? return ?initParams?==? null ??? super .getInitialRoute()?:?initParams; ??? } } |
可参考:课程源码
编写Dart代码
接下来就是在编写Flutter module中的lib下编写Dart带了,快去Enjoy Coding吧!!!
运行项目
接下来,我们就可以运行它了,经过上述步骤,我们就可以以运行普通Android项目的方式来通过AndroidStudio运行一个集成了Flutter的Android项目了。
热重启/重新加载:
大家知道我们在做Flutter开发的时候,它带有热重启/重新加载的功能,但是你可能会发现,混合开发中在Android项目中集成了Flutter项目,Flutter的热重启/重新加载功能好像失效了,那怎么启用混合开发汇总Flutter的热重启/重新加载呢:
1 2 3 | $? cd ?flutter_hybrid /flutter_module $?flutter?attach Waiting? for ?a?connection?from?Flutter?on?Nexus?5X... |
注意如果,你同时有多个模拟器或连接的设备,运行flutter attach会提示你选择一个设备:
1 2 | Android?SDK?built? for ?x86???emulator-5554??????????????????????????android-x86???Android?8.1.0?(API?27)?(emulator) iPhone?X????????????????????3E3FA943-715F-482F-B003-D46F5902C56C???ios???????????iOS?12.1?(simulator) |
接下来我们需要flutter attach -d来指定一个设备:
1 | flutter?attach?-d? 'emulator-5554' |
注意-d后面跟的设备ID。
运行APP,然后你会看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $?flutter?attach More?than?one?device?connected;?please?specify?a?device?with?the? '-d?<deviceId>' ?flag,?or?use? '-d?all' ?to?act?on?all?devices. Android?SDK?built? for ?x86???emulator-5554??????????????????????????android-x86???Android?8.1.0?(API?27)?(emulator) iPhone?X????????????????????3E3FA943-715F-482F-B003-D46F5902C56C???ios???????????iOS?12.1?(simulator) jphdeMacBook-Pro:flutter_module?jph$?flutter?attach?-d? 'emulator-5554' Waiting? for ?a?connection?from?Flutter?on?Android?SDK?built? for ?x86... Done. Syncing?files?to?device?Android?SDK?built? for ?x86...?????????????1,744ms ???To?hot?reload?changes? while ?running,?press? "r" .?To?hot?restart?(and?rebuild?state),?press? "R" . An?Observatory?debugger?and?profiler?on?Android?SDK?built? for ?x86?is?available?at:?http: //127 .0.0.1:60324/ For?a? more ?detailed?help?message,?press? "h" .?To?detach,?press? "d" ;?to?quit,?press? "q" . |
说明连接成功了,接下来就可以通过上面的提示来进行热加载/热重启了,在终端输入:
-
r : 热加载; -
R : 热重启; -
h : 获取帮助; -
d : 断开连接;
调试Dart代码
混合开发的模式下,如何更好更高效的调试我们的代码呢,接下来我就跟大家分享一种混合开发模式下高效调试代码的方式:
接下来就可以像调试普通Flutter项目一样来调试混合开发的模式下的Dart代码了。
除了以上步骤不同之外,接下来的调试和我们之前课程中的Flutter调试技巧都是通用的,需要的同学可以学习下我们前面的课程;
还有一点需要注意:
大家在运行Android工程时一定要在Android模式下的AndroidStudio中运行,因为Flutter模式下的AndroidStudio运行的是Flutter module下的.android中的Android工程。
发布应用
打包
用Flutter开发好APP之后,如何将APP发布以供用户使用呢?一款APP的发布流程无外乎:签名打包—>发布到各store这两大步骤。下面的课程将向大家分享如何签名打包一款Flutter APP。?
众所周知,Android要求所有的APP都需要进行数字签名后,才能够被安装到相应的设备上。签名打包一个Android APP已经是每一位Android开发者的家常便饭了。
那么如何签名打包一款用Flutter开发的APP呢?
如果你已经有签名证书可以绕过此步骤。 签名APK需要一个证书用于为APP签名,生成签名证书可以Android Studio以可视化的方式生成,也可以使用终端采用命令行的方式生成,需要的可以自行Google这里不再敖述。?
将你的签名证书copy到 android/app目录下。
编辑~/.gradle/gradle.properties或../android/gradle.properties(一个是全局gradle.properties,一个是项目中的gradle.properties,大家可以根据需要进行修改) ,加入如下代码:
1 2 3 4 | MYAPP_RELEASE_STORE_FILE=your?keystore?filename?? MYAPP_RELEASE_KEY_ALIAS=your?keystore? alias ?? MYAPP_RELEASE_STORE_PASSWORD=*****???? MYAPP_RELEASE_KEY_PASSWORD=***** |
提示:用正确的证书密码、alias以及key密码替换掉 *****。
编辑 android/app/build.gradle文件添加如下代码:?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | ...?? android?{?? ??? ...?? ??? defaultConfig?{?...?}?? ??? signingConfigs?{?? ??????? release?{?? ??????????? storeFile? file (MYAPP_RELEASE_STORE_FILE)?? ??????????? storePassword?MYAPP_RELEASE_STORE_PASSWORD?? ??????????? keyAlias?MYAPP_RELEASE_KEY_ALIAS?? ??????????? keyPassword?MYAPP_RELEASE_KEY_PASSWORD?? ??????? }?? ??? }?? ??? buildTypes?{?? ??????? release?{?? ??????????? ...?? ??????????? signingConfig?signingConfigs.release?? ??????? }?? ??? }?? }?? ... |
terminal进入项目下的android目录,运行如下代码:
1 | . /gradlew ?assembleRelease |
签名打包成功后你会在 "android/app/build/outputs/apk/"目录下看到签名成功后的app-release.apk文件。 提示:如果你需要对apk进行混淆打包 编辑android/app/build.gradle:?
1 2 3 4 | /**????? *?Run?Proguard?to?shrink?the?Java?bytecode? in ?release?builds.?? */?? def?enableProguardInReleaseBuilds?=? true |
如何在gradle中不使用明文密码?
上文中直接将证书密码以明文的形式写在了gradle.properties文件中,虽然可以将此文件排除在版本控制之外,但也无法保证密码的安全,下面将向大家分享一种方法避免在gradle中直接使用明文密码。?
通过“钥匙串访问(Keychain Access)”工具保护密码安全
下面阐述的方法只在OS X上可行。 我们可以通过将发布证书密码委托在“钥匙串访问(Keychain Access)”工具中,然后通过gradle访问“钥匙串访问”工具来获取证书密码。?
具体步骤:
-
cmd+space打开“钥匙串访问(Keychain Access)”工具。 -
在登录选项中新钥匙串,如图:
提示: 你可以在terminal中运行如下命令检查新建的钥匙串是否成功。
1 | security? find -generic-password?-s?android_keystore?-w |
3.? 在build.gradle中访问你的秘钥串,将下列代码编辑到android/app/build.gradle中:?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | def?getPassword(String?currentUser,?String?keyChain)?{ ?? def?stdout?=?new?ByteArrayOutputStream() ?? def?stderr?=?new?ByteArrayOutputStream() ?? exec ?{ ?????? commandLine? 'security' ,? '-q' ,? 'find-generic-password' ,? '-a' ,?currentUser,? '-s' ,?keyChain,? '-w' ?????? standardOutput?=?stdout ?????? errorOutput?=?stderr ?????? ignoreExitValue? true ?? } ?? //noinspection ?GroovyAssignabilityCheck ????? stdout.toString().trim() } // ?Add?this?line def?pass?=?getPassword( "YOUR_USER_NAME" , "android_keystore" ) ... android?{ ??? ... ??? defaultConfig?{?...?} ??? signingConfigs?{ ??????? release?{ ??????????? storeFile? file (MYAPP_RELEASE_STORE_FILE) ??????????? storePassword?pass? // ?Change?this ??????????? keyAlias?MYAPP_RELEASE_KEY_ALIAS ??????????? keyPassword?pass? // ?Change?this ??????? } ??? } ??? buildTypes?{ ??????? release?{ ??????????? ... ??????????? signingConfig?signingConfigs.release ??????? } ??? } } ... |
注意事项
钥匙串访问(Keychain Access)工具只是帮我们托管了,证书密码,证书明和alias还是需要我们在gradle.properties中设置一下的。
发布应用
通过上述步骤我们完成了将Flutter代码打包,并生成了APK,并放到了assets目录下,接下来我们就可在各大Android市场上传我们的应用了。
|