美文网首页Flutter
Flutter 实践记录

Flutter 实践记录

作者: h2coder | 来源:发表于2022-02-28 18:55 被阅读0次

    最近使用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() {
        }
    });
    

    相关文章

      网友评论

        本文标题:Flutter 实践记录

        本文链接:https://www.haomeiwen.com/subject/byzlrrtx.html