Flutter 实践记录

最近使用Flutter做了一个模块的一个页面,虽然第一期做完后,上级觉得效果不满意,第二期要用原生重做,但还是记录一下。

项目配置

配置资源图片目录

在安卓原生中,添加资源图片,一般会放在drawable-xhdpidrawable-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页面,在一个ActivityFragmentView上,Flutter给我们提供了FlutterActivityFlutterFragmentActivityFlutterFragmentFlutterView,这里只介绍常用的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() {
    }
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容