25

腾讯课堂Flutter工程实践系列——接入篇

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUxMDk2MTAyMA%3D%3D&%3Bmid=2247485371&%3Bidx=1&%3Bsn=cfb1d45605b52d7ba0b58c2a3714b3cd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

vyIBJbZ.png!web

前言

课堂目前的技术栈是 React Native + Hybird + Native ,随着技术的演进多端融合的趋势越来越明显,而RN的弊端也突显出来,jsBridge性能不是最优,占用前端人力,定位问题链路较长等问题,让我们重新思考有没有更好的跨平台方案来解决业务场景,这个时候Flutter出现了,课堂iPad版本已经完成Flutter化并且稳定上线,让我们对这门新的跨平台技术有了信心。本文介绍课堂App如何实现在原生工程中嵌入Flutter,完成初步的框架搭建。

接入方案对比

在接入之前我们做了目前主流的两种接入方案进行了对比,分别是:

  • Standalone管理模式

  • Add To App管理模式

Standalone管理模式

ZRJJ7n2.png!web

Flutter Application标准工程,lib存放Flutter的代码,android是标准的Android工程、iOS是标准的的Xcode工程。

在我们团队实践后发现,统一管理模式有以下明显的缺点:

  • 比较适合 Flutter为主,Native为辅 的场景

  • 原生工程迁移成本高,需要另开独立工程维护

  • 工程臃肿,代码依赖耦合度高,无法独立编译构建

  • 混编模式下,相关工具链耗时大幅增加,导致开发效率降低

那既然Standalone管理模式不适合存量项目,那有没有一种能够直接嵌入到原生工程,把Flutter当做一个module来集成,答案是有的。

Add to App管理模式

创建一个Flutter Module工程如下图:

jeAbaqz.png!web

那么原生工程怎么接入,我们分别看下Android和iOS:

Android的接入

settings.gradle中进行以下配置:

zIruIva.png!web

app/build.gradle 引入Flutter Module:

ZNNJb2a.png!web

iOS的接入

Podfile引入Flutter:

3Ifmmiu.png!web

课堂最终选择了这种集成方式,因为它具有以下优点:

  • 三端分离,最小化程度影响原生工程

  • 可以更方便的实现源码依赖和产物依赖

  • 直接进行原生和Dart代码开发调试

因为绝大部分存量App不会重新开发一个Flutter App,因为成本太高,所以如果你的App并不是新项目笔者是比较推荐采用Add-To-App这种管理模式,可以逐步切入Flutter开发。

踩坑经验

这部分主要讲下Android接入Flutter过程中遇到一些典型的问题:

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

3ABvmqe.png!web

课堂用的gradle版本还是比较旧的,需要升级一下:

UjueUzz.png!web

升级完gradle,也要升级插件:

qYn6Fvz.png!web

异常2:AAPT error:resource android:attr/fontVariationSettings not found

reIbUzA.png!web

这个异常需要将compileSdkVersion升级到28,之前是26。

异常3:assert appProject !=null

36NBf2V.png!web

这个问题是Flutter SDK中的一个bug,我们主工程名并并不是 app ,而是 course ,所以在编译的时候会找不到project的问题,这里有两种解法:

  1. 重命名module名字,命名为app

  2. 修改flutter脚本(课堂选的是这种)

YzEbUnv.png!web

通过上图的修改,flutter脚本也能找到我们的工程,编译也ok了。

但编译ok不代表能真正run起来,以module接入的我们遇到的一个巨坑就是找不到 libflutter.so libapp.so 的问题。如下图所示:

QRzeMbE.png!web

出现这个问题的原因是我们项目为了减少包大小,只用了armeabi一个so架构,这也是很多旧项目采用的cpu架构。而 Flutter SDK最低支持到armeabi-v7a架构 ,如果不做特殊处理,就会出现上面的Crash。

一般我们的做法就是直接将armeabi-v7a的so复制到armeabi下,所以我们只需要在找到合适的时机将Flutter的构建产物copy到原生的armeabi架构下即可解决问题。 在混编Flutter的流程中,通过hook gradle task的方式插入自定义task来实现libflutter.so的复制

app/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 "\033[47;30m ========== copy $1 libflutter.so ========== \033[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

以上脚本的作 用就是找到armeabi-v7a的so直接copy一份到armeabi目录下,可以看到使用不同的Gradle版本,so的路径会有一些差异,这个也是分析Gradle Task的执行结果的时候发现的。

app/build.gradle

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"])

}

}

熟悉gradle编译的同学应该能看懂以上脚本,需要注意下不同的gradle版本除了构建产物的位置不同,连需要hook的task也有所不同。

nQnMn2N.png!web

u6Znaei.png!web

通过上图可以看到我们已经把debug版本的apk已经把 libflutter.so 从armeabi-v7a目录下复制到了armeabi目录下。

解决了so的问题,基本上我们能够正常的把项目编译运行起来了。下面我们尝试将我们的首页替换成Flutter的页面。

原生页面引入Flutter页面

先说明一点,写这篇文章的时候我们用的Flutter版本是: v1.12.13+hotfix5 ,跟以前的版本使用会有些差异,具体可以参考官方的wiki。

我们首先做的尝试是将首页替换成Flutter页面,做了以下调整:

CategoryFragment .java

Override

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.fragment_index, container, false);


FlutterEngine flutterEngine = new FlutterEngine(getActivity());

flutterEngine.getDartExecutor().executeDartEntrypoint(

DartExecutor.DartEntrypoint.createDefault()

);

flutterEngine.getNavigationChannel().setInitialRoute("category");



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/category_page_app.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: CategoryPageApp(route, params),

);

}

}

category_page_app.dart

import 'package:flutter/material.dart';


class CategoryPageApp extends StatefulWidget {

String route;

Map<String, dynamic> params;


CategoryPageApp(this.route, this.params);


@override

State<StatefulWidget> createState() {

return _CategoryPageState();

}

}


class _CategoryPageState extends State<CategoryPageApp> {

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text('Flutter页面'),

automaticallyImplyLeading: false,

),

body: Center(

child: Text('分类页'),

),

);

}

}

以上代码是我们尝试在原生 Fragment嵌入FlutterView 的方式来展示Flutter页面。详细的代码实现这里就不方便贴了,我们目前分类页Flutter迁移效果如下图:

可以看到分类页已经成功替换成Flutter页面了,基本跟React Native无异,目前课堂分类页版本正在灰度当中,相信很快大家就能够体验到我们迁移Flutter后的效果了。

总结

笔者花了很大的篇幅讲了课堂接入Flutter的过程,可以发现并不是一帆风顺的,遇到很多坑坑洼洼,但总的来说以module的形式接入算是完成了。本篇作为Flutter系列的第一篇,后续会有更多的工程实践在路上,帮助其他产品在接入过程中少走一些弯路和知识沉淀是我们的初衷,毕竟腾讯课堂就是一款传播知识的产品,也希望借助Flutter作为新的跨平台框架给产品带来更多赋能,为统一技术栈和多端融合打下坚实的基础。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK