我们如何使用Flutter和Rust构建AppFlowy
2023 年 10 月 23 日

我们如何使用Flutter和Rust构建AppFlowy

AppFlowy是一个使用Flutter和Rust构建的开源替代品,用于Notion (opens new window)。有关该产品的更多信息,请访问www.appflowy.io (opens new window)。自2021年11月13日在GitHub上首次发布以来,该项目已经获得了11.5k的星星和21位贡献者。项目的知名度增长速度很快。感谢大家的支持。

本文主要面向对AppFlowy技术设计感兴趣的黑客和开发者。它作为社区交流思想和共同增长知识的起点。我们欢迎对本文的任何反馈和建议。如果你对AppFlowy的技术设计感到好奇,或者想要参与其中的开发,那么本文对你来说将是一个很好的起点。

AppFlowy是一个使用Flutter和Rust构建的开源替代品,用于Notion (opens new window)。有关该产品的更多信息,请访问www.appflowy.io (opens new window)。自2021年11月13日在GitHub上首次发布以来,该项目已经获得了11.5k的星星和21位贡献者。项目的知名度增长速度很快。感谢大家的支持。

本文主要面向对AppFlowy技术设计感兴趣的黑客和开发者。它作为社区交流思想和共同增长知识的起点。我们欢迎对本文的任何反馈和建议。如果你对AppFlowy的技术设计感到好奇,或者想要参与其中的开发,那么本文对你来说将是一个很好的起点。 关于以下任何主题,请继续阅读:

  1. AppFlowy的DDD设计
  2. 采用Flutter支持多平台的策略
  3. Rust在项目中扮演的角色
  4. 逐步示例引导您浏览代码库

层次结构 #

领域驱动设计 #

AppFlowy的前端遵循领域驱动设计范例,包括展示层、应用层、领域层和基础设施层。为了使基础设施层更具可移植性,我们决定使用Rust来实现该层,不仅具有高性能和内存安全性。此外,我们选择使用Flutter来实现其他层,这将在跨平台部分进行解释。我们将这些层分为两个组件: ## 层定义

本节介绍了DDD层的基本概念,请随意跳过。

表示层

  • 负责向用户展示信息和解释用户命令。
  • 由小部件和小部件的状态组成。

应用层

  • 定义软件应该执行的任务。(UI代码或网络代码不在此处。)
  • 协调应用程序活动并将工作委托给领域层。
  • 不包含任何复杂的业务逻辑,只包含对用户输入进行基本验证,然后将其传递给领域层。

领域层

  • 负责表示业务概念。
  • 管理业务状态或委派给基础设施层。
  • 自包含,不依赖于任何其他层。 领域应该与其他层进行良好的隔离。

基础设施层

  • 提供支持上层的通用技术能力。
  • 处理API、持久化、网络等。
  • 实现仓库接口并隐藏领域层的复杂性。

考虑因素

每个层的抽象和复杂性不同,如下图所示。较高的层使用较低层提供的功能,每个层提供与其上下层不同的抽象。表示层具有较高的抽象和较低的复杂性,而基础设施层具有较低的抽象和较高的复杂性。我们应该始终将复杂性向下推,因为这将导致应用程序中的许多简化。还有一件我们应该注意的事情是依赖方向。较高的层依赖于较低的层,但较低的层不应该依赖于较高的层。例如,领域层不应该依赖于表示层。通过良好的层次结构和依赖关系,我们可以实现更好的代码组织和可维护性。 Flutter Values — 跨平台

我们的使命是使任何人都能创建适合自己需求的应用程序。我们的目标是提供Notion的功能,并结合数据安全和跨平台本机体验。我们决定通过坚持以下三个最基本的价值观来实现这一使命:

  • 数据隐私至上
  • 可靠的本机体验
  • 社区驱动的可扩展性

Flutter是由Google使用Dart编写的开源框架,对于我们来说,它是一个完美的选择,可以通过单一的代码库支持多平台应用程序,从而提供出色的一致的本机体验。想要了解更多信息,您可以在官方网站 (opens new window)上查看。

由于Flutter相对较新,您可能会想知道:

如果Flutter在其中一个平台上表现不佳会发生什么?

我们也有同样的担忧。AppFlowy的策略是重新编写UI层,使其不依赖于演示层。 如何以最低成本构建可在多个平台上运行的UI组件。下面是我们的处理方式。我们会尽可能使UI组件尽可能纯净,专注于UI渲染,将复杂的业务逻辑留给数据组件处理。因此,如果UI组件从一个平台切换到另一个平台,数据组件不需要进行任何更改,如下图所示。基础设施层将成为一个使用Dart/JS/Swift和Rust实现的混合基础设施层。

最复杂的层将是基础设施层。然而,我们将基础设施层分为两部分:接口和实现。我们创造了一个术语,FlowySDK,在Dart中定义接口,在Rust中实现。由于Dart的FFI (opens new window),很容易将接口与其实现进行绑定。例如,在Dart中调用HelloWorld()的接口,通过HelloWorldEvent映射到在Rust中调用hello_world()的实现。当触发事件时,事件被发送 通过dart_ffi传递给FlowySDK。在FlowySDK内部,一个映射将事件连接到相应的组件。每个组件都会声明它处理的事件,并在FlowySDK初始化时进行注册。

我们将这种模式称为事件分发。

优点:

  1. 横向扩展

我们可以轻松地添加或删除一个模块。例如,flowy User模块将自己注册到事件分发系统。当相应的事件发生时,处理程序将被调用。此外,我们可以将模块转换为动态库,并按需加载,以提高性能。

  1. 可移植和灵活

由于FlowySDK的FFI接口简单,因此很容易将其集成到不同的平台上。

  1. 更好的控制

我们可以无缝地将不同事件与不同的CPU/IO资源分类。例如,音频处理事件应比其他事件在分配CPU资源时具有更高的优先级。

缺点:

  1. 性能问题

我们使用protobuf进行序列化和反序列化。这可能会导致一些性能问题,尤其是在处理大量数据时。 AppFlowy前端

模块

AppFlowy被划分为许多模块,每个模块都有独立的功能和特性。通过使用模块化的架构,我们能够更轻松地管理和开发整个应用程序。

Protobuf和Flutter

为了实现Flutter和Rust之间的通信,我们选择使用Google的Protocol Buffers(https://zh.wikipedia.org/wiki/Protocol_Buffers)。然而,这种方法也存在一些问题。随着业务的增长,序列化和反序列化的时间会变长。

认知负荷

事件调度也有其缺点。实现一个函数似乎太过繁琐。为什么不像flutter rust bridge (opens new window)一样使用CodeGen从Rust的函数生成Dart的函数呢?原因是当我们开始编写AppFlowy时,Flutter在Web和桌面上的支持并不好。如果Flutter Mac桌面的性能不符合我们的需求,我们就必须在macOS原生上实现桌面功能。因此,我们需要swift_rust_bridge,这需要额外的工作量。考虑到我们目前只有两个人的团队,我们选择了一个折中的方案,即事件调度。

删除第一级标题,并删除其中的图片链接,同时尽可能删除Markdown格式错误和一些无用的段落,重新修饰整篇文章,使其读起来更加自然。 lar架构通过将一个模块与其他模块的功能隔离,使得更改一个模块不会影响其他模块的功能,开发人员可以根据个体客户的需求或偏好自定义应用程序。目前,AppFlowy由核心模块和用户模块组成,每个模块都分为两个部分,如下所示。在Flutter中实现的左侧部分遵循DDD设计模式,重点是UI渲染。由Rust crates组成的右侧部分专注于数据处理。我们将在核心模块中深入了解更多细节。

核心模块

核心模块为AppFlowy应用程序定义了基本的有界上下文,并充当协调其他模块的容器。基本实体如下所示。

“实体”是可引用的,因为它们具有允许我们引用它们的身份。您可以使用“实体”来表达您的业务。

用户可以拥有许多工作区,每个工作区包含许多应用程序。每个应用程序由多个视图组成。视图是一个自包含的对象。 以下是Markdown的中文翻译,并同时删除了一级标题,并删除了其中的图片链接,同时尽可能删除了Markdown格式错误和一些无用的段落,重新装饰了整篇文章,使文章读起来更加自然:并为任何可显示对象提供了抽象。在撰写本文时,我们只有Document对象。

我们使用flutter_bloc (opens new window)来实现每个实体的业务。

让我向您介绍AppFlowy如何使用DDD来实现业务规则。

  1. Widget接受用户交互并将交互转化为特定的Bloc事件。这些事件将被发送到相应的Bloc。Bloc将由DDD中的应用程序层代表,该层使用领域层提供的存储库或服务来处理Bloc事件引起的更改。

  2. 仅将数据传播到领域层。

  3. 存储库定义了实现其业务需求的接口和数据模型。我们使用从Rust端生成的protobuf来描述数据模型。例如,proto文件可以是ge。 从Rust结构体workspace.rs生成了workspace.dartworkspace.rs两个文件,它们代表相同的结构体,但使用不同的编程语言实现。使用protobuf使得在Flutter和Rust之间转换数据变得更加容易。然而,序列化和反序列化会带来一定的性能开销。

对于一般情况来说,这是可以接受的,但是在某些情况下会导致严重的性能问题,比如处理图像时可能会出现内存问题。虽然有很多优化的方法,但我们选择不深入细节。在这一步中,Dart对象将被封装成请求并传递到基础架构层。

  1. 将请求序列化为二进制数据,并通过Dart_ffi发送给FlowySDK。

  2. 请求将由调度程序安排。调度程序会找到请求的处理程序,并使用其数据调用它。每个模块声明它可以处理的事件,并向调度程序注册自己。

  3. 处理程序根据事件提取二进制数据并将其反序列化为特定的数据结构,然后执行一些业务逻辑。

  4. 将返回值序列化为二进制数据并发送给调度程序。

  5. 响应包含状态码,并将二进制数据作为返回值传递给调用方。

  6. 将二进制数据反序列化为特定的Dart对象。我们使用CodeGen自动将二进制数据映射到Dart对象。您可以查看code_gen.dart (opens new window)获取更多信息。

  7. 将protobuf对象传播到上层。

  8. 如果状态发生变化,Bloc将等待未来的完成并重新构建小部件。

您可以按照以下步骤深入了解codebase (opens new window)。如果您有任何问题,请联系[email protected]

感谢阅读本文。同时,欢迎提出任何建议。 以下是我们将在接下来的问题中涵盖的主题:

  1. AppFlowy如何使用Flutter Bloc
  2. AppFlowy的编辑器
  3. AppFlowy的离线与同步
  4. AppFlowy的后端
  5. AppFlowy的Flutter状态管理

请通过订阅我们的新闻通讯 (opens new window)继续关注。

最后,请花费1分钟时间参与调查 (opens new window)。我们希望收集反馈并了解您最感兴趣的内容。