Flutter多引擎路由开发实践

一、引言

背景

Flutter 多引擎模式允许在一个应用中同时运行多个 Flutter 引擎实例。这种模式特别适用于一些复杂的应用场景,如跨平台应用和需要独立状态管理的子模块。

在Flutter跨平台开发中,难免会遇到Flutter和原生交互的问题,特别是Flutter和原生页面切换问题。在单引擎方案中(常见的如咸鱼的FlutterBoost),管理Flutter与原生页面之间的路由可能会比较复杂,特别是在需要在原生和Flutter页面之间频繁切换的情况下。多引擎方案允许为不同的页面或模块创建独立的引擎,使得路由管理更为直观和灵活。

在跨平台应用中,可能会有多个独立开发的模块,它们各自有自己的状态管理需求。通过多引擎模式,每个模块可以运行在独立的 Flutter 引擎上,确保它们之间的状态不会互相干扰。此外,某些大型应用可能需要将不同的功能模块分开处理,以实现更好的模块化和独立性,这时多引擎模式也是一个理想的解决方案。

多引擎路由方案是为了解决如何在多个 Flutter 引擎之间高效地管理页面导航和状态的问题。随着应用复杂性的增加,传统的单引擎路由方式可能无法满足需求,比如在独立的模块间实现无缝导航等。因此,设计一个合适的多引擎路由方案,可以帮助开发者更好地管理应用的整体结构,并提升用户体验。

目标

在设计Flutter多引擎页面路由方案时,主要的目标和解决的问题包括以下几个方面:

  1. 性能优化
  • 目标:确保应用在复杂的页面切换和渲染过程中保持高性能,减少卡顿和延迟。

  • 问题:Flutter多引擎方案可能会带来额外的资源消耗,如内存和CPU的占用增加,需要平衡各个引擎的资源分配。

  1. 页面状态管理
  • 目标:在多个引擎之间实现一致的状态管理,确保页面切换时状态保持正确。

  • 问题:多个引擎之间可能会存在状态同步的问题,如何在不同引擎间高效地共享和传递状态是一个关键挑战。

  1. 路由的灵活性与扩展性
  • 目标:设计一个能够适应不同需求、具有高度灵活性和可扩展性的路由方案,以支持复杂的应用场景。

  • 问题:需要考虑如何处理复杂的页面跳转逻辑,包括在原生页面与Flutter页面之间的双向路由跳转,以及多引擎之间的跨引擎路由跳转。

  1. 模块化设计
  • 目标:将应用拆分为多个模块,每个模块可以独立开发、测试和部署,并且可以轻松地在不同引擎间集成。

  • 问题:如何设计一个合理的模块化架构,使得各个模块之间的依赖关系明确,同时保持良好的解耦性。

  1. 原生与Flutter的无缝集成
  • 目标:实现原生页面与Flutter页面之间的无缝切换,用户体验流畅。

  • 问题:在页面切换时,如何确保过渡的平滑性和一致性,以及如何处理原生与Flutter之间的通信和数据共享。

  1. 导航历史的管理
  • 目标:维护用户的导航历史,支持返回、前进等操作。

  • 问题:在多引擎场景下,如何有效管理导航历史,确保用户能够按预期行为返回到之前的页面。

这些目标和问题构成了设计Flutter多引擎页面路由方案时的核心关注点,通过有效的解决这些问题,能够实现一个高效、灵活、可扩展的多引擎路由方案。

二、基础概念与原理

2.1 Flutter 多引擎模式

Flutter 多引擎模式是一种在应用中同时使用多个Flutter引擎的架构方案,允许在一个应用中同时运行多个Flutter引擎实例。每个引擎实例可以独立管理其各自的路由、页面和状态,彼此之间互不干扰。这种模式在复杂应用中,特别是需要在Flutter页面和原生页面之间频繁切换时,提供了更大的灵活性。

2.2 为什么使用多引擎模式?

  • 路由管理:多引擎模式可以简化跨模块、跨平台的路由管理,使页面切换更加流畅。在需要频繁切换Flutter页面和原生页面时,多引擎模式可以更自然地处理路由和状态管理,原生页面与Flutter页面可以通过多个引擎实现无缝跳转,提升用户体验。

  • 隔离性:每个引擎实例独立运行,拥有自己的内部导航栈、UI 和应用程序状态,避免了单一引擎处理多个任务时可能出现的性能瓶颈和资源争夺问题。

  • 性能优化:通过多个引擎并行处理复杂任务,减少单个引擎的压力,提升整体性能。

  • 模块化设计:允许不同团队独立开发各自的模块,并在最终应用中集成,增强了代码的可维护性和扩展性。

2.3 Flutter 页面路由机制

Flutter 提供了一套完整的路由机制来管理页面导航,主要包括 NavigatorRoute 这两个核心概念。

Navigator:

  • Navigator 是一个堆栈管理器,用于管理应用中的路由栈。每次页面切换时,新的 Route 会被压入栈顶,而当页面返回时,栈顶的 Route 会被弹出。

  • 可以通过 Navigator.pushNavigator.pop 等方法来在路由栈上进行页面的添加和移除。

Route:

  • Route 表示导航堆栈中的一个页面。Flutter 提供了几种不同类型的 Route,如 MaterialPageRouteCupertinoPageRoute 和自定义的 PageRoute,用于不同风格的页面切换动画和行为。

管理页面路由:

  • 在单引擎的情况下,页面路由的管理主要依赖于 NavigatorRoute 的组合。通过 Navigator.push 可以将新的页面压入路由栈,而通过 Navigator.pop 则可以返回到前一个页面。

  • 另外,Flutter 还支持命名路由(Named Routes),通过 Navigator.pushNamedNavigator.pop 来管理页面跳转,这种方式使得页面跳转更为直观和易于管理。

2.4 单引擎情况下原生页面和 Flutter 页面之间的路由

在单引擎的情况下,原生页面和 Flutter 页面之间的路由跳转会遇到以下挑战:

状态管理的复杂性:

  • 由于原生和 Flutter 页面之间的状态并不共享,每次从原生页面返回到 Flutter 页面时,可能需要重新构建 Flutter 页面,导致数据的丢失或状态的重置。这要求在页面切换之间,需要精确管理和保存应用状态。

动画和体验不一致:

  • 原生和 Flutter 页面之间的过渡动画可能不一致,这会导致用户在不同平台之间切换时的体验割裂。需要额外处理动画一致性问题。常见的是频繁切换导致导航栈错乱的问题。

深度链接和全局导航的处理:

  • 深度链接(Deep Link)和全局导航的处理在单引擎情况下更为复杂,因为必须协调原生和 Flutter 两套导航系统,确保用户可以从任意入口进入并导航至目标页面。

性能问题:

  • 在频繁进行原生与 Flutter 页面切换时,可能会产生性能瓶颈,尤其是在启动或销毁 Flutter 引擎时。这种情况下,如何平衡性能和用户体验是一个重要的考虑因素。

通过回顾这些问题,我们可以更清晰地理解为什么在复杂应用中需要引入多引擎模式,以及多引擎页面路由方案所要解决的核心问题。

三、多引擎页面路由的挑战

在多引擎模式下,每个 Flutter 引擎实例都是独立的,这意味着它们各自维护独立的内存空间、状态和生命周期。因此,如何在多个引擎之间共享状态以及处理相关的挑战,成为了一个关键问题。

3.1 基础性的问题

一致性和同步问题

  • 在多个引擎之间保持状态的一致性是一个挑战,尤其是在状态频繁变更时。如果不同引擎之间的状态不同步,可能会导致应用出现意外行为。

  • 需要考虑如何确保状态同步的实时性,以及如何处理同步延迟和冲突问题。

性能问题

  • 状态共享可能涉及大量的数据传输,尤其是在使用平台通道或共享存储时。如果同步数据量大或频率高,可能会影响应用的性能。

  • 必须权衡状态同步的实时性与系统性能之间的关系。

状态的生命周期管理

  • 每个引擎都有独立的生命周期管理,这意味着当一个引擎被销毁时,如何处理它的状态成为一个问题。如果状态需要在引擎被销毁后仍然保持,就需要额外的机制来管理这些状态。

  • 通过合理设计状态管理机制,并选择合适的状态同步方法,可以有效应对多引擎模式下的状态共享挑战。

安全性问题

  • 状态共享需要确保数据的安全性,尤其是在跨平台通道和共享存储的情况下。需要考虑数据的加密和访问控制,防止敏感数据在状态共享过程中被泄露。

3.2 多引擎状态共享

使用平台通道(Platform Channels):

  • 可以利用 Flutter 提供的 MethodChannelEventChannelBasicMessageChannel 等平台通道,将状态信息在原生层和各个 Flutter 引擎之间进行传递。例如,使用 MethodChannel 将状态数据从一个引擎传递到原生层,再由原生层将这些数据分发给另一个引擎。

  • 这种方式虽然灵活,但需要手动管理状态同步,并且可能涉及较多的原生代码实现。

通过共享存储(Shared Storage):

  • 在各个引擎之间使用共享存储来保存状态信息,例如通过数据库、文件系统、或共享的内存区域(如 SharedPreferencesUserDefaults)。当一个引擎需要获取其他引擎的状态时,可以从这些共享存储中读取数据。

  • 这种方法适用于需要持久化的数据,但对于实时性要求较高的状态同步,可能会有延迟或一致性问题。

引入中间件或事件总线(Event Bus):

  • 在应用中引入事件总线(Event Bus)或中间件架构,通过发布-订阅模式实现各个引擎之间的状态同步。状态变化被发布为事件,各个引擎可以根据需要订阅这些事件并更新自己的状态。

  • 这种方式解耦了状态变化与引擎之间的关系,有助于保持代码的清晰和模块化,但引入事件总线可能会增加延迟和复杂度。

3.3 路由一致性

在多引擎模式下,不同的引擎可能负责不同的功能模块,这样的设计虽然能够有效地隔离模块、提升性能,但是在管理路由时,会带来一定的复杂性。

在多引擎模式下,确保路由一致性需要综合考虑不同的同步机制和设计模式。通过统一的路由管理服务、原生平台的中介作用、共享的路由栈或者事件总线,能够有效地管理多个引擎之间的路由状态同步,从而提升应用的可靠性和用户体验。

3.4 资源管理

在多引擎模式下,资源管理变得尤为重要,因为每个引擎独立运行,可能导致资源的重复加载和浪费。要在这种情况下高效地管理资源,需要采取一系列措施,确保资源的合理使用与优化。

3.4.1 共享资源管理层

建立一个共享的资源管理层,用于管理各个引擎之间的资源访问与分配。这个层可以作为一个全局服务,通过依赖注入或单例模式进行共享。

实现思路

  • 缓存系统:建立全局缓存系统,避免多个引擎重复加载相同的资源。资源首次加载后,存储在缓存中,其他引擎可以直接从缓存中读取。

  • 资源引用计数:通过引用计数来跟踪资源的使用情况,当所有引擎都不再需要某个资源时,才释放该资源。

3.4.2 统一的网络连接管理

在多引擎模式下,多个引擎可能会同时发起网络请求。为了避免重复请求和网络连接浪费,建议建立统一的网络连接管理机制。

在多引擎模式下,高效的资源管理至关重要。通过共享资源管理层、统一的网络连接管理、资源复用与懒加载、引擎生命周期管理以及异步任务与后台处理等方法,可以有效减少资源浪费,提升应用性能和用户体验。针对具体的应用需求,可以灵活组合以上策略,确保资源的高效使用。

实现思路

  • 网络请求代理:引入一个网络请求代理层,所有的网络请求通过该代理层进行统一管理。同样的请求只发起一次,其结果在各个引擎间共享。

  • 请求队列与合并:如果多个引擎需要相同的数据,可以将这些请求合并为一个,减少不必要的网络消耗。

3.4.3 资源复用与懒加载

资源复用与懒加载是有效的资源管理策略,特别是在多引擎模式下。可以通过懒加载减少初始资源消耗,通过资源复用避免重复加载。

实现思路

  • 懒加载:只在需要时才加载资源,避免占用多余的内存或计算资源。

  • 资源复用:尽可能复用已经加载的资源,避免多个引擎重复加载相同的资源。

3.4.4 引擎生命周期管理

管理各个引擎的生命周期,合理地分配和释放资源。未使用的引擎应该及时销毁或休眠,以释放资源。

实现思路

  • 引擎激活与休眠:当引擎不再需要活跃时,将其休眠或销毁,并释放所有占用的资源。

  • 资源池:为常用资源建立资源池,避免频繁的创建与销毁操作,提高资源利用率。

3.4.5 异步任务与后台处理

对于耗时的资源操作,可以采用异步任务或后台处理,避免阻塞主线程,提升应用的整体性能。

实现思路

  • 异步加载:将资源加载操作放在异步任务中,避免阻塞 UI 渲染。

  • 后台处理:使用后台线程处理复杂的计算或 I/O 操作,减少主线程的负担。

3.5 第三方库的支持

在使用多引擎替代单引擎时,确实需要特别注意一些第三方库和插件的兼容性问题。以下是一些关键点和注意事项:

3.5.1 Channel 管理

问题:在多引擎环境中,每个引擎都可能需要独立的 Channel 实例。这意味着原生与 Flutter 的通信通道需要在每个引擎之间正确管理。

注意事项

  • 确保每个引擎的 Channel 实例都正确创建并且不会互相覆盖。
  • 当一个引擎被销毁时,处理好相关 Channel 的销毁,避免造成消息发送错误或异常。

3.5.2 状态管理

问题:某些第三方库可能假设只有一个 Flutter 引擎在运行,这可能导致在多引擎环境下出现状态同步或管理问题。

注意事项

  • 验证第三方库的状态管理是否支持多引擎环境,特别是涉及到跨引擎的数据同步和状态保持。
  • 考虑使用专门为多引擎设计的状态管理解决方案,例如结合 EventBusSharedMemoryCache 实现跨引擎状态同步。

3.5.3 资源共享

问题:在多引擎环境中,多个引擎之间的资源(如 GPU 上下文、字体资源等)需要有效共享,以避免不必要的内存开销和性能问题。

注意事项

  • 确保使用支持多引擎资源共享的库或插件,特别是那些涉及图形渲染和其他资源管理的库。
  • 使用 FlutterEngineGroup 来优化资源共享,提高多个引擎的创建和管理效率。

3.5.4 插件兼容性

问题:一些插件可能依赖于单引擎的特性或行为,在多引擎环境下可能不兼容或表现异常。

注意事项

  • 在决定使用某个插件之前,检查其是否支持多引擎环境,查看插件的文档或联系插件维护者获取支持信息。
  • 如果某个插件不支持多引擎环境,考虑寻找替代插件或对插件进行适配。

3.5.5 解决方案

在进行多引擎替代单引擎的过程中,保持对第三方库和插件兼容性的关注,并且通过详细的测试来确保系统的稳定性和性能。

在多引擎环境中遇到第三方库不支持的情况时,可以通过以下两种主要解决方案来解决这些问题:

本地化第三方库,自行维护 Channel
  • 自定义和调整:对不支持多引擎的第三方库进行本地化处理,以确保它们能够适应多引擎环境的需求。这包括对库的源代码进行必要的修改和优化,以支持多个 FlutterEngine 实例的独立通信和管理。
  • 维护独立的 Channel:手动管理和维护 Channel 实例,确保每个 FlutterEngine 有独立的通信通道。这样可以避免 Channel 实例被覆盖或重复使用的问题。
自行实现替代插件
  • 开发自定义插件:在无法本地化现有库的情况下,可以开发自定义插件以替代功能。设计并实现一个新的插件,确保其能够支持多引擎环境中的所有必需功能。
  • 功能匹配和扩展:自定义插件应尽可能地匹配现有插件的功能,并根据需要进行扩展或优化,以满足特定的需求。

通过本地化第三方库和自行实现替代插件,能够有效应对多引擎环境中第三方库不兼容的问题。这些解决方案可以确保项目在复杂环境中的稳定性和兼容性,同时提升系统的性能和可靠性。实施这些策略时,务必进行充分的测试和验证,以确保所有修改和插件都能符合项目的需求和标准。

四、多引擎路由方案设计

4.1 设计原则

在设计多引擎路由方案时,需要遵循以下核心原则:

  • 性能优先: 确保路由方案不会对应用性能造成负担,特别是在内存和CPU使用方面。引擎之间的通信应该是高效的,避免不必要的资源消耗。

  • 模块化设计: 将不同的功能模块划分为独立的引擎,确保它们可以独立开发和维护。每个模块可以专注于自己的职责,而不需要了解其他模块的内部实现。

  • 易扩展性: 路由方案应易于扩展,能够支持未来的功能扩展或模块添加。引擎之间的接口应设计得尽可能通用,以便轻松引入新的模块或引擎。

  • 状态管理: 需要确保多引擎之间的状态同步,避免状态不一致的问题。可以使用共享服务层、全局状态管理器或依赖注入框架来管理状态。

  • 资源管理: 在多个引擎之间高效管理资源,避免重复加载和资源浪费。例如,共享常见的资源(如网络连接、缓存)以减少资源开销。

  • 容错性与稳定性: 路由方案应具备良好的容错性,能够处理异常情况,确保应用的稳定性。在某个引擎发生故障时,其他引擎应能正常工作。

  • 用户体验一致性: 确保用户在不同引擎之间切换时,体验的一致性。无论用户在哪个模块,路由逻辑应保持连贯性和一致性。

4.2 架构概览

以下提供方案的总体架构图,展示插件的各个层级和模块的基本构成。

4.2.1 多引擎插件(Flutter Meteor)架构

截屏2024-09-03 17.44.46.png

4.2.2 多引擎插件(Flutter Meteor)架构简介

Flutter层:

  • Navigator:用于管理 Flutter 页面导航,负责处理从 Flutter 端发起的导航请求。通过参数判断并决定打开 Flutter 页面、原生页面或启动新引擎。

  • EventBus:负责在 Flutter 端发送和接收消息。当发送消息时,通过 BasicMessageChannel 通知原生端,原生端再将消息分发给各个引擎和原生订阅者;当接收到消息时,则向 Flutter 端的订阅者传递消息。

  • SharedCache:用于处理 Flutter 端发起的共享内存读写操作,最终将数据存储在原生端,实现原生与多个 Flutter 引擎之间的内存共享。

  • SharedState:负责多引擎间的状态共享,基于 EventBusSharedCacheChangeNotifier 实现跨平台、跨引擎的状态同步。通过结合 Provider,可以在 Flutter 端实现 Widget 状态的同步。

Channel层:

  • NavigatorChannelNavigatorChannel 是一个 MethodChannel 对象,用于在原生和 Flutter 之间处理页面导航。

  • EventBusChannelEventBusChannel 是一个 BasicMessageChannel 对象,负责在原生与 Flutter 之间,以及 Flutter 不同引擎之间发送和接收消息。

  • SharedCacheChannelSharedCacheChannel 是一个 BasicMessageChannel 对象,用于在原生与 Flutter 之间,以及 Flutter 各个引擎之间进行共享内存的读写操作。

Native层:

  • Navigator: 负责处理原生端的页面导航请求,根据需求决定是打开原生页面还是启动新的 Flutter 引擎。
  • EventBus: 负责在原生端发送和接收信息,可以向原生订阅者发送消息,同时通过各引擎的 BasicMessageChannel 向 Flutter 发送消息;在接收到消息后,向自身的订阅者广播,并通过各引擎的 BasicMessageChannel 向 Flutter 转发消息。
  • SharedCache: 处理原生端发起的共享内存读写操作,确保原生和 Flutter 多引擎之间能够共享内存。

4.3 核心组件

  • 多引擎管理器(EngineManager) : 负责多引擎的创建和生命周期管理,维护各引擎之间以及引擎与原生之间的通信通道。确保各引擎在不同上下文中的独立运行,同时支持跨引擎与原生模块的双向通信。
  • 路由导航器(Navigator) : 实现跨引擎的路由器,负责管理跨引擎的页面导航。通过统一的路由机制,确保在不同引擎和原生模块间的页面跳转流畅、一致,并处理复杂的导航场景。
  • 引擎间通信机制: 使用 MethodChannel 实现 Flutter 与原生之间的路由导航,利用 BasicMessageChannel 处理多引擎之间的数据共享和通信,通过 EventBus 实现引擎间事件的广播与订阅,确保各模块之间的无缝交互。
  • 事件 总线 (EventBus) : 作为多引擎间以及各引擎与原生模块间的消息中枢,负责消息的发布与订阅,实现引擎间的协同工作,确保各模块能够及时接收到相关事件。
  • 共享存储(SharedMemoryCache) : 提供统一的缓存机制,管理多个引擎与原生模块间需要共享的内存数据,确保数据在不同引擎实例间的一致性与持久性。
  • 共享状态(SharedState) : 实现状态的同步与共享,负责更新多个引擎与原生模块间需要保持一致的状态,确保在复杂的应用场景中,各个引擎实例及原生模块间的状态能够实时同步,避免状态不一致带来的问题。

五、实现细节

5.1 引擎管理 (EngineManager)

如何在 Flutter 应用中初始化多个引擎,并且如何管理它们的生命周期?

利用FlutterEngineGroup来创建和管理多个引擎,这样引擎的生命周期由FlutterEngineGroup来自动维护。在 Flutter 应用中使用 FlutterEngineGroup 可以方便地创建和管理多个 FlutterEngine 实例。FlutterEngineGroup 允许多个引擎共享 Dart VM 的资源,从而减少内存占用和启动时间。

摘自multiple-flutters

在 Android 和 iOS 上添加多个 Flutter 实例的主要 API 基于新的 FlutterEngineGroup 类(Android API,iOS API)来构建 FlutterEngines,而不是以前使用的 FlutterEngine 构造器。

尽管 FlutterEngine API 是直接的且更容易使用,但从相同的 FlutterEngineGroup 中产生的 FlutterEngine 具有性能优势,可以共享许多常见的、可重复使用的资源,例如 GPU 上下文、字体度量和隔离组快照,从而实现更快的初始渲染延迟和更低的内存占用。

FlutterEngineGroup 中产生的 FlutterEngines 可以像通常构建的缓存 FlutterEngines 一样连接到 UI 类,如 FlutterActivityFlutterViewController

FlutterEngineGroup 创建的第一个 FlutterEngine 不需要继续存在,只要至少有一个 FlutterEngine 存活,后续的 FlutterEngines 仍可以共享资源。

FlutterEngineGroup 创建第一个 FlutterEngine 的性能特性与以前使用构造器构建 FlutterEngine 相同。

FlutterEngineGroup 中的所有 FlutterEngines 都被销毁后,下一次创建的 FlutterEngine 具有与第一次引擎相同的性能特性。

FlutterEngineGroup 本身不需要在所有生成的引擎之后继续存在。销毁 FlutterEngineGroup 不会影响现有的生成引擎,但会移除生成其他共享资源的 FlutterEngines 的能力。

代码示例(以iOS为例)

class MeteorEngineManager: NSObject {

    private static let channelProviderList = MeteorWeakArray<FlutterMeteorChannelProvider>()

    // FlutterEngineGroup 用于管理所有引擎
    private static let flutterEngineGroup = FlutterEngineGroup(name: "itbox.meteor.flutterEnginGroup", project: nil)

    public static func createFlutterEngine(options: MeteorEngineGroupOptions? = nil) -> FlutterEngine  {

        var arguments: Dictionary<String, Any> = Dictionary<String, Any>.init()

        let initialRoute = options?.initialRoute
        let entrypointArgs = options?.entrypointArgs
        if(initialRoute != nil) {
            arguments["initialRoute"] = initialRoute
        }
        if(initialRoute != nil) {
            arguments["routeArguments"] = entrypointArgs
        }

        var entrypointArgList:Array<String> = Array<String>.init()
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: arguments, options: .prettyPrinted)
            if let jsonString = String(data: jsonData, encoding: .utf8) {
                entrypointArgList.append(jsonString)
            }
        } catch {
            print("Error converting dictionary to JSON")
        }

        // 创建新引擎
        let engineGroupOptions = FlutterEngineGroupOptions.init()
        engineGroupOptions.entrypoint = options?.entrypoint
        engineGroupOptions.initialRoute = initialRoute
        engineGroupOptions.entrypointArgs = entrypointArgList
        engineGroupOptions.libraryURI = options?.libraryURI
        let flutterEngine: FlutterEngine = flutterEngineGroup.makeEngine(with: engineGroupOptions)
//        engineCache[flutterEngine.binaryMessenger] = flutterEngine
        return flutterEngine
    }

    /// 第一个引擎的Channel
    public static func firstEngineChannelProvider() -> FlutterMeteorChannelProvider? {
        return channelProviderList.allObjects.first
    }

    /// 最后一个引擎的Channel
    public static func lastEngineChannelProvider() -> FlutterMeteorChannelProvider? {
        return channelProviderList.allObjects.last
    }

    /// 所有引擎的Channel
    public static func allEngineChannelProvider() -> [FlutterMeteorChannelProvider] {
        return channelProviderList.allObjects
    }

    /// 保存Channel
    public static func saveEngineChannelProvider(provider: FlutterMeteorChannelProvider) {
        if !channelProviderList.contains(provider) {
            channelProviderList.add(provider)
        }
    }
}

代码中通过弱引用列表channelProviderList来维护各个引擎的methodChannel, 使得原生和Flutter或者Flutter引擎之间可以通过MethodChannel相互传递数据、同步状态和路由跳转等。

5.2 路由导航(Navigator)

在多引擎模式下,不同的引擎可能负责不同的功能模块,这样的设计虽然能够有效地隔离模块、提升性能,但是在管理全局路由时,会带来一定的复杂性。为了确保全局路由的一致性,需要有专门的路由管理机制。

5.2.1 页面路由管理

  • Flutter引擎内部页面跳转: 在Flutter内部,页面跳转直接使用Flutter框架提供的NavigatorRoute进行管理。这种方式是最常见的Flutter页面导航方案,能够在同一引擎中实现流畅的页面过渡和导航管理。
  • Flutter跳转至Native: 当需要从Flutter页面跳转到原生页面时,可以通过MethodChannel与原生进行通信。通过这种方式,Flutter可以调用原生代码来管理页面跳转,从而实现从Flutter页面切换到原生页面。
  • Native跳转至Flutter: 通常情况下,从原生页面跳转到Flutter页面时,会创建一个新的Flutter引擎,并通过原生代码来打开Flutter页面。这种方式有助于保持Flutter与原生之间的模块化,同时允许多个引擎独立运行,避免资源冲突。
  • Native跳转至Native: 在原生页面之间的跳转完全由原生代码处理。通过标准的原生导航机制,如UINavigationControllerFragmentManager,可以实现页面的跳转和管理,确保原生页面之间的无缝切换。

5.2.2 导航栈管理

Flutter和Native各自维护各自的路由栈,提供全局接口,通过MethodChannel 可以动态获取当前整个应用的路由栈,;利用这个全局路由栈可以使得原生也能像纯Flutter页面跳转一样实现pushReplacementNamed、pushNamedAndRemoveUntil、popUntil等操作,保证跳转方法的一致性;

导航栈结构示例:

截屏2024-09-03 17.45.04.png

FlutterContainer在原生对应的是iOS-FlutterViewController, Android-FlutterActivity;

NativeContainer在原生对应的是iOS-UIViewViewController, Android-Activity;

示例中的导航栈:FlutterPage1->...->FlutterPage5->NativeContainerB->FlutterPage6->...->FlutterPage8->FlutterPage9->...->FlutterPage12->NativeContainerE

在页面的导航过程中也是按着这个顺序push和pop操作,原生和Flutter的Navigator相互配合,在适当的时机切换页面。

5.2.3 Navigator工作流程

push跳转逻辑示例:

截屏2024-09-03 17.45.15.png

5.3 事件总线(EventBus)

5.3.1 EventBus工作流程

截屏2024-09-03 17.45.24.png

5.3.2 Flutter代码示例

import 'package:flutter/services.dart';
import 'package:flutter_meteor/engine/engine.dart';
import 'package:hz_tools/hz_tools.dart';

typedef MeteorEventBusListener = void Function(dynamic arguments);

class MeteorEventBusListenerItem {
  final String? listenerId;
  final MeteorEventBusListener listener;

  MeteorEventBusListenerItem(this.listenerId, this.listener);
}

class MeteorEventBus {
  final BasicMessageChannel methodChannel =
      const BasicMessageChannel('itbox.meteor.multiEnginEventChannel', StandardMessageCodec());

  // 工厂方法构造函数 - 通过MeteorEventBus()获取对象
  factory MeteorEventBus() => _getInstance();

  // instance的getter方法 - 通过MeteorEventBus.instance获取对象2
  static MeteorEventBus get instance => _getInstance();

  // 静态变量_instance,存储唯一对象
  static MeteorEventBus? _instance;

  // 获取唯一对象
  static MeteorEventBus _getInstance() {
    _instance ??= MeteorEventBus._internal();
    return _instance!;
  }

  //初始化...
  MeteorEventBus._internal() {
    //初始化其他操作...
    // _pluginPlatform = MeteorMethodChannel();

    methodChannel.setMessageHandler(
      (message) async {
        HzLog.d('收到来自原生的通知message:$message');
        receiveEvent(message);
      },
    );
  }

  final Map<String, List<MeteorEventBusListenerItem>> _listenerMap = {};

  /// 添加订阅者-接收事件
  static void addListener(
      {required String eventName, String? listenerId, required MeteorEventBusListener listener}) {
    HzLog.t(
        'MeteorEventBus addListener isMain:${MeteorEngine.isMain} eventName:$eventName, listener:$listener');
    var list = instance._listenerMap[eventName];
    list ??= <MeteorEventBusListenerItem>[];
    list.add(MeteorEventBusListenerItem(listenerId, listener));
    instance._listenerMap[eventName] = list;
  }

  /// 移除订阅者-结束事件
  /// 当listener 为空时会移除eventName的所有listener,因此慎用
  static void removeListener(
      {required String eventName, String? listenerId, MeteorEventBusListener? listener}) {
    HzLog.t(
        'MeteorEventBus removeListener isMain:${MeteorEngine.isMain} eventName:$eventName, listener:$listener');
    var list = instance._listenerMap[eventName];
    if (eventName.isEmpty || list == null) return;
    if (listenerId != null) {
      list.removeWhere(
        (element) => element.listenerId == listenerId,
      );
    } else if (listener != null) {
      list.removeWhere(
        (element) => element.listener == listener,
      );
    } else {
      list.clear();
    }
    instance._listenerMap[eventName] = list;
  }

  /// 已加订阅者-发送事件
  /// eventName 事件名称
  /// withMultiEngine 是否发送多引擎,默认true表示支持多引擎
  /// data 传送的数据
  static void commit({required String eventName, bool? withMultiEngine = true, dynamic data}) {
    HzLog.t(
        'MeteorEventBus commit isMain:${MeteorEngine.isMain} eventName:$eventName, data:$data, withMultiEngine:$withMultiEngine');
    if (withMultiEngine == true) {
      /// 多引擎则交给原生处理
      commitToMultiEngine(eventName: eventName, data: data);
    } else {
      /// 如果只支持当前引擎则直接调用
      commitToCurrentEngine(eventName: eventName, data: data);
    }
  }

  static void commitToCurrentEngine({required String eventName, dynamic data}) {
    var list = instance._listenerMap[eventName];
    HzLog.t(
        'MeteorEventBus commitToCurrentEngine isMain:${MeteorEngine.isMain} eventName:$eventName, data:$data, listeners:$list');
    if (list == null) return;
    int len = list.length - 1;
    //反向遍历,防止在订阅者在回调中移除自身带来的下标错位
    if (list.isNotEmpty) {
      for (var i = len; i > -1; --i) {
        MeteorEventBusListener listener = list[i].listener;
        listener.call(data);
      }
    }
  }

  static Future<dynamic> commitToMultiEngine({required String eventName, dynamic data}) async {
    HzLog.d(
        'MeteorEventBus commitToMultiEngine isMain:${MeteorEngine.isMain} eventName:$eventName, data:$data');

    /// 如果支持多引擎则交给原生处理
    Map<String, dynamic> methodArguments = {};
    methodArguments['eventName'] = eventName;
    methodArguments['data'] = data;
    // final result = await instance.methodChannel
    //     .invokeMethod(MeteorChannelMethod.multiEngineEventCallMethod, methodArguments);
    final result = await instance.methodChannel.send(methodArguments);
    HzLog.t('MeteorEventBus commitToMultiEngine isMain:${MeteorEngine.isMain} result:$result');
    return result;
  }

  static void receiveEvent(dynamic event) {
    if (event is Map) {
      final eventMap = event;
      final eventName = event['eventName'];
      HzLog.t('MeteorEventBus isMain:${MeteorEngine.isMain} allListeners:${instance._listenerMap}');
      var list = instance._listenerMap[eventName];
      list?.forEach((listenerItem) {
        listenerItem.listener.call(eventMap['data']);
      });
      HzLog.d('MeteorEventBus isMain:${MeteorEngine.isMain} eventName:$eventName, listeners $list');
    }
  }
}

5.4 共享缓存(SharedMemoryCache)

共享缓存用于在多个Flutter引擎之间或Flutter与原生之间共享状态信息。共享缓存可以分为内存缓存和磁盘缓存两种方式。

  • 磁盘缓存:
    磁盘缓存通常通过数据库、文件系统或共享存储区域(如SharedPreferencesUserDefaults)实现。当某个引擎需要获取其他引擎的状态时,可以从这些共享存储中读取数据,这样可以确保不同引擎在应用的不同生命周期中持久化和共享状态。
  • 内存缓存:
    内存缓存主要用于解决应用运行过程中,各个引擎以及原生模块之间的状态共享问题。这类缓存存在于内存中,仅在应用运行时有效,当应用退出后,这些状态不会被保留。我们可以自定义内存缓存来实现这一目的。

为了使得各个Flutter引擎以及Flutter与原生之间都能够共享缓存,缓存的实际存储应放置在原生端。需要共享的状态会被写入同一个共享缓存中,各个引擎通过各自的Channel与原生端通信,从而实现状态的存取和同步。这样不仅能够确保状态的统一管理,还可以避免因多个引擎独立运行而带来的数据不一致问题。

5.4.1 缓存结构示例图

截屏2024-09-03 17.45.48.png

如图所示缓存在原生端实现,每个引擎都有对应的Channel和原生进行通信,通过Channel存取共享状态。

5.4.2 Flutter代码示例

// Autogenerated from Pigeon (v21.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;

import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';

PlatformException _createConnectionError(String channelName) {
  return PlatformException(
    code: 'channel-error',
    message: 'Unable to establish connection on channel: "$channelName".',
  );
}

class _PigeonCodec extends StandardMessageCodec {
  const _PigeonCodec();
}

class SharedCacheApi {
  /// Constructor for [SharedCacheApi].  The [binaryMessenger] named argument is
  /// available for dependency injection.  If it is left null, the default
  /// BinaryMessenger will be used which routes to the host platform.
  SharedCacheApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
      : __pigeon_binaryMessenger = binaryMessenger,
        __pigeon_messageChannelSuffix =
            messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  final BinaryMessenger? __pigeon_binaryMessenger;

  static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();

  final String __pigeon_messageChannelSuffix;

  Future<void> setString(String key, String? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setString$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<String?> getString(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getString$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as String?);
    }
  }

  Future<void> setBool(String key, bool? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setBool$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<bool?> getBool(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getBool$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as bool?);
    }
  }

  Future<void> setInt(String key, int? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setInt$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<int?> getInt(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getInt$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as int?);
    }
  }

  Future<void> setDouble(String key, double? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setDouble$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<double?> getDouble(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getDouble$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as double?);
    }
  }

  Future<void> setList(String key, List<Object?>? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setList$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<List<Object?>?> getList(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getList$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as List<Object?>?)?.cast<Object?>();
    }
  }

  Future<void> setMap(String key, Map<String?, Object?>? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setMap$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<Map<String?, Object?>?> getMap(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getMap$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as Map<Object?, Object?>?)?.cast<String?, Object?>();
    }
  }

  Future<void> setBytes(String key, List<int?>? value) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.setBytes$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key, value]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return;
    }
  }

  Future<List<int?>?> getBytes(String key) async {
    final String __pigeon_channelName =
        'cn.itbox.flutter_meteor.CacheApi.getBytes$__pigeon_messageChannelSuffix';
    final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
      __pigeon_channelName,
      pigeonChannelCodec,
      binaryMessenger: __pigeon_binaryMessenger,
    );
    final List<Object?>? __pigeon_replyList =
        await __pigeon_channel.send(<Object?>[key]) as List<Object?>?;
    if (__pigeon_replyList == null) {
      throw _createConnectionError(__pigeon_channelName);
    } else if (__pigeon_replyList.length > 1) {
      throw PlatformException(
        code: __pigeon_replyList[0]! as String,
        message: __pigeon_replyList[1] as String?,
        details: __pigeon_replyList[2],
      );
    } else {
      return (__pigeon_replyList[0] as List<Object?>?)?.cast<int?>();
    }
  }
}

5.6 状态共享(SharedState)

利用 EventBusSharedMemoryCacheChangeNotifier 的组合,实现跨平台和跨引擎之间的状态同步机制。

  • EventBus:
    用于在不同的Flutter引擎之间,或者Flutter与原生之间,实现消息的高效传递。通过EventBus,可以广播和接收来自不同引擎或平台的状态变化事件,从而触发相应的状态更新。
  • SharedMemoryCache:
    作为共享状态的存储媒介,SharedMemoryCache确保了跨引擎和跨平台状态的一致性和持久性。各个引擎和原生模块可以通过它共享和访问相同的状态数据,实现状态的同步和统一管理。
  • ChangeNotifier:
    负责监听状态变化并通知订阅者,在状态发生改变时自动触发相应的UI更新。ChangeNotifier与EventBus和SharedMemoryCache配合,使得状态变化能够及时传播到各个引擎和平台,并驱动对应的UI响应。

业务端还可以结合 Provider 框架,通过注入ChangeNotifier来实现Widget的状态同步。Provider能够简化状态管理,使得跨引擎和跨平台的状态同步变得更加直观和高效,同时确保UI在状态变化时能够保持一致性和实时性。

5.6.1 实现流程

截屏2024-09-03 17.49.57.png

5.6.2 Flutter代码示例

import 'package:flutter/cupertino.dart';
import 'package:flutter_meteor/flutter_meteor.dart';

class GlobalStateService extends ChangeNotifier with GlobalSharedStateMixin {
  String eventName = 'GlobalStateChanged';
  GlobalStateService() {
    fetchState();
    MeteorEventBus.addListener(
      eventName: eventName,
      listener: (dynamic data) {
        if (data is String) {
          _updateCurrentEngineState(data);
        }
      },
    );
  }
  String _sharedState = "Initial State";
  String get sharedState => _sharedState;

  // Fetch state from native platform
  Future<void> fetchState() async {
    final String? state = await getString('state');
    _updateCurrentEngineState(state ?? 'Initial State');
  }

  // Update state on both Flutter and native platform
  Future<void> updateState(String newState) async {
    // _updateCurrentEngineState(newState);
    await setString('state', newState);
    MeteorEventBus.commit(eventName: eventName, data: newState);
  }

  void _updateCurrentEngineState(String newState) {
    _sharedState = newState;
    notifyListeners();
  }
}

六、实践案例

案例分析
demo示例

七、测试与调试

为多引擎路由方案测试时,你需要考虑如何模拟多引擎场景、如何测试路由逻辑的正确性、以及如何确保各个引擎之间的状态一致性。

多引擎测试的问题

在多引擎场景下,测试主要关注以下几个方面:

  • 引擎的初始化与管理:需要确保每个测试用例中都能正确地初始化和销毁多个引擎。

  • 路由的正确性:测试多个引擎之间的路由操作,确保路由能按照预期进行跳转。

  • 状态的共享与同步:在多个引擎之间进行状态的同步测试,确保状态能在不同引擎中保持一致。

  • 资源的管理:确保资源在多个引擎之间不会冲突或泄漏。

测试策略

  1. 模拟多引擎场景

  2. 测试引擎之间的状态共享

  3. 测试资源管理

八、总结与展望

8.1 总结

使用 Flutter 多引擎方案有以下几个主要优点:

8.1.1 性能提升

  • 负载分担:通过分配不同的任务给不同的引擎实例,避免了单一引擎的过度负载,提升了应用的响应速度和流畅度。

  • 独立资源管理:每个引擎实例独立管理资源,减少了资源竞争,从而提高了性能。

8.1.2 更好的原生集成

  • 原生与Flutter的平滑切换:多引擎模式下,每个引擎可以独立处理自己的视图和生命周期,使得在Flutter页面和原生页面之间的切换更加自然和高效。

  • 定制化路由管理:允许在原生页面和Flutter页面之间灵活定义和管理路由,满足复杂的导航需求。

8.1.3 提高稳定性

  • 错误隔离:一个引擎中的异常不会影响其他引擎的正常运行,增强了应用的稳定性。

  • 独立调试:可以分别对不同引擎进行调试,定位问题更加直接和快速。

8.1.4 适应复杂场景

  • 多任务并行:在需要同时处理多个复杂任务或显示多个Flutter视图时,多引擎模式提供了更好的并行处理能力。

  • 灵活的UI展示:可以在不同引擎实例上显示不同的Flutter UI,满足多窗口或多屏幕应用的需求。

8.2 展望

8.2.1 进一步提升性能:

  • 引擎间通信优化: 探索低延迟的引擎间通信机制,减少跨引擎状态同步和数据共享的延迟,提升应用响应速度和用户体验。

  • 资源预加载与缓存: 实现智能化的资源预加载和缓存机制,根据用户行为预测和预加载即将使用的资源,避免资源加载的卡顿问题,从而提升性能。

8.2.2 扩展功能与兼容性:

  • 跨引擎路由的统一管理: 开发更智能的全局路由管理器,支持复杂的跨引擎路由规则和动态路由调整,增强多引擎方案的灵活性和兼容性。支持深度链接(Deep Link)和全局导航的处理,确保用户可以从任意入口进入并导航至目标页面。

  • 更细粒度的路由管理:应该进一步精细化,不同的模块对应的引擎维护自己的路由;

  • 支持更多应用场景: 研究如何在多引擎模式下支持多窗口、多用户的应用场景,确保在复杂交互和多任务处理中的应用表现稳定。

  • 更细粒度的状态管理: 通过细化状态管理的粒度,进一步隔离引擎间的状态,支持复杂应用中的多态性和高可扩展性,适应更复杂的业务场景。

8.2.3 体验优化:

  • 导航操作的流畅性改进:
    当前在实现诸如 pushReplacementNamedpushNamedAndRemoveUntilpopUntil 等原生导航操作时,用户体验尚未达到最佳效果,需要进一步优化,提升页面跳转和返回的流畅性。

  • 初次引擎加载的速度优化:
    目前第一个引擎启动时存在加载较慢的问题,用户可能会经历一段白屏时间。未来可以通过预加载等技术手段进行优化,加快引擎的启动速度,从而改善用户的初次加载体验。

8.2.4 模块化开发:

  • 独立开发:不同的模块可以使用各自的引擎进行开发,互不干扰,提升开发效率和代码复用性。

  • 灵活的代码组织:通过多引擎,可以根据不同业务需求将应用分解为多个独立的部分,方便后期的维护和扩展。

随着多引擎应用场景的拓展和技术的不断发展,未来的优化方向和扩展功能将更加注重性能提升、兼容性扩展、开发者体验优化。通过这些优化措施,多引擎页面路由方案将更加成熟,能够更好地满足复杂应用的需求,并为业务提供更稳健的保障。

九、参考文献与资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容