• Flutter混合栈管理


    由于Flutter框架出色的UI渲染能力,多平台一致性,大大的提高了研发效率,降低了人力成本。越来越多的厂商开始接入Flutter,但是很多厂商都是成熟的App,完全从头使用Flutter开发应用不现实,采用混合开发则是一种非常好的切入方式。

    那混合栈管理则是一个避免不了的话题。

    本文从0到1的角度阐释Flutter混合栈实现思路,介绍关键技术点而忽略部分细节,让大家从全局认识Flutter混合栈管理。

    一、引导问题

    • 为什么同一个原生页面不能显示两个及以上的flutter页面?
    • ​为什么在使用flutterboost时不能用官方的Navigator的API?

    二、页面栈结构分析

    常见的几种flutter页面与原生页面混合的情况如下:

    • 交替出现:原生页面与flutter页面交替出现
    • 连续出现:多个flutter页面连续出现
    • 同时出现:原生页面与flutter页面在子tab中同时出现
    • 复合情况:上述情况组合出现,页面结构比较复杂

    在这里插入图片描述

    2.1 标准栈的情况

    对于1,2两种情况比较简单,可以抽象成两个比较标准的栈结构,如下图:

    在这里插入图片描述

    对于Android侧:页面栈结构无需做任何改变,直接复用原生页面栈管理即可。
    对于Flutter侧:页面栈结构可以由Navigator实现,也是可以直接使用现有的栈管理即可。

    关键点:

    • Flutter与Activity都可以复用各自的线性栈结构功能
    • Flutter与Activity(或Fragment)的页面映射关系实现
    • 连续多个Flutter页面使用一个原生容器优化内存
    2.2 同级Tab的情况

    对于情况3,多个Flutter页面同级可以分为两种情况:
    在这里插入图片描述

    • 多个Flutter需要同时显示页面内容,会同时出现,如Pad情况

    由于一个FlutterEngine同一时刻只能渲染一块画布FlutterView,故如果需要显示这种页面结构,则必须使用多引擎。

    如富途牛牛Pad的情况,FlutterPage1所在主屏需要一个独立的引擎渲染,FlutterPage2所在副屏使用一个独立引擎并关联其上的其他页面栈,则可以转换为标准的栈结构情况。

    • 多个Flutter页面不需要同时显示页面内容,会交替出现,如手机App一级主tab,二级页面的子Tab

    在这里插入图片描述

    由于页面不会同时出现,这样的情况可以使用单引擎来实现,可以减少不必要的内存开销,也可以避开多引擎内存无法共享的问题。

    由于有多个同级的FlutterPage页面,flutter侧不能简单的使用Navigator线行栈结构实现。

    对于这种情况更好的做法是:抛弃强耦合的线性栈结构,用key-value的形式映射Flutter页面与原生页面,哪个原生页面在前台则渲染其对应的FlutterPage即可。

    关键点:

    • 在Flutter页面交替出现时单引擎如何渲染不同的Flutter页面
    • 原生页面与Flutter页面实现k-v映射关系
    2.3 复合情况

    对于复合情况,这里只考虑Flutter页面交替出现的场景,对于同时显示的页面需要使用到多引擎,页面管理更复杂,本次暂不考虑。

    在这里插入图片描述

    这种复合情况可以看组1,2,3种情况的结合体,将上述两小节的关键点合考虑即可。

    2.4 总结

    综合上述关键点,得出我们需要的栈管理结构需要支持一下的关键点:

    • 内存优化与共享:使用单引擎,需要解决在页面交替出现时单引擎如何渲染不同页面
    • 连续多个Flutter页面:使用单个原生容器承载,内部可以使用Navigator管理多个Flutter页面
    • 不连续的Flutter页面:一个Flutter页面一个原生页面(Fragment/Activity),栈结构由原生维护,原生页面与Flutter页面实现k-v映射关系(可以由Overlay实现)。

    整个栈结构可以抽象成如下结构图:

    在这里插入图片描述

    三、页面功能分析

    混合栈管理的主要功能是路由管理和页面生命周期管理,实现这两项能力就完成了混合栈管理的主体框架,后续可以在此基础上进行拓展,丰富使用能力。

    3.1 页面导航

    页面导航的基础能力:

    • push:打开一个新的Flutter页面并携带参数,需要考虑内部路由情况
    • pop:关闭一个Flutter页面并携带参数,需要考虑内部路由情况

    打开页面可以分两种:

    • 原生侧触发打开页
    • Flutter侧触发打开页

    路由管理:

    • 注册路由
    • 路由表管理
    3.2 页面生命周期

    页面生命周期的基础能力:

    • onPageShow:页面显示
    • onPageHide:页面隐藏
    • onBackground:App进入后台
    • onForeground:App回到前台

    由于上节页面栈结构分析可知,原生页面与Flutter页面是一对多关系,由于多个连续的Flutter页面在同一个原生容器内并有FlutterNavigator管理,故可以看成一个整体。

    所以,页面生命周期分发关系如下:

    • 原生页面与Flutter页面一对一,将原生页面生命周期事件通知给Flutter页面即可。
    • 原生页面与Flutter页面一对多,将原生页面生命周期事件通知给Flutter页面管理层,由管理层再次分发给到Flutter栈顶页面,故将一对多关系转化为一对一关系。

    四、混合栈整体框架

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G44TJQ6f-1661506696194)(assets/v2-cec9a709ee743c3abd1a2b7949b24a7a_1440w.jpg)]

    整体架构可以分为如下几个部分:

    • 页面导航接口
    • Native容器管理
    • 页面生命周期管理
    • Dart路由管理
    • 通知回调
    • 双层路由栈结构
    4.1 混合栈管理:Flutter Android侧类图

    在这里插入图片描述

    4.1 混合栈管理:Flutter侧类图

    在这里插入图片描述

    五、功能实现

    在了解了整体的设计思路后,进入实现部分。实现部分整体分为三块:

    • Dart侧双层路由栈结构、路由管理
    • Native侧容器管理、页面生命周期管理
    • 页面导航接口、通知回调

    主要技术点:

    • 单引擎交替渲染不同Flutter页面
    • 非线性页面栈实现原生页面与Flutter页面实现k-v映射关系
    • Dart侧内部栈结构的管理
    5.1 双层路由栈结构

    双层路由栈实现的关键点,非线性页面栈实现原生页面与Flutter页面实现k-v映射关系,使用k-v映射关系管理的好处是页面无顺序耦合关系,方便应付复杂的原生使用情况,谁在前台就显示谁即可。

    连续多个Flutter页面,使用单个原生容器承载,内部可以使用Navigator管理多个Flutter页面。

    5.1.1 外层路由栈结构Overlay

    Flutter中Overlay可以很好满足我们的需要,Overlay的介绍如下:

    Overlay 中维护了一个 OverlayEntry 栈,并且每一个 OverlayEntry 是自管理的
    Navigator 已经创建了一个 Overlay 组件,开发者可以直接使用,并且通过 Overlay 实现了页面管理
    Overlay 的应用场景有两个:实现悬浮窗口的功能,实现页面叠加

    Overlay部分代码:

    class Overlay extends StatefulWidget {
    
      const Overlay({
        Key? key,
        this.initialEntries = const <OverlayEntry>[],
        this.clipBehavior = Clip.hardEdge,
      }) ;
      
    }
    
    class OverlayState extends State<Overlay>{
    
    // 内部记录OverlayEntry
     final List<OverlayEntry> _entries = <OverlayEntry>[];
     
        void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }){
        // 更新OverlayEntry列表 setState
        }
    
        void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) {
        // 更新OverlayEntry列表 setState
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    OverlayEntry部分代码:

    class OverlayEntry extends ChangeNotifier {
    
      OverlayEntry({
        required this.builder,// 构建页面
        bool opaque = false, //是否不透明
        bool maintainState = false,// 是否保持状态
      });
      
      void remove() { 
        // 从Overlay中移除自己,并通知Overlay刷新
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通过改变Overlay中_entries的列表,可以轻松实现页面的显示、隐藏、添加和移除。

    5.1.2 内层路由栈结构Navigator

    由于内部路由结构都是有Flutter页面构成的,那么使用Navigator管理Flutter是一个必然的选则。

    Navigator部分代码

    class Navigator extends StatefulWidget {
    
      const Navigator({
        Key? key,
        this.pages = const >[], //页面集合
        this.onPopPage, // 页面pop监听
        this.observers = const [], // 页面导航监听 pop、push等
      })
      
    }
    
    class NavigatorState extends State{
      late GlobalKey _overlayKey;
      late List _effectiveObservers;
      
      Widget build(BuildContext context) {
           return Overlay( // Navigator内部也是Overlay实现的
                    key: _overlayKey,
                    initialEntries: const [],
                  )
      }
      
       bool canPop() {
       // 是否可以关闭当前页面,当只有一个页面时返回false,>1时返回true
       }
       
       void pop([ T? result ]) {
       // 弹出当前页面
       }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    Page部分代码:

    abstract class Page extends RouteSettings {
    
      const Page({
        this.key,
        String? name,
        Object? arguments,
        this.restorationId,
      }) : super(name: name, arguments: arguments);
    
    
      @factory
      Route createRoute(BuildContext context);
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过Navigator中管理Page就可实现页面的栈管理。

    5.1.3 双路由栈结构Overlay+Navigator

    由于外部路由的页面需要与原生容器绑定,需要一个唯一标识key来建立k-v关系,故需要通过扩展OverlayEntry增加key,可以提供原生页面映射Flutter页面的能力。

    再者外部路由的页面需要包含内部路由的情况,外部留有创建的Flutter页面需要具有导航的能力,故需要创建一个包含Navigator的widget作为页面根,然后将第一个页面添加到Navigator中,后续连续页面的导航能力转到Navigator即可。

    故可以扩展OverlayEntry,增加key字段,并返回带有Navigator的Widget管理容器。

    _ContainerOverlayEntry部分代码:

    // 扩展OverlayEntry增加key能力
    class _ContainerOverlayEntry extends OverlayEntry {
      _ContainerOverlayEntry(BoostContainer container)
          : containerUniqueId = container.pageInfo!.uniqueId!,
            super(
                builder: (ctx) => BoostContainerWidget(container: container),
                opaque: true,
                maintainState: true);
    
      ///This overlay's id,which is the same as the it's related container
      final String containerUniqueId;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    BoostContainerWidget带有Navigator:

    class BoostContainerWidget extends StatefulWidget {
      BoostContainerWidget({LocalKey? key, required this.container})
          : super(key: container.key!);
    
      final BoostContainer container;
    
      @override
      State createState() => BoostContainerState();
    
    }
    
    class BoostContainerState extends State {
      BoostContainer get container => widget.container;
     
     @override
      Widget build(BuildContext context) {
        return NavigatorExt(
              key: container._navKey, //关联GlobalKey
              pages: List>.of(container.pages),
              onPopPage: (route, result) {
                if (route.didPop(result)) {
                  assert(route.settings is BoostPage);
                  _updatePagesList(route.settings as BoostPage, result);
                  return true;
                }
                return false;
              },
              observers: [
                BoostNavigatorObserver(),
              ],
            );
      } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    BoostContainer用于记录页面和关联Navigator:

    class BoostContainer extends ChangeNotifier {
     
     // 原生传递过来的页面参数信息,包括唯一key=pageInfo!.uniqueId!
      final PageInfo? pageInfo;
    
    // 多个Flutter页面信息
      final List> _pages = >[];
      
      // 关联Navigator
      final GlobalKey _navKey = GlobalKey();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    整体双层页面栈结构如下如:

    在这里插入图片描述

    5.1.4 路由管理

    由上一节可知,所有页面最后都会被放到Navigator中,而Navigator需要使用Page与Route,通过提供路由工程,通过路由关联。

    typedef FlutterBoostRouteFactory = Route? Function(
        RouteSettings settings, String? uniqueId);
    
    final Map _routerMap = {
      
      "/HomePage": (settings, uniqueId) {
        Object? map = settings.arguments;
        var params = Map();
        if (map != null && map is Map) {
          params = Map.from(map);
        }
        return PageRouteBuilder(
          settings: settings,
          pageBuilder: (_, __, ___) => HomePage(params),
        );
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    5.2 Native侧容器管理

    容器管理的之前需要解决单个FlutterEngine如何显示多个Flutter页面。

    5.2.1 单引擎多Flutter页面渲染

    在这里插入图片描述

    Flutter当前版的设计中,FlutterEngine渲染与绘制分离的设计,单个FlutterEngine可以在多个FlutterView间切换显示。

    Flutter这种设计效果类似于投影仪,FlutterEngine相当于投影器,FlutterView页面相当于幕布,FlutterView内部有多种实现方式就相当于不同的材质的幕布。

    主要方法如下:

    • FlutterView#detachFromFlutterEngine:FlutterView断开关联的FlutterEngine
    • FlutterView#attachToFlutterEngine:FlutterView关联FlutterEngine

    通过上述方法就可以在不同FlutterView间显示不同的Flutter页面了。

    5.2.2 Native与Flutter页面映射

    Android显示页面需要使用Activity或Fragment,Fragment需要依附于Activity存在。Fragment比Activity更清理,可以组合到页面的各个地方。

    Flutter页面可能存在两种情况:

    • 页面有一个实例,如课堂首页
    • 页面有多个实例,如课程详情页

    页面有一个实例,使用路由最为key就可建立确定的映射关系。
    页面有多个实例,仅使用路由则无法区分同一类页面的映射关系。

    如果要满足上述两种情况,有两种方式:

    • 组合key,路由地址与另一参数组合为一个唯一key
    • 单个key,为每一个页面实例生成一个独一无二的key,如UUID

    所以可以采用单个key的形式,管理简单方便,然后将key(UUID)、路由地址和页面启动参数等打包传给Flutter侧用来创建具体的页面。

    主要参数如下:

    • key(UUID)
    • 路由地址
    • 是否带原生容器
    • 是否透明
    • 页面数据参数

    具体原生侧Flutter容器接口如下:

    public interface FlutterViewContainer {
        Activity getContextActivity();
    
        String getUrl();
    
        Map getUrlParams();
    
        String getUniqueId();
    
        void finishContainer(Map var1);
    
        default boolean isPausing() {
            return false;
        }
    
        default boolean isOpaque() {
            return true;
        }
    
        default void detachFromEngineIfNeeded() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    由于上一节可以,显示一个新的Flutter页面时需要获取FlutterEngine,为了确定FlutterEngine没有被其他页面使用,故需要在启动新页面前主动释放其他页面占用的引擎。

    为了知道当前是哪个FlutterViewContainer的实例持有FlutterEngine,需要记录FlutterViewContainer的实例来提提供必要的信息。

    5.2.3 页面生命周期管理

    在业务需求开发过程中,有很多逻辑是需要依赖页面的生命周期,而Flutter页面本身是无法感知原生页面的生命周期,故需要将原生页面的生命周期通过MethodChannel回调通知Flutter侧。

    Flutter页面需的生命周期:

    • onPageShow:页面显示
    • onPageHide:页面隐藏
    • onBackground:App进入后台
    • onForeground:App回到前台

    Activity生命周期:

    • onPageShow:onResume页面显示
    • onPageHide:onPause页面隐藏

    在这里插入图片描述

    Fragment生命周期:

    • onPageShow:onResume页面显示
    • onPageHide:onPause页面隐藏

    Fragment生命周期往往不准确,需要借助很多其他的回调方法已经页面状态来判断。

    在这里插入图片描述

    原生切换显示的时机:

    • 原生页面显示时:先将Flutter切到对应页面,然后FlutterView#attachToFlutterEngine
    • 原生页面隐藏时:FlutterView#detachFromFlutterEngine
    5.3 页面导航接口
    5.3.1 页面导航接口实现

    原生侧接口:

    • openPage:打开Flutter页面
    • closePage:关闭Flutter页面
    • NativeRouterApi#pushNativeRoute:接收Flutter侧打开页原生面请求
    • NativeRouterApi#pushFlutterRoute:接收Flutter侧打开Flutter页面请求

    Flutter侧接口:

    • 路由相关操作接口
    • 页面生命周期相关接口

    整体接口如下:

    class CommonParams {
      String pageName;
      String uniqueId;
      Map arguments;
      bool opaque;
      String key;
    }
    
    @HostApi()
    abstract class NativeRouterApi {
      void pushNativeRoute(CommonParams param);
      void pushFlutterRoute(CommonParams param);
      @async
      void popRoute(CommonParams param);
    }
    
    @FlutterApi()
    abstract class FlutterRouterApi {
      void pushRoute(CommonParams param);
      void popRoute(CommonParams param);
      void removeRoute(CommonParams param);
      
      void onForeground(CommonParams param);
      void onBackground(CommonParams param);
      
      void onContainerShow(CommonParams param);
      void onContainerHide(CommonParams param);
      void onBackPressed();
      
      void onNativeResult(CommonParams param);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    5.3.2 操作Flutter页面相关时序图

    打开与关闭流程图

    在这里插入图片描述

    六、多引擎混合栈

    多引擎主要是为了解决多个Flutter页面同时显示的问题,但是也会带了一些问题,如多个引擎见内存不可见,通信问题等

    多引擎在实际开发中还是有需求的,如何在上述实现单引擎混合栈的逻辑中能否支持多引擎呢?当然是可以的~~

    首先需要注意这里还是不能解决内存可见性问题,但是可以以引擎对象实例为key同时维护多个单引擎混合栈是很容易做到。

    效果如下图:

    在这里插入图片描述

    七、总结

    本文思路与实现均来源于FlutterBoost,整体实现比较清晰。
    主要关注点:

    • 外部页面栈使用k-v形式实现
    • 单引擎在多个FlutterView之间切换逻辑

    想深入研究一下实现细节的,可以跟着FlutterBoost源码过一遍能更好的加深理解!

    参考文档:
    Flutter Boost
    Flutter Boost3.0初探
    flutter_thrio
    Flutter Navigator 2.0原理详解
    Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则
    一款零侵入的高效Flutter混合栈管理方案,你值得拥有!

  • 相关阅读:
    实现简单的shared_ptr
    [C++ 网络协议] Windows平台下的线程
    如何安全得下载官方的Notepad++
    Idea加载gradle项目问题小记Gradle‘s dependency cache may be corrupt
    13-JVM调优实战-3
    微服务远程调用组件Feign的使用详解
    ElementUI组件库,分页组件靠右显示
    数字人技术在直播场景下的应用
    Python --- 入门与基本语法
    2022版 的IDEA创建一个maven项目(超详细)
  • 原文地址:https://blog.csdn.net/u013038616/article/details/126547880