• 原生工程接入Flutter实现混编


    前言

    上半年我定的OKR目标是帮助团队将App切入Flutter,实现统一技术栈,变革成多端融合开发模式。Flutter目前是跨平台方案中最有潜力实现我们这个目标的,不管是Hybird还是React Native,我们的项目都有落地应用,跨平台一直是终端团队所追求的技术,能够快速研发和部署也是我们不断给自己提出的挑战。Flutter是什么我在这里就不多说了,很多文章都有介绍,本篇文章想分享的是如何在原生工程中嵌入Flutter来实现混编,帮助团队快速落地Flutter迁移,这个对小团队来说应该会有一定借鉴意义。

    前置动作

    在接入Flutter之前需要具备以下前置条件:

    1. 易于开发的操作系统(首推macOS)
    2. 配置Flutter开发环境(参考:https://flutter.dev/docs/get-started/install/macos )
    3. Android和iOS开发环境(自行搜索解决)

    接入方案

    业内绝大部分的App都不可能推倒重来,所以混合工程的方式接入Flutter是目前主流开发模式,下面我简单说说业界两种工程管理模式:

    在这里插入图片描述
    统一管理模式(不推荐)

    • 优点
      • 适合全新使用Flutter开发的项目
    • 缺点
      • 后期代码耦合严重,相关工具链耗时大幅增长,导致开发效率低

    三端分离模式(推荐)

    咸鱼方案:https://mp.weixin.qq.com/s/Q1z6Mal2pZbequxk5I5UYA?​
    官方方案:https://flutter.dev/docs/development/add-to-app​

    • 优点
      • 快速实现Flutter功能“热插拔”,降低原生工程的改造成本
      • 可以直接进行Dart代码和原生代码开发调试

    目前我们采用的是以module的形式接入,因为我们团队人员少,沟通协作起来成本不大,初期直接源码接入也方便我们快速接入开发和调试。

    踩坑实践

    flutter doctor
    flutter doctor
    如果想确认你当前的环境是否ok,执行下flutter doctor命令,基本能解决大部分问题。如果遇到一直卡住,说明你当前环境是不通的,检查下代理是否配置正确。

    创建Flutter module工程
    创建Flutter module工程
    如果点击Finish创建module一直卡死,说明还是网络问题,命令行输入vi ~/.bash_profile检查下代理。如果实在不行,则通过命令行创建module:

     flutter create -t module --org com.example my_flutter
    

    Android原生工程集成Flutter

    一期我们先接入Android工程,所以接下来主要以Android为主,后续如果有iOS相关的实践会补充到这里。

    先看下我们的module工程:
    在这里插入图片描述
    目录结构:
    在这里插入图片描述

    • .android(隐藏目录,自动生成的Android工程)
    • .ios(隐藏目录,自动生成的iOS工程)
    • build(Android和iOS的构建产物)
    • lib(Flutter应用源文件)
    • test(测试文件)
    • *.iml(工程配置文件)
    • pubspec.lock(记录当前项目实际依赖信息的文件)
    • pubspec.yaml(管理第三方库资源信息的配置文件)

    除了工程配置文件和自动生成的工程目录之外,其他文件都需要进行托管。

    了解完工程目录之后,我们开始集成:

    1. 打开原生工程setting.gradle,加入以下配置
    // 引入flutter module
    setBinding(new Binding([gradle: this]))                                 // new
    evaluate(new File(                                                      // new
            settingsDir.parentFile,                                               // new
            'edu_flutter_module/.android/include_flutter.groovy'                          // new
    ))
    
    
    
    include ':edu_flutter_module'
    project(':edu_flutter_module').projectDir = new File('../edu_flutter_module')
    

    可以看到目前我们依赖的flutter module,是在原生工程目录同级的。

    1. 打开主工程的build.gradle文件,在dependencies下加入以下配置:
        implementation project(":flutter")
    

    ok,这两步是官方的指引,配置完之后就完事了? 太天真了,还需要有一些额外的调整。构建一下就知道了:

    异常1:Gradle DSL method not found: 'google()'

    在这里插入图片描述
    项目中用的gradle版本还是比较旧的,需要升级一下:
    在这里插入图片描述

    异常2:AAPT error:resource android:attr/fontVariationSettings not found
    在这里插入图片描述
    这个异常需要将compileSdkVersion升级到28,之前是26。

    异常3:assert appProject !=null
    在这里插入图片描述
    这个问题巨坑,我们的主工程名是course,但flutter的构建脚本是硬编码为app,有两种解决办法:

    1. 重命名module名字,命名为app
    2. 修改flutter脚本(我选的是这种)

    在这里插入图片描述
    这样,flutter脚本就能找到我们的工程,编译也ok了。

    但其实还有问题,因为目前我们还未升级support包到AndroidX版本,而创建出来的module工程默认是支持AndroidX的,所以我们需要进行降级,等后续升级工程之后再处理。

    修改edu_flutter_module/pubspec.yaml,将androidX改为false:

      module:
        androidX: false
        androidPackage: com.tencent.edu
        iosBundleIdentifier: com.tencent.edu
    

    改完这个之后,终于工程编译通过了,但这就结束了吗,还有坑等着你呢。

    原生页面引入Flutter页面

    上一个主题我们解决掉一些坑之后终于把flutter作为一个module集成到我们的工程中,接下来我们尝试写个页面嵌入到我们页面。

    目前课堂用的flutter版本是:v1.12.13+hotfix.5,这个版本的使用跟之前的版本会有些差异,可以参考官方的wiki:

    https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects

    这里我尝试把课堂的首页替换成Flutter页面,做了以下调整:

    
     @Override
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            // TODO: 2020-04-01 增加flutter视图
            View view = inflater.inflate(R.layout.fragment_index, container, false);
    
            FlutterEngine flutterEngine = new FlutterEngine(getActivity());
            flutterEngine.getDartExecutor().executeDartEntrypoint(
                    DartExecutor.DartEntrypoint.createDefault()
            );
            flutterEngine.getNavigationChannel().setInitialRoute("route1");
    
    
            FlutterView flutterView = new FlutterView(getActivity());
            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            FrameLayout flContainer = view.findViewById(R.id.fl_content);
            // 关键代码,将Flutter页面显示到FlutterView
            flutterView.attachToFlutterEngine(flutterEngine);
            flContainer.addView(flutterView, lp);
            return view;
        }
    

    fragment_index.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <!-- 嵌入flutter视图    -->
        <FrameLayout
            android:id="@+id/fl_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />
    
    </LinearLayout>
    

    dart代码实现:

    main.dart

    import 'package:edu/home_page.dart';
    import 'package:flutter/material.dart';
    import 'dart:ui';
    import 'dart:convert';
    
    void main() {
      runApp(_widgetForRoute(window.defaultRouteName));
    }
    // 获取路由名称
    String _getRouteName(String s) {
      if (s.indexOf('?') == -1) {
        return s;
      } else {
        return s.substring(0, s.indexOf('?'));
      }
    }
    
    // 获取参数
    Map<String, dynamic> _getParamsStr(String s) {
      if (s.indexOf('?') == -1) {
        return Map();
      } else {
        return json.decode(s.substring(s.indexOf('?') + 1));
      }
    }
    
    Widget _widgetForRoute(String url) {
      String route = _getRouteName(url);
      Map<String, dynamic> params = _getParamsStr(url);
      switch (route) {
        default:
          return MaterialApp(
            theme: ThemeData(
              primaryColor: Color(0xFF008577),
              primaryColorDark: Color(0xFF00574B),
            ),
            home: HomePage(route, params),
          );
      }
    }
    

    home_page.dart

    import 'package:flutter/material.dart';
    
    class HomePage extends StatefulWidget {
      String route;
      Map<String, dynamic> params;
    
      HomePage(this.route, this.params);
    
      @override
      State<StatefulWidget> createState() {
        return _HomePageState();
      }
    }
    
    class _HomePageState extends State<HomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Flutter页面'),
            automaticallyImplyLeading: false,
          ),
          body: Center(
            child: Text('首页'),
          ),
        );
      }
    }
    

    ok,Demo代码到这里就写完了,然后信心满满的run起来,发现直接崩了。这就是我要跟你说的其中一个坑,so架构的问题:
    在这里插入图片描述
    大部分老项目工程中用到的是armeabi架构,但flutter最低支持到armeabi-v7a,如果不做特殊处理,就会出现上面的Crash。怎么办?解决办法自然有,就是找到flutter module工程的构建物,把armeabi-v7a下的libFlutter.so拿出来,放到原生工程的armeabi下,我写了个shell脚本,然后通过Hook Gradle Task的方式插入到编译流程中去。

    copyFlutterSo.sh

    #!/bin/bash
    
    # 当前目录
    CURRENT_DIR="`pwd`"
    # 当前build目录,具体以工程为准
    BUILD_DIR="`pwd`/build"
    # gradle 5.6.2 armeabi so路径
    #ARMEABI_DIR="$BUILD_DIR/intermediates/merged_native_libs/debug/out/lib/armeabi"
    # gradle 4.10.1 armeabi so路径
    ARMEABI_DIR="$BUILD_DIR/intermediates/transforms/mergeJniLibs/$1/0/lib/armeabi"
    # armeabi-v7a so存放路径
    ARMEABI_V7A_DIR="$BUILD_DIR/intermediates/transforms/mergeJniLibs/$1/0/lib/armeabi-v7a"
    
    echo -e "33[47;30m ========== copy $1 libflutter.so ========== 33[0m"
    if [[ "$1" == "debug" ]]; then
      # 将libflutter.so copy到armeabi架构中去
      cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR}
      echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}"
    
    elif [[ "$1" == "profile" ]]; then
      # 将libflutter.so copy到armeabi架构中去
      cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR}
      # 将libapp.so也copy到armeabi架构中去
      cp -rf ${ARMEABI_V7A_DIR}/libapp.so ${ARMEABI_DIR}
      echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}"
      echo "copy ${ARMEABI_V7A_DIR}/libapp.so to ${ARMEABI_DIR}"
    
    
    elif [[ "$1" == "release" ]]; then
      # 将libflutter.so copy到armeabi架构中去
      cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR}
      # 将libapp.so也copy到armeabi架构中去
      cp -rf ${ARMEABI_V7A_DIR}/libapp.so ${ARMEABI_DIR}
      echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}"
      echo "copy ${ARMEABI_V7A_DIR}/libapp.so to ${ARMEABI_DIR}"
    fi
    

    Hook Gradle Task

    afterEvaluate { project ->
            android.applicationVariants.each { variant ->
               /**
                 * 由于flutter不支持armeabi,此处在merge(Debug|Profile|Release)NativeLibs与strip(Debug|Profile|Release)DebugSymbols之间插入一个任务,
                 * 将libflutter.so和libapp.so拷贝到merged_native_libs/(debug|profile/release)/out/lib/armeabi目录下,使它们能打到最终的apk里。
                 *
                 * 详情见copyFlutterSo.sh
                 */
                def taskPostfix = variant.name.substring(0, 1).toUpperCase() +
                        variant.name.substring(1)
                project.task("copyFlutterSo$taskPostfix") {
                    doLast {
                        exec {
                            // 执行shell脚本
                            commandLine "sh", "./copyFlutterSo.sh", variant.name
                        }
                    }
                }
                // 注意这个是在gradle 5.6.2版本的task
    //            project.tasks["copyFlutterSo$taskPostfix"].dependsOn(project.tasks["merge$taskPostfix" + "NativeLibs"])
    //            project.tasks["strip$taskPostfix" + "DebugSymbols"].dependsOn(project.tasks["copyFlutterSo$taskPostfix"])
    //
                // gradle 4.10.1,注意插入task的依赖顺序
                project.tasks["copyFlutterSo${taskPostfix}"].dependsOn(project.tasks["transformNativeLibsWithMergeJniLibsFor${taskPostfix}"])
                project.tasks["process${taskPostfix}JavaRes"].dependsOn(project.tasks["copyFlutterSo$taskPostfix"])
            }
        }
    }
    

    这样我们每次执行assembleDebug 或者assembleRelease都能自动将对应的armeabi-v7alibflutter.solibapp.so复制到armeabi下。

    然后再run一次,这个时候就真正把我们的混合工程跑起来了。

    工程最佳实践

    这里我提我们目前的做法:

    1. flutter module工程单独以git仓库托管
    2. 以submodule的方式将flutter module工程管理
    3. 调整依赖路径如下:
    // 引入flutter module
    setBinding(new Binding([gradle: this]))// new
    // module工程和setting.gradle文件同级
    evaluate(new File(                                                      // new
            settingsDir,                                               // new
            'edu_flutter_module/.android/include_flutter.groovy'                          // new
    ))
    
    include ':edu_flutter_module'
    project(':edu_flutter_module').projectDir = new File('edu_flutter_module')
    

    主要改动是将module工程和setting.gradle文件同级.

    1. 通过持续构建系统搭建Flutter构建环境,满足日常开发构建

    总结

    以module方式接入Flutter适合大部分存量的项目,目前我们项目已经以这种方式跑起来并且打通持续构建,目前已经踩了部分坑,总得来说经过这段时间对Flutter这个框架的实践我们团队已经掌握的新技术栈去为业务赋能,接下来的工作就是不断提升和优化新的研发体验,让统一技术栈这个目标不是说说而已。未来也将会输出更多干货,帮助业内的朋友也能加入到终端的研发变革中来。

  • 相关阅读:
    Java学习-068-多线程01:继承 Thread 类
    Linux-026-Centos Nginx 配置 pid 文件路径解决 service nginx status 提示:Can't open PID file /var/run/nginx.pid (yet?) after start: No such file or directory
    Linux-025-Centos Nginx 代理配置:同一端口代理不同服务
    Linux-024-Centos Nginx 代理配置:不同端口代理不同服务
    Linux-023-Centos Nginx Lua 脚本三种基本引用方式示例
    Linux-022-Centos Nginx 配置环境变量,常规命令
    Linux-021-Centos Nginx 配置服务管理,并设置开机启动
    Linux-020-Centos Shell 安装 Nginx 1.18.0
    Linux-019-Centos Shell 安装 Nginx 后启动时提示找不到Lua模块的libluajit-5.1.so.2文件,具体提示信息:./nginx: error while loading shared libraries: libluajit-5.1.so.2: cannot open shared object file: No such file or directory
    PySe-021-requests 使用 proxies 参数实现通过代理访问目标地址
  • 原文地址:https://www.cnblogs.com/cnchemmy/p/13220213.html
Copyright © 2020-2023  润新知