最近使用Flutter做了一个模块的一个页面,虽然第一期做完后,上级觉得效果不满意,第二期要用原生重做,但还是记录一下。
项目配置
配置资源图片目录
在安卓原生中,添加资源图片,一般会放在drawable-xhdpi
、drawable-xxhdpi
目录下,就可以通过R.drawable.xxx来使用。而Flutter则放在根目录的assets
下,并且需要在pubspec.yaml
中配置图片的所在目录。例如我的图片放在assets/images/album/
,需要注意2点
- 文件目录,如果目录为空,不要加,否则编译会报错
- 文件夹的最后必须加 / ,否则报错
flutter:
...
# 配置资源文件目录
assets:
- assets/images/album/
...
- 使用(注意目录和文件名和文件后缀都要写上!!!)
/// 分割线
class PartingLine extends StatelessWidget {
const PartingLine({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
"assets/images/album/parting_line.png"
),
);
}
}
配置IconFont
一些图标,如果是纯色的,并且可以动态设置颜色,一般会使用IconFont,而不是切图,好处是减少体积,并且它本质是一个字体文件,可以动态设置颜色和大小。在Flutter中使用IconFont,也需要在pubspec.yaml
中配置。
如果有多个iconFont,都需要在该文件中配置,并且路径要写全。
flutter:
uses-material-design: true
# 导入自定义的IconFont,路径要写全
fonts:
- family: myIconFont
fonts:
- asset: assets/fonts/my_iconfont.ttf
- 使用
class Loading extends StatelessWidget {
/// 加载中
static const int loading = 0xe635;
@override
Widget build(BuildContext context) {
return Container(
width: 20,
height: 20,
child: Icon(
IconData(loading, fontFamily: "myIconFont"),
color: Colors.white,
size: 15,
),
);
}
}
Flutter Json 生成模型
由于Dart中没有反射,所以不能像Java那样使用反射来解析Json并映射到Bean实体中,Flutter中使用生成代码的方式,生成Json解析代码,但一般我们不会手写,下面给出一个生成的网站json2dart。
- Bean实体生成命令
//只生成一次
flutter pub run build_runner build —delete-conflicting-outputs
//监听保存,持续生成
flutter pub run build_runner watch
-
pubspec.yaml
文件中加入依赖
dependencies:
flutter:
sdk: flutter
# JSON解析
json_annotation: ^4.3.0
json_serializable: ^6.0.0
-
复制从json2dart中生成的dart模型类到项目中,运行上面的生成命令
-
运行后,会在dart模型类的同目录下,生成一个同名的.g.dart文件
-
使用模型类解析Json
import 'dart:convert';
//要解析的Json字符串
var dataJson = '{"userName": "wally"}';
XxxBean.fromJson(
//json转map
json.decode(dataJson)
);
Flutter 打包命令
- 打包aar(与原生混合项目,一般打aar给原生项目使用)
flutter build aar --no-tree-shake-icons
- 打包apk(纯Flutter项目使用)
flutter build apk --no-tree-shake-icons
原生项目接入Flutter模块
原生项目接入Flutter模块有2种方式
- 打包aar包进行依赖,优点:原生项目不需要有Flutter环境即可运行 缺点:不能debug断点调试
- 依赖Flutter模块的module,优点:可断点调试,但需要Flutter环境,并且编译时间加长
综合下来,最好是结合使用,例如项目gradle.properties
有一个布尔值,控制使用aar依赖还是本地依赖,这样开发Flutter模块的人员把开关打开,进行debug调试,而非Flutter模块的开发人员则设置为false,使用远程aar依赖。
aar方式依赖
控制台输入打包aar的命令,开始编译,最后控制台会输出以下信息。
大概意思就是生成的aar到了本地的maven仓库,需要在原生项目中,加入依赖来添加。
- 打包的aar存放目录为:
flutterProject/build/host/outputs/repo
(步骤一、二:在根build.gradle中,allprojects 节点中 添加maven仓库地址)
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 '/Users/charming/Desktop/Work/Android/flutterProject/build/host/outputs/repo'
}
maven {
url "$storageUrl/download.flutter.io"
}
}
(步骤三:在需要使用Flutter模块的module中,添加依赖)
3. Make the host app depend on the Flutter module:
dependencies {
debugImplementation 'com.flutter.flutterProject:flutter_debug:1.0'
profileImplementation 'com.flutter.flutterProject:flutter_profile:1.0'
releaseImplementation 'com.flutter.flutterProject:flutter_release:1.0'
}
(步骤四:在可运行模块,例如app下,添加一个名叫profile的类型)
4. Add the `profile` build type:
android {
buildTypes {
profile {
initWith debug
}
}
}
- 缺点
因为上面指定的maven地址是绝对路径,所以另外一个同事的电脑上,没有这个flutter项目的话,aar就不存在,就会找不到依赖,解决方案有2种:
- 如果有私有maven服务器的话,上传这个aar来远程依赖即可
- 如果没有私有maven服务器,则把aar拷贝到原生项目中,通过相对路径来引用
改良后的aar依赖方式
由于我的公司没有私有maven服务器,所以我把打包的aar手动拷贝到原生项目中的flutter文件夹,然后指定这个目录来依赖,通过Jenkins
打包的时候,就不需要配置flutter环境和flutter项目
- 在原生项目的根目录下,新建一个flutter文件夹
- 拷贝
flutterProject/build/host/outputs/repo
目录,到原生项目的根目录下的flutter文件夹下 - 在原生项目的
根build.gradle
下,添加maven地址
allprojects {
//Flutter依赖
String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
repositories {
//Flutter打包的aar路径,根目录的flutter/repo
//-------- 重点:每次flutter打包为aar后,会生成本地maven仓库文件夹,要手动把repo拷贝进原生工程目录 --------
maven {
url "file://${getRootDir()}/flutter/repo"
}
//-------- 重点:每次flutter打包为aar后,会生成本地maven仓库文件夹,要手动把repo拷贝进原生工程目录 --------
maven {
url "$storageUrl/download.flutter.io"
}
}
}
本地依赖Flutter模块
上面的aar依赖方式,不需要Flutter环境和Flutter项目,好处是提高编译速度,但不可断点debug,开发中及其不便利,所以配置一个开关,切换本地依赖和aar依赖
-
Flutter工程,需要和原生工程同一级目录!
-
gradle.properties
- 添加
isLocalFlutterModule
开关变量 - 由于Flutter模块接入到原生工程,需要指定原生工程的可运行模块的名称,默认是app,则不需要配置,但如果不是,则需要指定
flutter.hostAppProjectName
为你的值
- 添加
#是否本地依赖Flutter模块(需要把Flutter工程,放在和原生工程平行的目录下)
isLocalFlutterModule=false
#指定Flutter宿主模块的名称,默认为app,因为我的原生工程的可运行模块并不是app,所以需要在这里指定,不指定会报错!
flutter.hostAppProjectName=flutterProject
- settings.gradle
- 如果
isLocalFlutterModule
为true,则添加本地Flutter模块的信息
- 如果
//本地Flutter模块依赖配置
if (isLocalFlutterModule.toBoolean()) {
setBinding(new Binding([gradle: this]))
//相对路径依赖,指定Flutter工程
evaluate(new File(
settingsDir.parentFile,
'/flutterProject/.android/include_flutter.groovy'
))
include ': flutterProject'
project(': flutterProject').projectDir = new File('../flutterProject')
}
- 运行模块中,添加flutter依赖
dependencies {
if (isLocalFlutterModule.toBoolean()) {
//直接依赖,Flutter模块工程的源码模块,开发、联调时使用
implementation project(':flutter')
} else {
//aar方式依赖
debugApi 'com.flutter.flutterProject:flutter_debug:1.0'
releaseApi 'com.flutter.flutterProject:flutter_release:1.0'
}
}
Flutter与原生之间交互
Flutter和原生之间,不可避免的就是交互和通信。方法的参数是一个Object,一般我们定义Json字符串来通信,调用或被调用时用Json解析参数即可,方法的返回值也是一个Json字符串。
方法要注册给Flutter引擎,而一般Flutter引擎有2种创建方式
- 手动创建,一般一个App只有一个Flutter引擎实例,多个Flutter页面都在一个引擎上
- 一个Activity对应一个Flutter引擎
新建Flutter页面
一般多个Flutter页面,在一个Activity
或Fragment
或View
上,Flutter给我们提供了FlutterActivity
、FlutterFragmentActivity
、FlutterFragment
、FlutterView
,这里只介绍常用的FlutterFragmentActivity
新建一个MyFlutterActivity
,继承于FlutterFragmentActivity
,复写configureFlutterEngine()
方法,该方法在Flutter引擎创建完毕后回调,所以在这个方法中可以注册一些原生方法给Flutter端调用
public class MyFlutterActivity extends FlutterFragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
}
}
创建Flutter引擎
手动创建的方式,可在Application的onCreate()创建
- 创建引擎
/**
* Flutter引擎Id
*/
public static final String APP_FLUTTER_ENGINE_ID = "app_flutter";
//创建一个引擎实例
FlutterEngine flutterEngine = new FlutterEngine(context.getApplicationContext());
//配置初始路由
flutterEngine.getNavigationChannel().setInitialRoute("/");
//指定Dart代码,预热引擎
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
//缓存引擎,提供给FlutterActivity或FlutterFragment调用
FlutterEngineCache.getInstance().put(APP_FLUTTER_ENGINE_ID, flutterEngine);
- 使用引擎
FlutterFragmentActivity.CachedEngineIntentBuilder builder = new FlutterFragmentActivity
//通过唯一ID,指定上面创建的Flutter引擎,并指定我们自己定义的FlutterFragmentActivity
.CachedEngineIntentBuilder(MyFlutterActivity.class, FlutterConfig.APP_FLUTTER_ENGINE_ID);
Intent intent = builder.build(context);
startActivity(intent);
Flutter调原生方法
- 注册原生方法
拿到Flutter引擎实例,就可以注册原生方法了,例如手动创建FlutterEngine实例的时候就已经有实例可用了。而通过FlutterFragmentActivity的方式的话,则复写configureFlutterEngine
方法来获取。
方法调用,通过MethodChannel
,并且需要指定一个名称,这个名称需要原生端和Flutter对应,否则调不通。
给MethodChannel设置一个MethodCallHandler
回调对象,回调时传入的MethodCall
可以拿到被调用的方法名以及方法参数,MethodChannel.Result
为原生方法调用后的结果。
/**
* 全局唯一的通道名称
*/
public static final String CHANNEL_NAME = "com.flutter.flutterProject";
MethodChannel methodChannel = new MethodChannel(engine.getDartExecutor().getBinaryMessenger(), FlutterConfig.CHANNEL_NAME);
methodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
//方法名
String methodName = call.method;
//方法参数
Object arguments = call.arguments();
//执行对应的方法
if(methodName.equals("showToast")) {
Toast.makeText(getApplicationContext(), arguments.toString(), Toast.LENGTH_SHORT).show();
//根据业务,回调Flutter端告知调用结果
if (isSuccess) {
result.success("success");
} else {
result.error("-1", "调用失败", "");
}
}
}
});
- Flutter调用原生
/// 给native发消息,此处应和客户端名称保持一致
static const String _NATIVE_CHANNEL_NAME = "com.flutter.flutterProject";
//创建MethodChannel
var MethodChannel methodChannel = const MethodChannel(_NATIVE_CHANNEL_NAME);
//调用原生方法,并获取返回结果
T resultValue = await methodChannel.invokeMethod("toast", "我是Toast的内容");
原生调Flutter方法
- 注册Flutter方法
/// 给native发消息,此处应和客户端名称保持一致
static const String _NATIVE_CHANNEL_NAME = "com.flutter.flutterProject";
//创建MethodChannel
var MethodChannel methodChannel = const MethodChannel(_NATIVE_CHANNEL_NAME);
methodChannel.setMethodCallHandler((MethodCall handler) {
return Future(() {
//被调用的Flutter方法名
var methodName = handler.method;
//传递的参数
var arguments = handler.arguments
if("flutterMethodName" == methodName) {
//...处理
}
});
});
- 原生调用Flutter方法
/**
* 全局唯一的通道名称
*/
public static final String CHANNEL_NAME = "com.flutter.flutterProject";
DartExecutor dartExecutor = flutterEngine.getDartExecutor();
BinaryMessenger messenger = dartExecutor.getBinaryMessenger();
//创建MethodChannel
MethodChannel methodChannel = new MethodChannel(messenger, CHANNEL_NAME);
//调用Flutter方法
String methodName = "flutterMethodName";
String parmas = "我是参数";
methodChannel.invokeMethod(methodName, parmas, new MethodChannel.Result() {
@Override
public void success(@Nullable Object result) {
}
@Override
public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
}
@Override
public void notImplemented() {
}
});
网友评论