- Flutter 和 Jetpack Compose 共享声明式、响应式 UI 模型,但在语言、生态系统和平台覆盖范围方面有所不同。
- 将组合映射到 Flutter 概念:可组合组件映射到组件,惰性列表映射到 ListView/GridView,Canvas 映射到 CustomPainter,主题映射到 ThemeData。
- Android 原生技能(生命周期、导航、资源、并发)通过 widgets、Navigator、assets 和 async/await 直接转移到 Flutter。
- 对于仅限 Android 的项目,Compose 表现出色;而当需要一套代码库用于 Android、iOS、Web 和桌面平台时,Flutter 则非常优秀。
如果你已经能熟练地使用 Jetpack Compose 编写 UI,并且想知道迁移到 Flutter 有多难,那么你正处于一个非常有利的位置。 这两个工具包都是声明式、响应式的,并且都由谷歌开发,因此你大部分的思维模式几乎可以完全照搬。主要区别在于编程语言(Kotlin 与 Dart)、项目结构以及每个框架与底层 Android 层(以及 Flutter 中的 iOS、Web 和桌面层)的交互方式。
本指南专为希望深入了解 Flutter 的 Jetpack Compose 开发人员编写,不包含任何营销噱头。 您将了解两个世界的核心概念是如何映射的:可组合组件与控件、修饰符与构造函数参数、惰性布局与 ListView/GridView、Canvas 与 CustomPainter、导航组合与 Navigator、Remember 与 StatefulWidget 等等。我们还会将您更广泛的 Android 背景知识(视图、生命周期、资源、Intent、后台工作)与 Flutter 中的对应概念联系起来,让您的学习过程更像是横向移动而不是向上攀登。
从 Jetpack Compose 到 Flutter:你的技能如何迁移
Flutter 是 Google 的 UI 框架,用于使用 Dart 语言构建跨平台应用程序;而 Jetpack Compose 是 Google 的现代 UI 工具包,用于使用 Kotlin 构建原生 Android 应用程序。 从底层来看,它们针对的是不同的运行时环境,但从架构上看,它们共享着同一个大理念:将 UI 描述为状态的函数,让框架来决定何时以及如何重新绘制。
在 Jetpack Compose 中,你考虑的是可组合函数、修饰符和重新组合;在 Flutter 中,你考虑的是组件、构造函数参数和重新构建。 尽管名称不同,但行为却惊人地相似:你构建一个 UI 元素树,每个节点都是不可变的,当状态改变时,框架会再次遍历这棵树以生成更新后的界面。
一个关键区别在于,Flutter 从设计之初就具备跨平台特性。 同一套 Dart 代码库可以面向 Android、iOS、Web、Windows、macOS 和 Linux 平台。Compose 正在扩展到 Android 以外的领域(例如 Compose Multiplatform),但 Flutter 的多设备支持目前更加成熟和完善,这正是许多以 Android 为先的团队在想要发布 iOS 或桌面应用时会考虑 Flutter 的原因。
您对 Android 平台本身的了解在 Flutter 项目中仍然非常有价值。 虽然 UI 层完全由 Dart 和组件构成,但 Flutter 依赖于 Android(以及 iOS)来获取权限、系统配置、平台 API、通知、后台运行以及许多其他功能,这些功能通过插件和平台通道访问。这意味着你对 Android 行为模式积累的所有经验都不会白费——它只是被转移到了更底层。
声明式 UI 模型:可组合组件与小部件
Jetpack Compose 和 Flutter 都实现了声明式 UI 模型:你描述的是在给定状态下 UI 应该是什么样子,而不是“如何”逐步改变视图。 与其在视图上调用 setter,不如在状态改变时重建视图树,让框架高效地进行差异比较和重绘。
在 Jetpack Compose 中,UI 元素是带有注解的可组合函数。 @Composable通常配置为 Modifier. 按钮可能是 Button(onClick = ..., modifier = Modifier.padding(16.dp))修饰符链装饰或布局可组合对象,而不改变其底层类型,而 Compose 使用重新组合来仅刷新树中输入发生变化的部分。
在 Flutter 中,UI 元素是 widgets——描述配置的普通 Dart 对象。 它们也是不可变的,并且以树状结构排列,但通常情况下,你不是传递修饰符,而是直接通过构造函数参数传递布局或样式参数,或者将一个控件包装在其他布局控件中。例如,你可能会这样写: Padding(padding: EdgeInsets.all(16), child: ElevatedButton(...)) 以达到类似的结果。
可组合组件和控件的生命周期都刻意设计得短暂且不可变。 它们只在新输入需要替换时才会存在;它们都不会尝试管理自身的生命周期或直接进行自我修改。这与旧版 Android View 的概念截然不同,在旧版 Android View 中,视图是长期存在的对象,可以随着时间的推移而被重用和修改。正因如此,你在 Flutter 中使用 Compose 的方式才会感觉如此自然。
从本质上讲,这两个框架的布局都遵循相同的父元素驱动、基于约束的模式。 父组件测量自身尺寸,并将约束条件传递给子组件,子组件根据这些约束条件选择合适的尺寸,然后父组件定位子组件。在 Flutter 中,你会看到这一过程直接以如下方式呈现: BoxConstraints在 Compose 中,这是通过 MeasurePolicy 实现来处理的。在这两种情况下,父组件都可以限制子组件——组件不能随意选择大小或位置。
应用架构:入口点、脚手架和布局
在 Android 上使用 Compose 时,您的入口点通常是 Activity (通常是 ComponentActivity)你打电话的地方 setContent 用于托管您的可组合文件。 从那里开始,构建可组合树,通常从一个……开始 MaterialTheme 以及定义您总体布局的表面或框架。
在 Flutter 中,入口点是 Dart。 main 调用函数 runApp 使用应用程序的根组件。 那个根通常是 MaterialApp or WidgetsApp 小部件,用于设置路由、主题、本地化和基本导航器。您显示的第一个“屏幕”通常使用一个 Scaffold 小部件的作用与此非常相似 Scaffold 在 Material 3 Compose 中:它提供了应用栏、主体、浮动操作按钮、抽屉等等。
对于简单的文本和静态内容,Compose 可能会默认将内容包裹起来,使大小与内容本身相匹配,而许多 Flutter 小部件默认会占用更多可用空间,除非受到限制。 例如,如果你放置一个 Text 即使它位于列内,也不会自动填充宽度。在 Flutter 中, Text 在里面 Column 根据父元素的约束条件,其行为可能有所不同。要在 Flutter 中居中内容,通常需要将元素包裹在 `<div>` 标签中。 Center 组件,或者使用布局组件,例如 Align, Row, Column和 Expanded 结合对齐属性。
线性布局几乎完美映射:Compose 有 Row 与 ColumnFlutter也是如此。 在 Flutter 中,你将 children 作为参数传递。 List<Widget> 并使用诸如以下属性控制间距和对齐方式 MainAxisAlignment 与 CrossAxisAlignment在 Compose 中,您依赖于 horizontalArrangement, verticalArrangement, horizontalAlignment 与 verticalAlignment一个有用的思考方式是:以“Arrangement”结尾的属性映射到 Flutter 的主轴,而以“Alignment”结尾的属性映射到交叉轴。
当您需要相对或重叠布局时,这些方法在概念上也是一致的。 在 Android XML 中,您可能会用到 RelativeLayout 或嵌套混合 LinearLayout 与 FrameLayout在 Compose 中,您可以进行创作 Row, Column 与 Box (或者编写自定义布局)。在 Flutter 中,类似的操作是…… Row, Column 与 Stack 结合定位子元素和对齐选项,您排列元素之间关系的思维模式几乎不会改变。
按钮、输入和交互
在 Jetpack Compose 中,构建按钮通常意味着使用 Button 或其 Material 变体之一,在 Material 3 下,该变体解析为一个特定的实现,例如 FilledTonalButton. 您提供 onClick lambda 表达式和可选样式,通常通过参数来实现,例如 colors 或者使用修饰符来调整内边距、宽度和对齐方式。
在 Flutter 中,等效的做法是使用类似这样的组件: FilledButton, ElevatedButton, TextButton or OutlinedButton. 每人都要花一个时间 onPressed 回调和 child 小部件——通常是 Text您可以通过传递参数来自定义它们。 style 通过 ButtonStyle 或者使用全局主题覆盖,您可以集中调整整个按钮系列的颜色、形状、高度和大小。
为了处理手势,Compose 依赖于修饰符,例如 Modifier.clickable 很多情况下都可以,但必要时也可以使用专门的手势检测器。 长按、拖动和自定义手势通常是通过专用的修饰符 API 和交互源来构成的。
Flutter 公开了一个显式的 GestureDetector 一个可以包裹在任何没有内置手势支持的应用上的小部件。 它提供多种回调方式: onTap, onDoubleTap, onLongPress, onVerticalDragStart, onVerticalDragUpdate, onHorizontalDragEnd 以及其他许多小部件,例如 ElevatedButton 已经暴露出来 onPressed 属性,但对于完全自定义的 UI 元素,您可以使用 GestureDetector 或更高级别的组件,例如 InkWell 用于材料涟漪反馈。
Flutter 中的文本输入是通过以下方式管理的: TextField or TextFormField其风格与 Compose 的风格相似 TextField 与 OutlinedTextField 可组合性。 您可以使用以下方式配置提示、标签、错误和边框: InputDecoration 类似于你使用的方式 TextFieldDefaults 或者在 Compose 文本字段中使用参数。与 Compose 类似,通常通过更改状态并重建装饰器来响应式地显示错误消息,而不是手动操作视图。
列表、网格和滚动内容
Jetpack Compose 为列表提供了两种主要策略:简单策略和简单策略。 Column/Row 对于小型集合,可以使用迭代法; LazyColumn/LazyRow/LazyVerticalGrid/LazyHorizontalGrid 适用于大型或动态列表。 惰性容器只组合可见的内容,这样在处理数千个项目时可以保持较高的性能。
Flutter 也采用了类似的小尺寸与大尺寸的设计方法,但使用了不同的组件。 对于一个可以完整显示在屏幕上的小型列表,你可以直接使用 Column 并将你的数据映射到 children对于任何可以滚动的东西,你都会伸手去拿 ListView or GridView,并采用构建器构造函数,仅在需要时才延迟创建子组件。
Flutter 中的常见模式是 ListView.builder这与 Compose 的惰性列表项 DSL 相呼应。 您提供 itemCount 和 itemBuilder 回调函数;Flutter 调用该构建器,并传入一个从 0 到 的索引。 itemCount - 1 每当有新项目进入视图时。在构建器中,您可以返回几乎任何小部件——从简单的 ListTile 使用文本和图标来创建复杂的自定义列表行。
对于网格,Compose 的 LazyVerticalGrid 与 LazyHorizontalGrid 映射到 Flutter 的 GridView 小部件。 Flutter 通常不会直接将列数传递给网格,而是使用代理,例如: SliverGridDelegateWithFixedCrossAxisCount or SliverGridDelegateWithMaxCrossAxisExtent 用于控制单元格的布局方式。这些委托封装了诸如“列数”或“最大单元格宽度”之类的规则,其理念类似于您在“组合”中使用的网格大小参数。
两种工具包的滚动行为也类似。 Compose 的惰性列表内置了滚动功能;你不需要将它们包裹在额外的滚动容器中。在 Flutter 中,许多列表和网格组件本身就是可滚动的,但对于需要滚动的单个、非重复内容,你可以使用 SingleChildScrollView构建自定义可滚动页面就变成了嵌套或组合 sliver 以适应更高级的用例。
自适应和响应式用户界面模式
Compose 为您提供了多种响应式设计策略:自定义布局、 BoxWithConstraints, WindowSizeClass 以及 Material 3 自适应库。 这些功能可以根据可用空间、姿势和设备类别改变您的构图,并且您可以根据项目的复杂性将它们混合使用。
Flutter 并没有试图直接模仿这些 API,但其基本思想是相同的:检查约束和屏幕特征,然后分支你的布局。 两种主要工具是 LayoutBuilder 与 MediaQuery. LayoutBuilder 通行证 BoxConstraints 向下移动,以便您可以交换或重新排列超过特定宽度或高度的小部件。 MediaQuery 显示屏幕尺寸、方向、边距和像素密度等高级断点信息。
与其追求 Compose 的自适应解决方案与 Flutter 的解决方案一一对应,不如根据你的设计需求来思考,这样更有效。 一旦您了解了您的 UI 应该如何在手机、平板电脑和台式机上进行自适应,您就可以通过 Compose 的以下方式表达该逻辑: WindowSizeClass 以及自适应布局或 Flutter 的约束驱动和媒体驱动分支。相同的设计理念——不同的 API。
状态管理:记住 StatefulWidget 与其他状态管理方式的区别及其他
Jetpack Compose 使用以下方式存储临时 UI 状态 remember 以及像州持有者这样的 mutableStateOf,常与 ViewModel 以及用于实现更持久状态的架构组件。 当状态发生变化时,会发生重组,相关的可组合元素会获得新的值。
Flutter 的底层状态故事围绕着…… StatefulWidget 及其相关的 State 目的。 您可以通过扩展来定义一个需要保存状态的小部件。 StatefulWidget然后实施一个单独的 State<MyWidget> 用于存储可变字段的类。每当您更新这些字段时,您都会调用它。 setState()这会将组件树的该部分标记为已修改,并触发重建。在这个层面上,它与存储 Compose 状态非常相似。 remember 当值发生变化时,使可组合对象失效。
对于更复杂的应用程序,Flutter 大量依赖社区和第一方模式: ProviderRiverpod、Bloc、Redux 风格的商店等等。 这些组件相当于 Android 架构栈:ViewModel + LiveData/Flow + Compose 项目中的存储库。它们集中管理业务逻辑,并公开驱动组件重建的响应式数据流。如果您有 Compose 方面的经验,即使 API 有所不同,您也会发现其中许多模式似曾相识。
Android 开发人员经常感到惊讶的一点是,Flutter 中的无状态和有状态组件都会频繁重建——在动画期间甚至可能每一帧都会重建。 区别不在于重建频率,而在于可变状态的存储位置: StatefulWidget 给你一个伴侣 State 重建后仍然存在的对象,就像…… remember 允许值在 Compose 中重新组合后仍然存在。
绘画、动画和视觉润色
如果你曾经直接使用过 Android 系统…… Canvas 与 Drawable,Compose的 Canvas 可组合性可能让人感觉很简单。 它提供了一种在 Kotlin 中绘制形状、图像和文本的声明式方法,隐藏了传统自定义视图中许多繁琐的命令式操作。
Flutter 通过以下方式提供了一个类似的绘图界面: Canvas API,可通过以下方式访问 CustomPaint 与 CustomPainter. 你实施了一个 CustomPainter 重写的类 paint 使用以下方法在画布上绘制: Paint 对象、路径、变换等等。然后,将该绘制器附加到 CustomPaint 小部件。Compose 和 Flutter 的底层都依赖于 Skia 引擎,因此基本元素——线条、路径、着色器——与 Android 的 2D 渲染非常相似。
对于动画,Flutter 依赖于一个围绕其构建的显式动画系统。 AnimationController, Animation<T> 还有 Tweens,以及丰富的动画组件。 您实例化一个控制器(通常使用) SingleTickerProviderStateMixin 对于垂直同步),定义将 0-1 进度映射到域值的 CurvedAnimations 或 Tweens,然后将它们连接到诸如 FadeTransition, ScaleTransition, AnimatedBuilder 或者隐式组件,例如 AnimatedContainer动画系统也展现了 AnimationStatus 回调函数用于响应开始、完成或反转。
Jetpack Compose 的动画 API 从上到下都是声明式的,包含诸如以下函数: animate*AsState过渡效果和动画可见性。 大多数情况下,无需手动管理控制器,只需描述目标状态,框架即可随时间自动驱动插值。如果需要更精细的控制,您仍然可以访问底层原语,但通常的方法比传统的 Android XML 或命令式动画代码更简洁。
从概念上讲,这两个工具包的使用方式相同:保持组件/可组合组件轻量且纯粹,通过它们推送随时间变化的值,并让框架处理插值和失效。 作为一名 Compose 开发者,Flutter 的额外明确性…… AnimationController 起初可能会感觉有点老派,但它能让你对时间、曲线和编曲进行非常精细的控制。
样式、主题、字体和素材
现代应用程序的成败取决于其外观设计,因此 Flutter 和 Compose 都非常重视主题和样式。 组合用途 MaterialTheme 通过配色方案、字体和形状定义,您可以嵌套主题来覆盖子树的值,包括强制特定区域使用浅色或深色表面。
在 Flutter 中,等效项是 ThemeData 传递给 MaterialApp or Theme 小部件。 您可以定义主色、亮度、字体和组件特定的主题,例如 elevatedButtonTheme, textButtonTheme, appBarTheme 还有更多功能。您可以通过将子树包装起来,在本地覆盖主题。 Theme 复制父组件并调整某些字段的小部件。可以通过提供以下信息在应用程序级别切换浅色和深色模式: theme 与 darkTheme 并控制 themeMode.
文本样式是大家熟悉的领域:在 Compose 中,您可以直接传递简单的属性到 Text 或提供 TextStyle 目的。 Flutter 也通过这种方式实现了这一点。 Text 接受以下参数的小部件 TextStyle 通过其 style 参数。 TextStyle 涵盖字体系列、字号、字重、字间距、行高、装饰等。您可以在其中定义全局文本主题。 ThemeData.textTheme 并像使用字体排印一样,在各处引用它们。 MaterialTheme 在 Compose 中。
字体和图像是通过资源而不是 Android 的传统方式来管理的。 /res 目录树。 Flutter 不强制要求特定的文件夹布局;你可以在其中声明资源。 pubspec.yaml 然后从代码中引用它们。图像通常以以下方式加载: Image.asset()从而根据以下条件解析到正确的密度桶: devicePixelRatio逻辑像素的作用与……相同 dp 在安卓系统上,物理像素密度被抽象化了。
对于自定义字体,Compose 允许您打包字体资源,或者在运行时通过 Google Fonts 等提供商获取字体资源,然后将其连接到…… FontFamily 以及排版。 Flutter 也采用了几乎相同的模式:将字体文件放在 assets 文件夹中,并在其中列出它们。 pubspec.yaml然后按名称引用字体系列。 TextStyle如果您需要运行时获取的字体,这里有一个常用的方法。 google_fonts 插件,用于公开以字体命名的 Dart 函数——例如 GoogleFonts.robotoTextTheme()—快速将它们集成到您的主题中。
这两个生态系统都将字符串和本地化视为首要考虑因素,尽管 Flutter 没有与 Android 的 XML 字符串资源直接对应的功能。 相反,最佳实践是将翻译保留在…… .arb 文件并将其与 Flutter 本地化工具链连接起来。然后通过生成的 Dart 类进行访问,这与使用类似。 R.string Android 代码中的标识符。
通过 Flutter 视角看 Android 平台概念
除了 UI 之外,Compose 开发人员面临的最大问题之一是,他们的 Android 知识如何映射到 Flutter 的架构。 幸运的是,许多核心概念——活动、生命周期、意图、后台工作、资源、网络——都有清晰的对应物,即使表面 API 看起来有所不同。
在安卓系统中, Activity 与 Fragment 是你的主要屏幕和容器;在 Flutter 中,一切皆为组件,导航通过它们进行。 Navigator 与 Route 对象。 路由大致对应于一个活动或片段,但通常只有一个托管服务器。 Activity 在嵌入了 Flutter 引擎的 Android 设备上,您可以通过在 Navigator 堆栈中定义的命名路由来推送和弹出路由。 MaterialApp 或通过直接建造 PageRoute 例如 MaterialPageRoute.
Android 的生命周期回调(onCreate, onStart, onResume等等)在 Flutter 代码中没有一对一的钩子,但您可以使用它们来观察应用程序的生命周期。 WidgetsBindingObserver. 它暴露了诸如此类的状态 resumed, inactive, paused 与 detached这大致对应于 Android 的可见、后台和销毁阶段。当确实需要用于资源管理的底层生命周期钩子时,通常会在原生 Android 端实现它们。 FlutterActivity 或者是一个插件,但不是 Dart 语言编写的。
Intent 在 Android 系统中扮演两个角色:应用内导航和跨应用通信。 如前所述,Flutter 没有基于 intent 的导航 API——Navigator 在 Dart 世界中完全取代了它。对于跨应用任务(例如启动相机、文件选择器、处理分享 intent),通常需要使用封装了必要 Android(和 iOS)调用的插件。如果没有现成的插件,您可以使用 MethodChannels 编写自己的插件,在 Dart 和原生代码之间进行通信,并将 intent 和结果作为消息转发。
你对后台工作和线程的理解也会迁移过来,但基本原理看起来有所不同。 Android 鼓励你使用协程、AsyncTask(旧版)、WorkManager、JobScheduler、RxJava 等工具将网络和磁盘 I/O 操作移出主线程。相比之下,Dart 为每个隔离区使用单线程事件循环,使用 async/await 处理 I/O 操作,并为 CPU 密集型任务创建单独的隔离区。对于任何 I/O 密集型操作,只需标记你的函数即可。 async, await 操作并让事件循环保持 UI 的响应性;对于 CPU 密集型任务,您可以启动一个隔离区并通过消息传递而不是共享内存进行通信。
在网络方面,Flutter 的流行之处在于 http 该软件包在基本用例中扮演着类似于 OkHttp + Retrofit 的角色。 它隐藏了许多底层套接字操作,并与 async/await 自然集成。对于更复杂的需求,您可以升级到类似这样的软件包。 dio但基本模式依然不变:发起异步调用,等待结果,然后用……更新状态 setState() 或者使用您选择的状态管理器,并重建受影响的小部件。
插件、存储、Firebase 和工具
在 Android 开发中,你习惯于在 Gradle 中声明依赖项;在 Flutter 中,你需要在 [此处应填写 Flutter 的文件名] 中声明它们。 pubspec.yaml 并从 pub.dev 获取它们。 Gradle 文件位于 android/ Flutter 项目的文件夹主要用于平台特定的集成,或者当你需要自定义原生库时——日常应用程序开发仍然在 Dart 领域进行。
Shared preferences 和 SQLite 也有现成的等效项。 Android 提供 SharedPreferences Flutter 通过插件封装了小型键值存储和 SQLite(或 Room)等存储设备,用于存储结构化数据。 shared_preferences 与 sqflite这些插件统一了 Android 和 iOS 的行为,因此无论平台如何,您都可以使用单个 Dart API,同时仍然依赖于底层原生实现。
Firebase 集成同样简单易用,而且非常出色。 大多数 Firebase 服务——包括身份验证、Firestore、实时数据库、云消息传递、分析、远程配置等等——都有由 Firebase 和 Flutter 团队维护的官方 Flutter 插件。这些插件借鉴了 Android Firebase SDK 的概念模型,但采用了 Dart 惯用的 API。对于 Firebase 未直接涵盖的更细分的功能,pub.dev 上也提供了丰富的第三方插件生态系统。
对于调试和性能分析,Flutter 的 DevTools 套件提供了一个功能丰富的工具箱,可以直接与 Android Studio 的性能分析器和布局检查器相媲美。 您可以检查组件树、跟踪重新构建、监控内存分配、诊断内存泄漏和碎片化问题,并单步执行 Dart 代码。结合 Android Studio 和 VS Code 中的 IDE 支持、热重载和热重启功能,Flutter 开发的反馈周期至少与您使用 Compose 时一样紧密,甚至通常更紧密。
推送通知是 Android 系统的另一个常见问题,在 Flutter 中,推送通知是通过插件来处理的,例如 firebase_messaging. 底层,这些组件会与 Firebase 云消息传递以及 Android 和 iOS 的原生通知框架进行通信,但您的应用逻辑运行在一个统一的 Dart API 中。配置和平台特定的行为(例如 Android 上的通知渠道)仍然非常重要,您之前在这些平台细节方面的经验仍然具有很高的参考价值。
即使是无法完全用 Flutter 实现的 Android 主屏幕小部件,仍然可以与 Flutter 代码集成。 通常情况下,您可以使用 Jetpack Glance 或 XML 布局来构建它们,然后使用诸如此类的软件包。 home_widget 为了与 Flutter 应用通信、共享数据,甚至将栅格化的 Flutter UI 作为图像嵌入到原生组件中,这种混合方法让您能够在遵守平台限制的同时,保持 Flutter 的核心体验。
纵观所有这些相似之处,Jetpack Compose 开发人员转而学习 Flutter 并非从零开始。 你对声明式 UI、Android 生命周期、导航、状态、资源和异步工作的理解可以非常自然地应用到 Flutter 的世界中;最大的变化在于名称、语言(Dart)和跨平台思维。一旦你理解了 widgets 和 Navigator 这些基础概念,其余部分就会很快上手。