赞
踩
flutter有四种工程模式:application、plugin、package、module
flutter create xxapp
│ pubspec.lock
│ pubspec.yaml
│ README.md
├─android
│
├─ios
│
├─lib
│ main.dart
│
├─test
│ widget_test.dart
│
└─web
Flutter Application是一个标准的Flutter App项目工程,包含标准的Dart层与Native平台层代码,项目主体是Flutter, 其内部包含 Android 和 iOS、 Web 项目。
flutter create --template=plugin xxapp_plugin
│ pubspec.lock │ pubspec.yaml │ README.md │ ├─example │ │ │ ├─lib │ │ main.dart │ │ │ └─test │ widget_test.dart │ ├─lib │ flutter_application_1.dart │ └─test flutter_application_1_test.dart
FlutterPlugin是Flutter 插件,包含Dart层与Native平台层的实现。Flutter Plugin提供Android或者iOS的底层封装,在Flutter层提供组件功能, 使Flutter可以方便的调取Native的模块。如果要开发一个 Plugin 且此 Plugin 涉及到原生支持,比如蓝牙功能、网络功能等,这些功能纯 Flutter 是无法实现得。 很多平台相关性或者对于Flutter实现起来比较复杂的部分,也都可以封装成Plugin。
flutter create --template=package xxapp_package
│ pubspec.lock
│ pubspec.yaml
│ README.md
├─lib
│ flutter_application_1.dart
│
└─test
flutter_application_1_test.dart
Flutter Package是纯 Flutter 模块,不需要原生开发,没有Android 和 iOS 项目,仅包含Dart层的实现,往往定义一些公共Widget。
flutter create --template module my_flutter
│ pubspec.lock
│ pubspec.yaml
│ README.md
│
├─.android
│
├─.ios
│
├─lib
│ main.dart
│
└─test
widget_test.dart
.android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。
通过查看app的build.gradle文件可以知道是将Flutter变成moudle的形式依赖到Android项目中的:
...
buildDir = new File(rootProject.projectDir, "../build/host")
dependencies {
implementation project(':flutter')
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
}
这种集成方式虽然比较方便, 但是在多人协作的app中, 尤其是对非flutter开发的同学来说, 是强依赖本地安装flutter环境的, 所以也是有缺点的。
一般来说混编方案有以下两种:
也就是Flutter Application创建工程的方案,目的是将iOS工程和Android工程作为Flutter工程的子工程,由Flutter统一管理。
优点:
缺点:
各端分离方案是iOS、Android和Flutter分别作为三个独立项目存在,将Flutter工程的编译产物作为iOS工程和Android工程的依赖模块,原有工程的管理模式不变,对原生工程没有侵入性,无需额外配置工作。 这种方案需要单独创建Flutter项目,然后通过iOS(CocoaPods)和安卓的依赖管理工具将Flutter项目build出来的framework、资源包等放入Native工程以供使用。 这种方式可以将iOS、Android和Flutter项目放在一个目录下面作为一个项目来管理,也可以不在同一目录下,关键是设置Flutter模块依赖时相对路径一定要设置正确。
通过Flutter Module创建工程的方案就可以完成这种混编。
官方也比较推荐各端分离方案这种混合开发解决方案,并且推出了Flutter Module工程方便,可将flutter作为module让原生native工程直接依赖。
在上文中得出Flutter和Native工程混编采用各端分离方案比较合理,下面来研究一下如果将Flutter作用一个单独的模块如何引入Android项目工程。
Flutter 可以作为 Gradle 子项目源码或者 AAR 嵌入到现有的 Android 应用程序中。因此Android 原生项目中接入Flutter项目一般有两种方式:
目前现有的 Android 项目可能支持 mips 或 x86 之类的架构,然而,Flutter 当前仅支持 为 x86_64,armeabi-v7a 和 arm64-v8a 构建预编(AOT)的库。 可以考虑使用 abiFilters 这个 Android Gradle 插件 API 来指定 APK 中支持的架构,从而避免 libflutter.so 无法生成而导致应用运行时崩溃
这种方式需要配置好Flutter的开发环境. 有两种方式:
在Android工程中集成flutter,要保证 将要使用的Flutter Module工程与原生Android工程 在同一根目录下 , Terminal进入到项目根目录, 执行以下命令:
flutter create -t module --org {package_name} {module_name}
// 此处 module_name 的命令遵循 Android 子 module 的命名即可。不能有中划线。比如,:
flutter create -t module --org com.emple.demo.flutter flutter_module
执行完成后会在Android的同一级目录下生成一个新的 flutter_module文件夹。这里在项目根目录创建子 module 只是为了把代码放在一个仓库,方便维护,理论上可以放在硬盘的任何位置。
Flutter module模块项目,其中包含一些 Dart 代码来帮助你入门以及一个隐藏的子文件夹 .android/。 .android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。 注意:
可以将Flutter模块打成Android AAR 作为依赖媒介引入到Android应用程序中,这样Android应用开发不用安装Flutter SDK也可以.缺点是如果Flutter需要经常修改就需要每次都需要重新编译一次打包AAR进行引入。这种方式会将 Flutter 库打包成由 AAR 和 POM artifacts 组成的本地 Maven 存储库。这种方案可以使你的团队不需要安装 Flutter SDK 即可编译宿主应用。之后,也可以从本地或远程存储库中分发更新AAR。
假设你在 some/path/my_flutter 下构建 Flutter 模块,执行如下命令:
content_copy
cd some/path/my_flutter
flutter build aar
执行打包命令flutter build aar
,这个打包AAR方式会包含多种abi,如果想要指定abi可以使用 flutter build aar --target-platform xxx
平台。
执行成功以后会输出以下信息。也就是说build以后会生成本地的maven仓库,仓库位于build->host->outputs->repo内,将这个仓库配置到需要混编的Android项目的gradle就可以完成混编。
1. Open <host>\app\build.gradle 2. Ensure you have the repositories configured, otherwise add them: String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com" repositories { maven { url 'C:\Users\WIN10\Desktop\fluttermodel\flutter_module2\build\host\outputs\repo' } maven { url "$storageUrl/download.flutter.io" } } 3. Make the host app depend on the Flutter module: dependencies { debugImplementation 'cn.tqxd.flutter_module2:flutter_debug:1.0' profileImplementation 'cn.tqxd.flutter_module2:flutter_profile:1.0' releaseImplementation 'cn.tqxd.flutter_module2:flutter_release:1.0' } 4. Add the `profile` build type: android { buildTypes { profile { initWith debug } } }
flutter build aar
该命令主要用于创建(默认情况下创建 debug/profile/release 所有模式)本地存储库,主要包含以下文件:
build/host/outputs/repo
└── com
└── example
└── my_flutter
├── flutter_release
│ ├── 1.0
│ │ ├── flutter_release-1.0.aar
│ │ ├── flutter_release-1.0.aar.md5
│ │ ├── flutter_release-1.0.aar.sha1
│ │ ├── flutter_release-1.0.pom
│ │ ├── flutter_release-1.0.pom.md5
│ │ └── flutter_release-1.0.pom.sha1
│ ├── maven-metadata.xml
│ ├── maven-metadata.xml.md5
│ └── maven-metadata.xml.sha1
├── flutter_profile
│ ├── …
└── flutter_debug
└── …
可以发现,使用上面的命令编译的AAR包主要分为debug、profile和release三个版本,使用哪个版本的AAR需要根据原生的环境进行选择。找到AAR包,然后再Android宿主应用程序中修改 app/build.gradle 文件, 使其包含本地存储库和上述依赖项。
android { // ... } repositories { maven { url 'some/path/my_flutter/build/host/outputs/repo' // This is relative to the location of the build.gradle file // if using a relative path. } maven { url 'https://storage.googleapis.com/download.flutter.io' } } 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' }
注意,不要将编译好的aar直接copy到项目中去,必须要通过maven坐标依赖才能将完整的Flutter模块引入。
如果需要打不同版本的AAR在构建命令后面加上--build-number [版本号]
;
例如:
flutter build aar --build-number 1.1
如果团队开发时,这种本地Maven仓库引用aar的方式就不方便了,毕竟本地仓库路径都不一样。可以先分析通过flutter build aar是如何生成本地的maven仓库的:
//执行-v命令,查看命令输出信息
flutter build aar -v
可以发现,整个flutter build aar命令中,其实就执行了这三个Gradle任务assembleAarDebug、assembleAarProfile、assembleAarRelease
而这三个任务都执行了flutter\packages\flutter_tools\gradle\aar_init_script.gradle
这个gradle脚本。
查看aar_init_script.gradle文件:
// This script is used to initialize the build in a module or plugin project. // During this phase, the script applies the Maven plugin and configures the // destination of the local repository. // The local repository will contain the AAR and POM files. // 此脚本用于在模块或插件项目中初始化构建。 // 在此阶段,脚本应用 Maven 插件并配置 // 本地存储库的目的地。 // 本地存储库将包含 AAR 和 POM 文件。 //配置了本地maven仓库的地址 project.uploadArchives { repositories { mavenDeployer { repository(url:"file://${outputDir}/outputs/repo") } } }
它在指定的目录下,生成了一个maven本地仓库,并将aar上传到了这里
。 因此如果需要将aar上传到自己的私有maven仓库中,只需要把repository中的地址改成自己的maven私服地址即可。
例如
...
repository(url: "http://172.17.0.172:8081/repository/maven-releases/") {
authentication(userName: "xxx", password: "xxx")
}
也可以通过shell脚本来实现动态的修改:
优点:
缺点:
和上文中一致,参考上文。
在settings.gradle里面配置如下代码:
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
rootProject.name + '/flutter_module/.android/include_flutter.groovy'
))
include ':flutter_module'
如果主module名称不是app,而是改成了其他的名称:,则还需要在project级别的gradle.properties中添加如下代码:
flutter.hostAppProjectName = app_module_name
在app主模块的build.gradle的dependencies中加入依赖库
implementation project(':flutter')
现在的原生项目就包含 Flutter SDK 的所有依赖了。 Flutter UI 相关的内容,还是用 dart 在 main.dart 中写, 然后可以把这个 dart 渲染出来的内容按照 Activity 、Fragment 或 View 的形式添加到已有的项目中了。
优点
方便快捷,所见即所得,不用频繁发布。
缺点
有两种方式:
//在清单配置文件中注册activity
<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"
/>
//跳转到flutter页面
startActivity(
FlutterActivity
.withNewEngine()
//.initialRoute("/my_route")
.build(MainActivity.this)
);
自己创建一个FlutterAppActivity继承自FlutterActivity.
public class MainActivity extends FlutterActivity {
}
如果单纯只是想打开一个Flutter页面,两种方式实际上基本没有太大区别,第一种方式也许还会更简单一点。 但是,在Flutter开发中,往往还需要开发一些Native插件供Flutter调用,如果使用复写FlutterActivity的方式更有利于在FlutterActivity中注册的Native插件, 所以实际开发中一般推荐使用第二种方式.
上文中的跳转是假定 Dart 代码入口是调用 main(),并且Flutter 初始路由是 ‘/’。 Dart 代码入口不能通过 Intent 改变,但是初始路由可以通过 Intent 来修改。Android跳转到Flutter指定页面是通过路由来跳转的,需要先声明路由;
在Flutter项目工程的MeterialApp下声明路由;并创建ImagePage()和TextPage()页面:
MaterialApp(
theme: ThemeData(
),
home: MyHomePage(),
routes: <String,WidgetBuilder>{
"one_page" : (context) => OnePage(),
"two_page" : (context) => TwoPage(),
},
);
Android部分通过指定路由跳转:
/**
* 进入Flutter页面 通过路由来指定页面
*/
startActivity(FlutterActivity.
withNewEngine().
initialRoute("two_page").//跳转到two_page页面
build(MainActivity.this));
默认情况下,每一个FlutterActivity都会创建自己的FlutterEngine对象,每一个FlutterEngine都需要加载耗时,也就是说打开每一个标准的FlutterActivity时都会有一个延迟。 为了减少这个延迟,可以在打开FlutterActivity前预加载FlutterEngine,需要使用的时候直接使用预加载好的FlutterEngine。
需要找一个合适的地方来预加载FlutterEngine,下面的示例在Application代码中进行预加载:
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Instantiate a FlutterEngine. flutterEngine = new FlutterEngine(this); // Start executing Dart code to pre-warm the FlutterEngine. flutterEngine.getDartExecutor().executeDartEntrypoint( DartEntrypoint.createDefault() ); // Cache the FlutterEngine to be used by FlutterActivity. FlutterEngineCache .getInstance() .put("my_engine_id", flutterEngine); } }
传递给FlutterEngineCache的id可以是任意值,只要确保和在FlutterActivity和FlutterFragment中取出FlutterEngine时使用的一致即可。要使用预热且缓存的 FlutterEngine 时,让你的 FlutterActivity 从缓存中获取 FlutterEngine,而不是创建一个新的。可以使用 FlutterActivity 的 withCachedEngine() 方法来实现:
myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(currentActivity)
);
}
});
当使用withCachedEngine方法时,注意id要和预加载FlutterEngine时保持一致。现在再打开FlutterActivity时,延迟问题就好很多了。
当使用一个缓存的 FlutterEngine 时, FlutterEngine 会比展示它的 FlutterActivity 或 FlutterFragment 存活得更久。切记,Dart 代码会在你预热 FlutterEngine 时就开始执行,并且在你的 FlutterActivity 或 FlutterFragment 销毁后继续运行。要停止代码运行和清理相关资源,可以从 FlutterEngineCache 中获取你的 FlutterEngine,然后使用 FlutterEngine.destroy() 来销毁 FlutterEngine。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//val flutterFragment =fragmentManager.findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment;
val flutterFragment = FlutterFragment.createDefault()
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_container, flutterFragment)
.commit()
}
}
FlutterFragment它不支持生命周期管理,需要借助Activity:
public class MyActivity extends FragmentActivity { @Override public void onPostResume() { super.onPostResume(); flutterFragment.onPostResume(); } @Override protected void onNewIntent(@NonNull Intent intent) { flutterFragment.onNewIntent(intent); } @Override public void onBackPressed() { flutterFragment.onBackPressed(); } @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults ) { flutterFragment.onRequestPermissionsResult( requestCode, permissions, grantResults ); } @Override public void onUserLeaveHint() { flutterFragment.onUserLeaveHint(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); flutterFragment.onTrimMemory(level); } }
默认情况下,FlutterFragment 会创建它自己的 FlutterEngine 实例,同时也需要不少的启动时间。这就意味着您的用户会看到短暂的白屏。通过使用已存在的、预热的 FlutterEngine 就可以大幅度减少启动的耗时。 要在 FlutterFragment 中使用预热 FlutterEngine,可以使用工厂方法 withCachedEngine() 实例化 FlutterFragment。
// Somewhere in your app, before your FlutterFragment is needed,
// like in the Application class ...
// Instantiate a FlutterEngine.
FlutterEngine flutterEngine = new FlutterEngine(context);
// Start executing Dart code in the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);
// Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
FlutterFragment.withCachedEngine("my_engine_id").build();
FlutterFragment 内部可访问 FlutterEngineCache,并且可以根据传递给 withCachedEngine() 的 ID 获取预热的 FlutterEngine。 如上所示,通过提供预热的 FlutterEngine,您的应用将以最快速度渲染出第一帧。
当配置一个使用新 FlutterEngine 的 FlutterActivity 或者 FlutterFragment 时,会使用到初始路由的概念。但是,使用缓存中的 Flutter 引擎时, FlutterActivity 或者 FlutterFragment 则没有涉及初始路由的概念。这是因为被缓存的引擎理论上已经执行了 Dart 代码,在这时配置初始路由已经太迟了。
开发者如果想要让缓存中的引擎从自定义的初始路由开始运行,那么可以执行 Dart 入口前,为缓存的 FlutterEngine 配置自定义的初始路由。如下面这个例子:
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Instantiate a FlutterEngine. flutterEngine = new FlutterEngine(this); // Configure an initial route. flutterEngine.getNavigationChannel().setInitialRoute("your/route/here"); // Start executing Dart code to pre-warm the FlutterEngine. flutterEngine.getDartExecutor().executeDartEntrypoint( DartEntrypoint.createDefault() ); // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment. FlutterEngineCache .getInstance() .put("my_engine_id", flutterEngine); } }
通过设置导航通道中的初始路由,会让关联的 FlutterEngine 在 runApp() 方法首次执行后,展示已配置的路由页面。在 runApp() 的首次执行之后,修改导航通道中的初始路由属性是不会生效的。想要在不同的 Activity 和 Fragment 之间使用同一个 FlutterEngine,并且在其展示时切换不同的路由,开发者需要设置一个方法通道,来显式地通知他们的 Dart 代码切换 Navigator 路由。
首先创建一个MethodChannel对象,它接受一个String作为参数,String可以为任意字符串,这个String的作用是用来和原生中注册的MethodChannel进行匹配,只有双方的值一致时,这个通道才起作用。
static const platform = const MethodChannel('samples.flutter.dev/battery');
然后通过MethodChannel的invokeMethod()方法调用原生,这个方法接受两个参数,第一个参数为标识,也就是原生内边的方法名字,第二个参数就是需要向原生传递的参数,可以是一个map或者jsonString,我这没有需要向原生传递的参数,所以就没填。
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = '您的手机电量还有 $result % .';
} on PlatformException catch (e) {
batteryLevel = "手机电量获取失败: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
Fluttrt的代码写完后,接着实现原生端的代码,首先需要在原生注册MethodChannel,如果你用的Activity,那么就继承FlutterActivity,如果用的Fragment,那么就继承FlutterFragment, 然后需要在Activity或者Fragment中实例化一下这个通道。常用的做法是在 configureFlutterEngine 这个方法中实例化通道就行了,有多少个通道,就在这里实例化多少个通道。
值得注意:不管执行结果如何都要执行result.success或者result.error,否则Flutter端可能会一直等待响应
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "samples.flutter.dev/battery") .setMethodCallHandler { call, result -> when (call.method) { "getBatteryLevel" -> { val batteryLevel = getBatteryLevel() if (batteryLevel != -1) { result.success(batteryLevel) } else { result.error("UNAVAILABLE", "Battery level not available.", null) } } } } }
注意,第二个参数的值要和Flutter中MethodChannel的值一致。
在Android实例化了一个MethodChannel对象,并调用了setMethodCallHandler方法进行注册。这个setMethodCallHandler方法需要实现一个接口参数,这个接口为MethodChannel.MethodCallHandler,看一下这个接口。
public interface MethodCallHandler {
@UiThread
void onMethodCall(@NonNull MethodCall call, @NonNull Result result);
}
这个接口只有一个onMethodCall方法,并且有两个参数,MethodCall和Result,其实最终就是通过这两个参数来与Flutter进行通信。Result,为一个接口,当Android处理完Flutter调用的方法逻辑后需要向Flutter回传一些数据时,就通过Result来实现。
Android调用Flutter,更简单,只是需要注意线程问题,UI相关就放主线程执行,耗时操作放子线程然后event更新即可.
例如
例如用的StatefulWidget,在initState方法中给它加个接收消息
@override void initState() { _initChannel(); } void _initChannel() { var channel = MethodChannel("your_channel_name"); channel.setMethodCallHandler((call) { // 同样也是根据方法名分发不同的函数 switch(call.method) { case "your_method_name": { String msg = call.arguments["msg"]; print("Native 调用 Flutter 成功,参数是:$msg"); return "成功"; } } return null; }); }
class MainActivity: FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val messenger = flutterEngine.dartExecutor.binaryMessenger // 新建一个 Channel 对象 val channel = MethodChannel(messenger, "your_channel_name") // 调用 Flutter 中的方法 val params = mapOf( "msg" to "这是来自 Android 端的参数" ) channel.invokeMethod("your_method_name", params) } }
Flutter 整个项目都是绘制在一个 Surface 画布上,而fluttet_boost 将栈统一到了原生层,通过一个单例的 flutter engine 进行绘制。每个 FlutterFragment 和 FlutterActivity 都是一个 Surface承载容器, 切换页面时就是切换 Surface 渲染显示,而对于不渲染的页面通过 Surface 截图缓存画面显示。 这样整个 Flutter 的路由就被映射到原生栈中,统一由原生页面栈管理,Flutter 内每 push 一个页面就是打开一个 Activity 。 一般情况下都会避免混合栈的相互调用 ,但是面对不得不如此为之的情况下,还是会出现问题。
即每次使用一个新的FlutterEngine来渲染Widget树。虽然Flutter 2.0之后的创建FlutterEngine的开销大大降低,但是依然没有解决每个FlutterEngine是一个单独isolate。
flutter_boost阿里巴巴闲鱼团队出品的一款解决Flutter栈管理的框架。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。