构建属于自己的Flutter混合开发框架

LongfellowBerg 发布于3月前

所谓混合开发,指的是 App 的整体架构以原生技术栈为基础,将 Flutter 运行环境嵌入到原生 App 工程中,然后由原生开发人员为 Flutter 运行提供宿主容器及基础能力支撑,而 Flutter 开发人员则负责应用层业务及 App 内大部分渲染工作。

在这种开发模式下,好处十分明显。对于工程师而言,跨平台的 Flutter 框架减少了对底层环境的依赖,使用完整的技术栈和工具链隔离了各个终端系统的差异,无论是 Android、iOS 甚至是前端工程师,都可以使用统一而标准化的能力进行业务开发,从而扩充了技能栈。而对于企业而言,这种方式不仅具备了原生 App 良好的用户体验,以及丰富的底层能力,还同时拥有了跨平台技术开发低成本和多端体验一致性的优势,直接节省研发资源。

那么,在原生工程中引入 Flutter 混合开发能力,我们应该如何设计工程架构,原生开发与 Flutter 开发的工作模式又是怎样的呢?

混合开发架构

与纯 Flutter 工程能够以自治的方式去分拆软件功能、管理工程依赖不同,Flutter 混合工程的功能分治需要原生工程与 Flutter 工程一起配合完成,即:在 Flutter 模块的视角看来,一部分与渲染相关的基础能力完全由 Flutter 代码实现,而另一部分涉及操作系统底层、业务通用能力部分,以及整体应用架构支撑,则需要借助于原生工程给予支持。

我们可以通过四象限分析法,把纯 Flutter 应用按照业务和 UI 分解成 4 类。同样,混合工程的功能单元也可以按照这个分治逻辑分为 4 个维度,即不具备业务属性的原生基础功能、不具备业务属性的原生 UI 控件、不具备 UI 属性的原生基础业务功能和带 UI 属性的独立业务模块,如下图所示。

构建属于自己的Flutter混合开发框架

从图中可以看到,对于前 3 个维度(即原生 UI 控件、原生基础功能、原生基础业务功能)的定义,纯 Flutter 工程与混合工程并无区别,只不过实现的方式由 Flutter 变成了原生;对于第四个维度(即独立业务模块)的功能归属,考虑到业务模块的最小单元是页面,而 Flutter 的最终呈现形式也是独立的页面,因此我们把 Flutter 模块也归为此类,我们的工程可以像依赖原生业务模块一样直接依赖它,为用户提供独立的业务功能。当我们把这些组件及其依赖按照从上到下的方式进行划分,然后再整体看,就是一个完整的混合开发架构了,整个架构下图所示。

构建属于自己的Flutter混合开发框架

可以看到,原生工程和 Flutter 工程的边界定义清晰,双方都可以保持原有的分层管理依赖的开发模式不变。需要注意的是,作为一个内嵌在原生工程的插件,Flutter 模块的运行环境是由原生工程提供支持的,这也就意味着在渲染交互能力之外的部分基础功能(比如网络、存储),以及和原生业务共享的业务通用能力(比如支付、账号)需要原生工程配合完成,即原生工程以分层的形式提供上层调用接口,Flutter 模块以插件的形式直接访问原生代码宿主对应功能实现。

因此,不仅不同归属定义的原生组件之前存在着分层依赖的关系,Flutter 模块与原生组件之前也隐含着分层依赖的关系。比如,Flutter 模块中处于基础业务模块的账号插件,依赖位于原生基础业务模块中的账号功能;Flutter 模块中处于基础业务模块的网络插件,依赖位于原生基础功能的网络引擎库。

在混合工程架构中,像原生工程依赖 Flutter 模块、Flutter 模块又依赖原生工程这样跨技术栈的依赖管理行为,实际上是通过将双方抽象为彼此对应技术栈的依赖,从而实现分层管理的:即将原生对 Flutter 的依赖抽象为依赖 Flutter 模块所封装的原生组件,而 Flutter 对原生的依赖则抽象为依赖插件所封装的原生行为。

Flutter 混合开发流程

在常规的软件开发流程中,工程师的职责涉及从需求到上线的整个生命周期,包含需求阶段 -> 方案阶段 -> 开发阶段 -> 发布阶段 -> 线上运维阶段,这其实就是一种抽象的工作流程。

其中,和工程化关联最为紧密的是开发阶段和发布阶段。我们可以将工作流中和工程开发相关的部分抽离定义为开发工作流,根据生命周期中关键节点和高频节点,可以将整个工作流划分为如下七个阶段,即初始化 -> 开发 / 调试 -> 构建 -> 测试 -> 发布 -> 集成 -> 原生工具链。下图演示了Flutter和原生开发的工作流。

构建属于自己的Flutter混合开发框架

其中,前 6 个阶段是 Flutter 的标准工作流,最后一个阶段是原生开发的标准工作流。可以看到,在混合开发工作模式中,Flutter 的开发模式与原生开发模式之间有着清晰的分工边界:Flutter 模块是原生工程的上游,其最终产物是原生工程的依赖对象。从原生工程视角看,其开发模式与普通原生应用并无区别。

对于 Flutter 标准工作流的 6 个阶段而言,每个阶段都会涉及业务或产品特性提出的特异性要求,技术方案的选型,各阶段工作成本可用性、可靠性的衡量,以及监控相关基础服务的接入和配置等。每件事儿都是一个固定的步骤,而当开发规模随着文档、代码、需求增加时,我们会发现重复的步骤越来越多。此时,如果我们把这些步骤像抽象代码一样,抽象出一些相同操作,就可以大大提升开发效率。

优秀的程序员会发掘工作中的问题,从中探索提高生产力的办法,而转变思维模式就是一个不错的起点。以持续交付的指导思想来看待这些问题,我们希望整体方案能够以可重复、可配置化的形式,来保障整个工作流的开发体验、效率、稳定性和可靠性,而这些都离不开 Flutter 对命令行工具支持。

比如,对于测试阶段的 Dart 代码分析,我们可以使用 flutter analyze 命令对代码中可能存在的语法或语义问题进行检查;又比如,在发布期的 package 发布环节,我们可以使用 flutter packages pub publish --dry-run 命令对待发布的包进行发布前检查,确认无误后使用去掉 dry-run 参数的 publish 命令将包提交至 Pub 站点。

这些基本命令对各个开发节点的输入、输出以及执行过程进行了抽象,熟练掌握它们及对应的扩展参数用法,我们不仅可以在本地开发时打造一个易用便捷的工程开发环境,还可以将这些命令部署到云端,实现工程构建及部署的自动化。在Flutter 标准工作流中,常用的命令如下所示。

构建属于自己的Flutter混合开发框架

混合开发的基本设计原则

在混合开发中,我们需要重点关注的是项目的基本设计原则,即确定分工边界。下面从工程架构维度和工作模式维度来进行拆分。

在工程架构维度,由于 Flutter 模块作为原生工程的一个业务依赖,其运行环境是由原生工程提供的,因此我们需要将它们各自抽象为对应技术栈的依赖管理方式,以分层依赖的方式确定二者的边界。

而在工作模式维度,考虑到 Flutter 模块开发是原生开发的上游,因此我们只需要从其构建产物的过程入手,抽象出开发过程中的关键节点和高频节点,以命令行的形式进行统一管理。构建产物是 Flutter 模块的输出,同时也是原生工程的输入,一旦产物完成构建,我们就可以接入原生开发的工作流了。

在 Flutter 混合框架中,Flutter 模块与原生工程是相互依存、互利共赢的关系。

  • Flutter 跨平台开发效率高,渲染性能和多端体验一致性好,因此在分工上主要专注于实现应用层的独立业务(页面)的渲染闭环;
  • 原生开发稳定性高,精细化控制力强,底层基础能力丰富,因此在分工上主要专注于提供整体应用架构,为 Flutter 模块提供稳定的运行环境及对应的基础能力支持。

那么,在原生工程中为 Flutter 模块提供基础能力支撑的过程中,面对跨技术栈的依赖管理,我们该遵循何种原则呢?对于 Flutter 模块及其依赖的原生插件们,我们又该如何以标准的原生工程依赖形式进行组件封装呢?下面重点看一下原生工程是如何进行插件管理的。

可以看到,在原生 App 工程中引入 Flutter 运行环境,由原生开发主做应用架构和基础能力赋能、Flutter 开发主做应用层业务的混合开发协作方式,能够综合原生 App 与 Flutter 框架双方的特点和优势,不仅可以直接节省研发资源,也符合目前行业人才能力模型的发展趋势。

原生插件管理

在Flutter 应用中,Dart 代码提供原生能力支持主要有两种方式,即在原生工程中的 Flutter 应用入口注册原生代码宿主回调的轻量级方案,以及使用插件工程进行独立拆分封装的工程化解耦方案。

不过,无论使用哪种方式,Flutter 应用工程提供的标准解决方案,都能够在集成构建时自动管理原生代码宿主及其相应的原生依赖,然后只需要在应用层使用 pubspec.yaml 文件去管理 Dart 的依赖即可。

但对于混合工程而言,依赖关系的管理则会复杂一些。这是因为与 Flutter 应用工程有着对原生组件简单清晰的单向依赖关系不同,混合工程对原生组件的依赖关系是多向的,即Flutter 模块工程会依赖原生组件,而原生工程的组件之间也会互相依赖。

如果继续使用Flutter 的工具链管理原生组件的依赖关系,那么整个工程就会陷入不稳定的状态之中。因此,对于混合工程的原生依赖,Flutter 模块并不需要介入,完全交由原生工程进行统一管理才是正确的做法。而 Flutter 模块工程对原生工程的依赖,体现在依赖原生代码宿主提供的底层基础能力的原生插件上。

下面我们就以网络通信这一基础能力为例,展开说明原生工程与 Flutter 模块工程之间应该如何管理依赖关系。

网络插件依赖管理实践

众所周知,在 Flutter开发中,我们可以使用 HttpClient、http 与 dio 这三种通信方式来实现与服务端的数据交换。不过,在混合工程中,考虑到原生组件也需要使用网络通信能力,所以通常是由原生工程来提供网络通信功能,然后封装后提供给Flutter使用。这样,不仅可以在工程架构层面实现更合理的功能分治,还可以统一整个 App 内数据交换的行为。比如,在网络引擎中为接口请求增加通用参数,或者是集中拦截错误等。

在原生网络通信方面,目前市面上有很多优秀的第三方开源 SDK,比如 iOS 的 AFNetworking 和 Alamofire、Android 的 OkHttp 和 Retrofit 等。考虑到 AFNetworking 和 OkHttp 在各自平台的社区活跃度相对最高,因此下面就以它俩为例演示混合工程的原生插件管理方法。

网络插件封装

要想搞清楚如何管理原生插件,我们需要先使用方法通道来建立 Dart 层与原生代码宿主之间的联系。

1,Dart代码封装

对于插件工程的 Dart 层代码而言,由于它仅仅是原生工程的代码宿主代理,所以这一层的接口设计比较简单,只需要提供一个可以接收请求 URL 和参数,并返回接口响应数据的方法即可 ,如下所示。

class FlutterPluginNetwork {
  ...
  static Future<String> doRequest(url,params)  async {
    //使用方法通道调用原生接口doRequest,传入URL和param两个参数
    final String result = await _channel.invokeMethod('doRequest', {
      "url": url,
      "param": params,
    });
    return result;
  }
}

关于Flutter如何与原生进行交互,可以查看我之前的文章: 混合开发简介

完成Dart 层接口封装后,接下来再看一下 Android 和 iOS 代码宿主是如何响应 Dart 层的接口调用的。

2,原生端封装

前面说过,原生代码的基础通信能力是基于 AFNetworking(iOS)和 OkHttp(Android)做的封装,所以为了在原生代码中使用它们,我们首先需要分别在 flutter_plugin_network.podspec 和 build.gradle 文件中添加插件的依赖。对于iOS工程来说,在 flutter_plugin_network.podspec 文件中,声明工程对 AFNetworking 的依赖。

Pod::Spec.new do |s|
  ...
  s.dependency 'AFNetworking'
end

对于Android原生工程来说, 在 build.gradle 文件中添加对 OkHttp 的依赖,如下所示。

dependencies {
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
}

然后,我们需要在原生接口 FlutterPluginNetworkPlugin 类中,完成例行的初始化插件实例、绑定方法通道工作。最后,我们还需要在方法通道中取出对应的 URL 和 请求 参数,为 doRequest 方法分别提供 AFNetworking 和 OkHttp 的实现版本。

对于 iOS 的调用而言,由于 AFNetworking 的网络调用对象是 AFHTTPSessionManager 类,所以我们需要对这个类进行实例化,并定义其接口返回的序列化方式(本例中为字符串),然后剩下的工作就是用它去发起网络请求,使用方法通道通知 Dart 层执行结果。

@implementation FlutterPluginNetworkPlugin
...
//方法通道回调
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    //响应doRequest方法调用
    if ([@"doRequest" isEqualToString:call.method]) {
        //取出query参数和URL
        NSDictionary *arguments = call.arguments[@"param"];
        NSString *url = call.arguments[@"url"];
        [self doRequest:url withParams:arguments andResult:result];
    } else {
        //其他方法未实现
        result(FlutterMethodNotImplemented);
    }
}
//处理网络调用
- (void)doRequest:(NSString *)url withParams:(NSDictionary *)params andResult:(FlutterResult)result {
    //初始化网络调用实例
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //定义数据序列化方式为字符串
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    NSMutableDictionary *newParams = [params mutableCopy];
    //增加自定义参数
    newParams[@"ppp"] = @"yyyy";
    //发起网络调用
    [manager GET:url parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        //取出响应数据,响应Dart调用
        NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
        result(string);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        //通知Dart调用失败
        result([FlutterError errorWithCode:@"Error" message:error.localizedDescription details:nil]);
    }];
}
@end

Android 的调用类似,OkHttp的网络调用对象是 OkHttpClient 类,所以我们同样需要对这个类进行实例化。OkHttp的默认序列化方式就是字符串,所以我们什么都不用做,只需要 URL 参数加工成 OkHttp 期望的格式,然后就是用它去发起网络请求,使用方法通道通知 Dart 层执行结果即可。

public class FlutterPluginNetworkPlugin implements MethodCallHandler {
  ...
  @Override
  //方法通道回调
  public void onMethodCall(MethodCall call, Result result) {
    //响应doRequest方法调用
    if (call.method.equals("doRequest")) {
      //取出query参数和URL
      HashMap param = call.argument("param");
      String url = call.argument("url");
      doRequest(url,param,result);
    } else {
      //其他方法未实现
      result.notImplemented();
    }
  }
  //处理网络调用
  void doRequest(String url, HashMap<String, String> param, final Result result) {
    //初始化网络调用实例
    OkHttpClient client = new OkHttpClient();
    //加工URL及query参数
    HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
    for (String key : param.keySet()) {
      String value = param.get(key);
      urlBuilder.addQueryParameter(key,value);
    }
    //加入自定义通用参数
    urlBuilder.addQueryParameter("ppp", "yyyy");
    String requestUrl = urlBuilder.build().toString();

    //发起网络调用
    final Request request = new Request.Builder().url(requestUrl).build();
    client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Call call, final IOException e) {
        //切换至主线程,通知Dart调用失败
        registrar.activity().runOnUiThread(new Runnable() {
          @Override
          public void run() {
            result.error("Error", e.toString(), null);
          }
        });
      }
      
      @Override
      public void onResponse(Call call, final Response response) throws IOException {
        //取出响应数据
        final String content = response.body().string();
        //切换至主线程,响应Dart调用
        registrar.activity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
              result.success(content);
            }
        });
      }
    });
  }
}

需要注意的是, 由于方法通道是非线程安全的,所以原生代码与 Flutter 之间所有的接口调用必须发生在主线程 。而 OktHtp 在处理网络请求时,由于涉及非主线程切换,所以需要调用 runOnUiThread 方法以确保回调过程是在 UI 线程中执行的,否则应用可能会出现奇怪的 Bug,甚至是 Crash。

有些同学可能会有疑问,为什么 doRequest 的 Android 实现需要手动切回 UI 线程,而 iOS 实现则不需要呢?这其实是因为 doRequest 的 iOS 实现背后依赖的 AFNetworking,已经在数据回调接口时为我们主动切换了 UI 线程,所以我们自然不需要重复再做一次了。

在完成了原生接口封装之后,Flutter 工程所需的网络通信功能的接口实现,就全部搞定了。

Flutter 模块工程依赖管理

通过上面这些步骤,我们以插件的形式提供了原生网络功能的封装。接下来,我们就需要在 Flutter 模块工程中使用这个插件,并提供对应的构建产物封装,提供给原生工程使用了。

  • 第一,如何使用 FlutterPluginNetworkPlugin 插件,也就是模块工程功能如何实现;
  • 第二,模块工程的 iOS 构建产物应该如何封装,也就是原生 iOS 工程如何管理 Flutter 模块工程的依赖;
  • 第三,模块工程的 Android 构建产物应该如何封装,也就是原生 Android 工程如何管理 Flutter 模块工程的依赖。

1,模块工程功能实现

为了使用 FlutterPluginNetworkPlugin 插件实现与服务端的数据交换能力,我们首先需要在 pubspec.yaml 文件中,将工程对它的依赖显示地声明出来,如下所示。

flutter_plugin_network:
    git:
      url: https://github.com/cyndibaby905/flutter_plugin_network.git

然后,我们还得在 main.dart 文件中为它提供一个触发入口。在下面的示例代码中,我们在界面上显示一个 RaisedButton 按钮,在其点击回调函数时使用 FlutterPluginNetwork 插件发起了一次网络接口调用,并把网络返回的数据打印到了控制台上,代码如下。

RaisedButton(
  child: Text("doRequest"),
  onPressed:()=>FlutterPluginNetwork.doRequest("https://jsonplaceholder.typicode.com/posts", {'userId':'2'}).then((s)=>print('Result:$s')),
)

运行这段代码,点击 doRequest 按钮时会观察控制台输出,证明 Flutter 模块的功能表现是完全符合预期的。

构建属于自己的Flutter混合开发框架

构建产物封装

我们都知道,模块工程的 Android 构建产物是 aar,iOS 构建产物是 Framework。Flutter插件依赖的模块工程构建产物的两种封装方案,即手动封装方案与自动化封装方案。这两种封装方案,最终都会输出同样的组织形式(Android 是 aar,iOS 则是带 podspec 的 Framework 封装组件)。

如果我们的模块工程存在插件依赖,又该如何进行封装,它的封装过程是否有区别呢?简单的说,对于模块工程本身而言,这个过程没有区别;但对于模块工程的插件依赖来说,我们需要主动告诉原生工程,哪些依赖是需要它去管理的。

由于 Flutter 模块工程把所有原生的依赖都交给了原生工程去管理,因此其构建产物并不会携带任何原生插件的封装实现,所以我们需要遍历模块工程所使用的原生依赖组件们,为它们逐一生成插件代码对应的原生组件封装。

在纯Flutter 工程中,管理第三方依赖库使用的是.packages 文件存储,它使用的是依赖包名与系统缓存中的包文件路径。类似的,插件依赖也可以使用类似的文件进行统一管理,即.flutter-plugins。我们可以通过这个文件,找到对应的插件名字(本例中即为 flutter_plugin_network)及缓存路径,如下所示。

flutter_plugin_network=/Users/hangchen/Documents/flutter/.pub-cache/git/flutter_plugin_network-9b4472aa46cf20c318b088573a30bc32c6961777/

同时,插件缓存本身也可以被视为一个 Flutter 模块工程,所以我们可以采用与模块工程类似的办法,为它生成对应的原生组件封装。

iOS 构建产物封装

对于 iOS 而言,这个过程相对简单些,所以我们先来看看模块工程的 iOS 构建产物封装过程。

首先,在插件工程的 iOS 目录下,模块工程提供了带 podspec 文件的源码组件,podspec 文件提供了组件的声明(及其依赖),因此我们可以把这个目录下的文件拷贝出来,连同 Flutter 模块组件一起放到原生工程中的专用目录,并写到 Podfile 文件中。

#Podfile
target 'iOSDemo' do
  pod 'Flutter', :path => 'Flutter'
  pod 'flutter_plugin_network', :path => 'flutter_plugin_network'
end

原生工程会识别出组件本身及其依赖,并按照声明的依赖关系依次遍历,自动安装。然后,我们就可以像使用不带插件依赖的模块工程一样,把它引入到原生工程中,为其设置入口,并在 FlutterViewController 中展示 Flutter 模块的页面了。

不过需要注意的是,由于 FlutterViewController 并不感知这个过程,因此不会主动初始化项目中的插件,所以我们还需要在入口处手动将工程里所有的插件依次声明出来,如下所示。

//AppDelegate.m:
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    //初始化Flutter入口
    FlutterViewController *vc = [[FlutterViewController alloc]init];
    //初始化插件
    [FlutterPluginNetworkPlugin registerWithRegistrar:[vc registrarForPlugin:@"FlutterPluginNetworkPlugin"]];
    //设置路由标识符
    [vc setInitialRoute:@"defaultRoute"]; 
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}

然后,使用Xcode 运行这段代码,点击 doRequest 按钮,如果可以看到接口返回的数据信息能够被正常打印,证明我们已经可以在原生 iOS 工程中顺利的使用 Flutter 模块了。

构建属于自己的Flutter混合开发框架

Android 构建产物封装

与 iOS 的插件工程组件在 ios 目录类似,Android 的插件工程组件在 android 目录下。对于 iOS 的插件工程,我们可以直接将源码组件提供给原生工程,但对于 Andriod 的插件工程来说,我们只能将 aar 组件提供给原生工程,所以我们不仅需要像 iOS 操作步骤那样进入插件的组件目录,还需要借助构建命令,为插件工程生成 aar。使用下面的命令即可生成插件工程的aar包。

cd android
./gradlew flutter_plugin_network:assRel

命令执行完成之后,aar 就生成好了,aar 包位于 android/build/outputs/aar 目录下,我们打开插件缓存对应的路径,提取出对应的 aar即可。我们把生成的插件 aar,连同 Flutter 模块 的aar 一起放到原生工程的 libs 目录下,最后在 build.gradle 文件里引入插件工程,如下所示。

//build.gradle
dependencies {
    ...
    implementation(name: 'flutter-debug', ext: 'aar')
    implementation(name: 'flutter_plugin_network-debug', ext: 'aar')
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
    ...
}

然后,我们就可以在原生工程中为其设置入口,在 FlutterView 中展示 Flutter 页面,接下来就可以使用 Flutter 模块带来的高效开发和高性能渲染能力了。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); 
        setContentView(FlutterView);
    }
}

需要注意的是,与 iOS 插件工程的 podspec 能够携带组件依赖不同,Android 插件工程的封装产物 aar 本身不携带任何配置信息。所以,如果插件工程本身存在原生依赖(如 flutter_plugin_network 依赖 OkHttp ),我们是无法通过 aar 去告诉原生工程其所需的原生依赖的。对于这种情况,我们只需要在原生工程中的 build.gradle 文件里手动地将插件工程的依赖的插件(即 OkHttp)显示地声明出来即可,如下所示。

//build.gradle
dependencies {
    ...
    implementation(name: 'flutter-debug', ext: 'aar')
    implementation(name: 'flutter_plugin_network-debug', ext: 'aar')
    implementation "com.squareup.okhttp3:okhttp:4.2.0"
    ...
}

至此,混合模块工程及其插件依赖封装成原生组件的全部工作就完成了,接下来原生工程可以像使用一个普通的原生组件一样去使用 Flutter 模块组件的功能了。在 Android Studio 中运行这段代码,并点击 doRequest 按钮,可以看到,我们可以在原生 Android 工程中正常使用 Flutter 封装的页面组件了。

构建属于自己的Flutter混合开发框架

当然,考虑到手动封装模块工程及其构建产物的过程,繁琐且容易出错,我们可以把这些步骤抽象成命令行脚本,并把它部署到 Travis 上。这样在 Travis 检测到代码变更之后,就会自动将 Flutter 模块的构建产物封装成原生工程期望的组件格式了。

总结

众所周知,Flutter 模块工程的原生组件封装形式是 aar(Android)和 Framework(Pod)。与纯 Flutter 应用工程能够自动管理插件的原生依赖不同,混合工程的这部分工作在模块工程中是完全交给原生工程去管理的。因此,我们需要查找记录了插件名称及缓存路径映射关系的.flutter-plugins 文件,提取出每个插件所对应的原生组件封装,集成到原生工程中。

相比iOS插件管理来说,Android的插件管理比较繁琐。对于有着插件依赖的 Android 组件封装来说,由于 aar 本身并不携带任何配置信息,因此其操作以手工为主:我们不仅要执行构建命令依次生成插件对应的 aar,还需要将插件自身的原生依赖拷贝至原生工程。

为了解决这一问题,业界出现了一种名为fat-aar的打包手段,它能够将模块工程本身,及其相关的插件依赖统一打包成一个大的 aar,从而省去了依赖遍历和依赖声明的过程,实现了更好的功能自治性。但这种解决方案存在一些较为明显的不足,以下是使用中存在的一些问题:

  • 依赖冲突问题 :如果原生工程与插件工程都引用了同样的原生依赖组件(OkHttp),则原生工程的组件引用其依赖时会产生合并冲突,因此在发布时必须手动去掉原生工程的组件依赖。
  • 嵌套依赖问题 :fat-aar 只会处理 embedded 关键字指向的这层一级依赖,而不会处理再下一层的依赖。因此,对于依赖关系复杂的插件支持,我们仍需要手动处理依赖问题。
  • Gradle 版本限制问题 :fat-aar 方案对 Gradle 插件版本有限制,且实现方式并不是官方设计考虑的点,加之 Gradle API 变更较快,所以存在后续难以维护的问题。
  • 不更新 。fat-aar 项目已经不再维护了,最近一次更新还是 2 年前,对Android的新版本存在较大的风险。

因此,fat-aar 并不是管理插件工程依赖的好的解决方案,所以最好还是得老老实实地去遍历插件依赖,以持续交付的方式自动化生成 aar。

参考资料

1, Flutter 应用程序调试

2, Flutter For Web入门实战

3, Flutter开发之路由与导航

4, Flutter 必备开源项目

5, Flutter混合开发

6, Flutter的Hot Reload是如何做到的

7, 《Flutter in action》开源

8, Flutter开发之JSON解析

9, Flutter开发之基础Widgets

10, Flutter开发之导航与路由管理

11, Flutter开发之网络请求

12, Flutter基础知识

13, Flutter开发之Dart语言基础

14, Flutter入门与环境搭建

15, 移动跨平台方案对比:WEEX、React Native、Flutter和PWA

16, Flutter开发之异步编程

查看原文: 构建属于自己的Flutter混合开发框架

  • ticklishwolf
  • tinybird
  • blackswan
  • whitedog
  • WallaceIsidore
  • FingerArlene
  • ButlerGenevieve