1. 项目概述折叠屏适配的挑战与机遇最近在做一个Flutter项目产品经理突然跑过来说“咱们的应用要上折叠屏设备了你看怎么适配一下” 我当时心里咯噔一下这可不是简单的屏幕旋转或者多分辨率适配。折叠屏设备无论是像三星Galaxy Z Fold系列那样的书本式折叠还是像Z Flip那样的翻盖式都给应用开发带来了全新的交互范式。它不再是单一、固定的屏幕而是一个可以根据用户物理操作在“手机”和“平板”形态间动态切换的复合显示系统。对于Flutter开发者而言这意味着我们需要重新思考布局逻辑。传统的响应式设计通常基于屏幕宽度MediaQuery.of(context).size.width的断点breakpoints来切换布局。但在折叠屏上情况复杂得多。设备可能处于展开大屏、折叠主屏外屏、折叠副屏内屏等不同状态而且用户可以在应用运行时随时折叠或展开设备这就要求我们的UI能够动态热切换而不是仅仅在应用启动时判断一次。核心要解决的问题有几个如何准确获取当前设备的折叠状态和屏幕信息如何设计布局使其既能充分利用展开后的大屏空间又能在折叠状态下保持良好的可用性如何在状态切换时实现流畅的过渡而不是生硬的布局跳转或重建这不仅仅是“拉伸布局”那么简单它涉及到X轴自适应适配甚至在某些情况下需要对组件进行布局重构。接下来我就结合最近的实际踩坑经验详细拆解一下Flutter在Android折叠屏上的适配方案。2. 核心概念与平台通道建立在动手写代码之前我们必须先理解Android系统为折叠屏提供的核心概念并打通Flutter与原生Android之间的通信桥梁。2.1 理解Android的折叠屏APIWindowManager与Jetpack WindowManager在Android原生开发中折叠屏信息主要通过WindowManager获取。在早期开发者需要直接使用WindowManager的getDefaultDisplay()等方法来计算但这种方式比较原始且易出错。Google后来推出了Jetpack WindowManager库它提供了更高级、更易用的API特别是WindowMetricsCalculator和FoldingFeature。WindowMetrics: 它描述了应用窗口的边界和状态比单纯的屏幕分辨率更准确因为它考虑到了系统UI如状态栏、导航栏的占用。FoldingFeature: 这是描述折叠硬件特征的核心类。它包含了折叠的位置bounds、方向orientation是水平折叠还是垂直折叠、以及状态state是FLAT平坦展开还是HALF_OPENED半开对于某些设备。我们的目标就是在Flutter侧能实时获取到这些信息。由于Flutter本身没有内置这些API我们必须通过平台通道来调用原生代码。2.2 创建Flutter平台通道平台通道是Flutter与宿主平台Android/iOS进行异步通信的机制。我们需要在Dart侧定义一个MethodChannel并在Android原生侧实现对应的处理逻辑。首先在Flutter的Dart代码中通常在主页或一个全局的服务类里我们创建通道并定义方法import ‘package:flutter/services.dart‘; class FoldableInfo { final bool isTabletop; // 是否处于展开状态桌面模式 final double hingeAngle; // 铰链角度如果设备支持 final Rect displayFeatures; // 折叠区域信息简化表示 FoldableInfo({required this.isTabletop, this.hingeAngle 0.0, required this.displayFeatures}); } class DeviceFoldableService { static const MethodChannel _channel MethodChannel(‘com.yourcompany.app/device_foldable‘); // 获取当前折叠状态 static FutureFoldableInfo getFoldableInfo() async { try { final Mapdynamic, dynamic? info await _channel.invokeMethod(‘getFoldableInfo‘); if (info ! null) { return FoldableInfo( isTabletop: info[‘isTabletop‘] ?? false, hingeAngle: (info[‘hingeAngle‘] ?? 0.0).toDouble(), displayFeatures: _parseRect(info[‘displayFeatures‘]), ); } return FoldableInfo(isTabletop: false, displayFeatures: Rect.zero); } on PlatformException catch (e) { print(“获取折叠信息失败: ${e.message}“); return FoldableInfo(isTabletop: false, displayFeatures: Rect.zero); } } // 监听折叠状态变化 static Futurevoid listenToFoldableChanges(void Function(FoldableInfo) callback) async { // 这里通常需要与EventChannel配合为了简化我们先使用轮询或原生侧回调的方式。 // 更优雅的方式是使用EventChannel让原生侧在状态变化时主动推送事件。 _channel.setMethodCallHandler((call) async { if (call.method ‘onFoldableChange‘) { final info call.arguments; callback(FoldableInfo( isTabletop: info[‘isTabletop‘] ?? false, hingeAngle: (info[‘hingeAngle‘] ?? 0.0).toDouble(), displayFeatures: _parseRect(info[‘displayFeatures‘]), )); } return null; }); } }注意这里我们定义了一个FoldableInfo数据类来封装信息。在实际项目中你可能需要更详细的信息比如多个FoldingFeature的列表。listenToFoldableChanges方法展示了一种通过MethodChannel接收回调的思路但生产环境更推荐使用EventChannel来实现真正的持续事件流。2.3 实现Android原生侧逻辑接下来我们需要在Android项目android/app/src/main/kotlin(or java)/.../MainActivity.kt中实现通道的响应逻辑。这里以Kotlin为例并引入Jetpack WindowManager库。首先在app/build.gradle中添加依赖dependencies { implementation “androidx.window:window:1.0.0“ // 请使用最新稳定版本 // ... 其他依赖 }然后在MainActivity中实现import androidx.window.layout.WindowLayoutInfo import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.FoldingFeature import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterActivity() { private val CHANNEL “com.yourcompany.app/device_foldable“ private lateinit var channel: MethodChannel // 用于监听布局变化的回调 private val layoutStateCallback { newLayoutInfo: WindowLayoutInfo - // 当窗口布局信息变化时通知Flutter val arguments parseWindowLayoutInfo(newLayoutInfo) channel.invokeMethod(“onFoldableChange“, arguments) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) channel MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) channel.setMethodCallHandler { call, result - when (call.method) { “getFoldableInfo“ - { val info getCurrentWindowLayoutInfo() result.success(info) } else - result.notImplemented() } } // 注册窗口布局信息监听器 // 注意需要在合适的生命周期内注册和注销这里是一个简单示例 androidx.window.layout.WindowInfoTracker.getOrCreate(this) .windowLayoutInfo(this) .observe(this) { newLayoutInfo - layoutStateCallback(newLayoutInfo) } } private fun getCurrentWindowLayoutInfo(): MapString, Any { val windowMetrics WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this) val currentBounds windowMetrics.bounds val foldingFeatures mutableListOfMapString, Any() // 这里简化处理实际应从WindowLayoutInfo获取 // 假设我们通过一个同步方法获取实际应使用上面注册的观察者获取的最新值 val layoutInfo androidx.window.layout.WindowInfoTracker.getOrCreate(this) .windowLayoutInfo(this).value // 注意这是简化可能为空或非最新 layoutInfo?.displayFeatures?.filterIsInstanceFoldingFeature()?.forEach { feature - foldingFeatures.add(mapOf( “bounds“ to mapOf(“left“ to feature.bounds.left, “top“ to feature.bounds.top, “right“ to feature.bounds.right, “bottom“ to feature.bounds.bottom), “state“ to feature.state.toString(), “orientation“ to feature.orientation.toString(), “isSeparating“ to feature.isSeparating )) } val isTabletop foldingFeatures.any { it[“isSeparating“] false it[“state“] “FLAT“ } // 简化判断逻辑 return mapOf( “windowBounds“ to mapOf(“width“ to currentBounds.width(), “height“ to currentBounds.height()), “isTabletop“ to isTabletop, “foldingFeatures“ to foldingFeatures, “hingeAngle“ to 0 // 铰链角度需要设备特定API这里暂设为0 ) } // 解析WindowLayoutInfo为Map用于回调 private fun parseWindowLayoutInfo(layoutInfo: WindowLayoutInfo): MapString, Any { // 解析逻辑与getCurrentWindowLayoutInfo类似 // ... return mapOf(“isTabletop“ to calculatedValue) } }实操心得在实现原生侧代码时最大的坑在于生命周期管理。WindowInfoTracker的观察者必须在Activity处于前台时注册并在后台时注销否则可能导致内存泄漏或无效回调。通常我会在onStart中注册在onStop中移除观察。上面的示例为了简洁写在了configureFlutterEngine中在实际项目中需要根据生命周期调整。3. Flutter侧布局适配策略拿到了折叠屏的状态信息后接下来就是如何在Flutter UI层面进行适配。核心思想是根据屏幕状态和窗口信息动态决定布局结构。3.1 状态管理与数据流设计我们不能在每一个需要适配的Widget里都去调用平台通道获取信息这既不高效也难以维护。正确的做法是建立一个全局的、可观察的状态管理模型。这里以Provider为例创建一个DeviceFoldableModelimport ‘package:flutter/material.dart‘; class DeviceFoldableModel extends ChangeNotifier { FoldableInfo _info FoldableInfo(isTabletop: false, displayFeatures: Rect.zero); FoldableInfo get info _info; bool get isTabletopMode _info.isTabletop; // 可以添加更多衍生getter如屏幕宽度分类 ScreenType get screenType { final width MediaQuery.of(/* 需要传入context或存储窗口宽度 */).size.width; if (isTabletopMode) { return ScreenType.expanded; } else { if (width 600) { // 这是一个示例断点可根据实际设备调整 return ScreenType.medium; } else { return ScreenType.compact; } } } void updateInfo(FoldableInfo newInfo) { if (_info.isTabletop ! newInfo.isTabletop || _info.hingeAngle ! newInfo.hingeAngle) { _info newInfo; notifyListeners(); // 通知所有监听者重建 } } } enum ScreenType { compact, medium, expanded }然后在应用的根Widget如MaterialApp之上提供这个Model并在initState中开始监听折叠状态变化void main() { runApp( ChangeNotifierProvider( create: (context) DeviceFoldableModel(), child: MyApp(), ), ); } class MyApp extends StatefulWidget { override _MyAppState createState() _MyAppState(); } class _MyAppState extends StateMyApp { override void initState() { super.initState(); // 开始监听折叠状态变化 DeviceFoldableService.listenToFoldableChanges((info) { final model Provider.ofDeviceFoldableModel(context, listen: false); model.updateInfo(info); }); // 初始化获取一次状态 WidgetsBinding.instance.addPostFrameCallback((_) async { final info await DeviceFoldableService.getFoldableInfo(); final model Provider.ofDeviceFoldableModel(context, listen: false); model.updateInfo(info); }); } // ... }这样任何需要根据折叠状态调整布局的Widget只需要ConsumerDeviceFoldableModel即可获取最新状态并重建。3.2 基于断点与状态的混合布局策略纯粹的宽度断点如width 600在折叠屏上可能失效因为折叠时的主屏宽度可能和展开时某个区域的宽度接近。因此我们需要混合策略首先判断物理设备状态是否展开再结合窗口的实际逻辑宽度。我常用的一个AdaptiveLayoutBuilder组件模式如下class AdaptiveScaffold extends StatelessWidget { override Widget build(BuildContext context) { return ConsumerDeviceFoldableModel( builder: (context, model, child) { final screenType model.screenType; final isTabletop model.isTabletopMode; final mediaQuery MediaQuery.of(context); final screenWidth mediaQuery.size.width; // 策略决策层 if (isTabletop) { // **展开大屏模式**采用平板/桌面布局 return _buildTabletopLayout(context, screenWidth); } else { // **折叠模式**采用手机布局但可能根据折叠方向铰链位置微调 // 这里可以获取折叠特征信息判断是左右折叠还是上下折叠 final hingeArea model.info.displayFeatures; if (hingeArea.width 0 hingeArea.left 0) { // 如果铰链区域在屏幕中间左右折叠可能需要特殊处理 return _buildFoldablePhoneLayout(context, screenWidth, hingeArea); } else { // 普通手机布局 return _buildCompactPhoneLayout(context, screenWidth); } } }, ); } Widget _buildTabletopLayout(BuildContext context, double width) { // 典型的大屏布局导航栏在左侧内容区在右侧或者采用多栏设计 return Scaffold( body: Row( children: [ // 固定宽度的导航抽屉 SizedBox( width: 280, // 适合手指点击的宽度 child: NavigationPanel(), ), // 可伸缩的内容区域 Expanded( child: ContentDetailPanel(), ), // 如果屏幕足够宽甚至可以添加第三个面板 if (width 1000) SizedBox( width: 300, child: SidebarPanel(), ), ], ), ); } Widget _buildCompactPhoneLayout(BuildContext context, double width) { // 经典的手机单栏布局底部导航 return Scaffold( appBar: AppBar(title: Text(‘首页‘)), body: HomeContent(), bottomNavigationBar: BottomNavigationBar(...), ); } Widget _buildFoldablePhoneLayout(BuildContext context, double width, Rect hingeArea) { // **针对折叠屏手机的特殊布局**。 // 例如当设备半折叠呈“帐篷模式”时内容可能需要避开铰链区域或上下分屏显示。 // 这里假设铰链将屏幕分为左右两部分 return Scaffold( body: Row( children: [ // 左半屏 Expanded( child: Container( color: Colors.grey[200], child: LeftPanel(), ), ), // 铰链视觉指示器可选可以是一个窄条 Container( width: hingeArea.width.toDouble(), color: Colors.black12, child: Center(child: Icon(Icons.drag_handle)), ), // 右半屏 Expanded( child: Container( color: Colors.grey[300], child: RightPanel(), ), ), ], ), ); } }注意事项_buildFoldablePhoneLayout中的铰链区域处理是一个高级话题。有些应用会选择将内容跨铰链显示以创造独特的连续视觉效果如图库应用这时就需要确保关键交互元素和文字不要被铰链遮挡。而有些应用则选择将铰链视为物理分隔在两侧显示关联但独立的内容如邮件列表和阅读。你的选择应取决于应用的核心交互。4. 动态热切换与状态保持折叠屏最酷也最具挑战的特性是动态热切换。用户可能在浏览文章时展开设备期望立即看到更丰富的排版或多窗内容也可能在编辑文档时折叠设备希望应用能智能地切换到更适合单手操作的视图。这个过程必须流畅不能丢失当前状态如滚动位置、输入文本、选中项。4.1 利用Flutter的响应式框架幸运的是Flutter的响应式框架天生适合这种场景。当折叠状态改变导致DeviceFoldableModel通知监听者时依赖该Model的Widget会重建。关键在于我们要最小化重建范围并保持子组件的状态。Consumer的精细使用不要在整个页面根节点使用Consumer而应该在布局发生变化的特定组件上使用。例如只有主体内容区域需要从单栏变为双栏那么只在这个区域包裹Consumer。Key的运用对于需要保持状态的Widget如PageView、TextField、带有滚动控制的ListView为其赋予一个基于内容的Key或GlobalKey可以防止在父Widget重建时被意外重置。// 在状态管理模型中维护一个全局的“页面状态”Key final GlobalKeyNavigatorState navigatorKey GlobalKey(); // 或者在特定页面保持滚动位置 final MapString, ScrollController scrollControllers {}; // 在布局切换时判断如果只是布局结构调整而内容组件类型不变则复用旧组件。 Widget _buildBody(BuildContext context, ScreenType oldType, ScreenType newType) { // 如果都是从“紧凑”切换到“中等”且只是布局方式变化内容组件相同Flutter的Element树会尝试复用。 // 为了更精确控制可以使用ValueKey或GlobalKey。 if (newType ScreenType.expanded oldType ScreenType.compact) { // 从手机布局切换到平板布局可能需要完全不同的组件树状态可能无法自动保持。 // 此时需要手动将关键状态如选中的ID、滚动偏移量保存到Model中并在新布局中恢复。 } }4.2 页面路由与导航栈的适配在展开大屏下我们可能采用单Activity/Fragment多Widget的导航模式例如使用Navigator 2.0Router来管理一个包含列表页和详情页的并行视图。而在折叠小屏下则使用经典的栈式推送导航。这需要我们的导航逻辑也是自适应的。一个可行的方案是在根Widget中根据屏幕类型选择不同的RouterDelegate或导航策略。class AdaptiveRouterDelegate extends RouterDelegateYourRoutePath with ChangeNotifier, PopNavigatorRouterDelegateMixinYourRoutePath { final DeviceFoldableModel foldableModel; AdaptiveRouterDelegate(this.foldableModel) { foldableModel.addListener(notifyListeners); // 折叠状态变化时重新配置路由 } override Widget build(BuildContext context) { if (foldableModel.screenType ScreenType.expanded) { // 大屏布局可能是一个包含左右两个Navigator的Row return Row( children: [ Navigator( key: _leftNavigatorKey, pages: _buildLeftPages(), onPopPage: ..., ), Expanded( child: Navigator( key: _rightNavigatorKey, pages: _buildRightPages(), onPopPage: ..., ), ), ], ); } else { // 小屏布局单一的全局Navigator栈 return Navigator( key: _rootNavigatorKey, pages: _buildRootPages(), onPopPage: ..., ); } } // 根据当前路径和屏幕类型构建不同的页面栈 ListPage _buildLeftPages() { ... } ListPage _buildRightPages() { ... } ListPage _buildRootPages() { ... } }踩坑实录在动态切换导航模式时最头疼的是路由状态的迁移。比如用户在大屏下左列表选中项A右侧显示详情A。此时折叠设备应用应切换到手机模式并自动将详情A页面推送到导航栈顶。这要求你的路由状态当前路径、参数必须与UI布局解耦存储在一个统一的、与屏幕形态无关的状态管理中如Provider、Bloc、Riverpod这样在布局重建时才能正确恢复用户意图。5. 具体组件适配与布局技巧掌握了整体架构和状态管理后我们来看看具体组件该如何适配。5.1 列表与网格的X轴自适应这是最常见的需求。在手机上我们通常使用垂直的单列列表。在展开的大屏上我们希望充分利用宽度显示多列网格或并排列表。不要写死crossAxisCount使用LayoutBuilder或Consumer动态计算。ConsumerDeviceFoldableModel( builder: (context, model, child) { final screenType model.screenType; int crossAxisCount; double childAspectRatio; switch (screenType) { case ScreenType.expanded: crossAxisCount 4; // 大屏显示4列 childAspectRatio 1.0; // 正方形 break; case ScreenType.medium: crossAxisCount 3; // 中等宽度如7寸平板或折叠屏半开显示3列 childAspectRatio 0.9; break; case ScreenType.compact: default: crossAxisCount 2; // 手机显示2列或单列列表 childAspectRatio 0.8; break; } // 更精细的控制直接根据逻辑像素宽度计算 final width MediaQuery.of(context).size.width; final itemWidth 180.0; // 你期望的每个项目最小宽度 crossAxisCount (width / itemWidth).floor().clamp(1, 4); // 确保至少1列最多4列 return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: childAspectRatio, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemBuilder: (context, index) YourGridItem(), ); }, )5.2 表单与输入区域的优化在大屏上表单可以并排显示标签和输入框甚至多列布局。在小屏上则需要垂直堆叠。Widget buildAdaptiveFormField(String label, Widget inputField) { return ConsumerDeviceFoldableModel( builder: (context, model, _) { final isWide model.screenType.index ScreenType.medium.index; if (isWide) { // 水平布局标签和输入框在一行 return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 120, // 固定标签宽度便于对齐 child: Padding( padding: const EdgeInsets.only(top: 16.0), child: Text(label, style: Theme.of(context).textTheme.bodyLarge), ), ), SizedBox(width: 16), Expanded(child: inputField), // 输入框占据剩余空间 ], ); } else { // 垂直布局 return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context).textTheme.bodyLarge), SizedBox(height: 8), inputField, ], ); } }, ); }5.3 导航组件抽屉、底部导航栏的切换在手机模式我们常用BottomNavigationBar。在平板模式NavigationRail或永久性Drawer更合适。我们可以根据屏幕宽度动态切换Scaffold( appBar: screenType ScreenType.compact ? AppBar(...) : null, // 大屏可能不需要AppBar drawer: screenType ScreenType.compact ? const AdaptiveDrawer() : null, // 手机模式用临时抽屉 body: Row( children: [ // 大屏模式显示永久性导航栏 if (screenType.index ScreenType.medium.index) SizedBox( width: 72, // NavigationRail的典型宽度 child: NavigationRail( selectedIndex: _selectedIndex, onDestinationSelected: (index) {...}, destinations: [...], ), ), // 主体内容 Expanded( child: _buildBodyForIndex(_selectedIndex), ), ], ), // 手机模式显示底部导航栏 bottomNavigationBar: screenType ScreenType.compact ? BottomNavigationBar(...) : null, );6. 测试、调试与常见问题折叠屏适配的测试至关重要但大多数开发者没有真机。以下是一些实用方法。6.1 使用Android模拟器进行测试Android Studio的模拟器提供了强大的折叠屏模拟功能。创建折叠屏虚拟设备在AVD Manager中选择带有折叠屏特性的设备模板如“Pixel Fold”或“Samsung Galaxy Z Fold”。模拟折叠动作在模拟器侧边栏找到“折叠”控制按钮。你可以模拟设备完全展开、完全折叠以及半开帐篷模式等状态。测试布局切换运行你的Flutter应用在模拟器上触发折叠/展开动作观察UI是否平滑过渡状态是否保持布局是否正确。6.2 在代码中模拟折叠状态为了在没有折叠屏设备或模拟器的情况下进行开发和单元测试可以在DeviceFoldableModel中提供一个“模拟模式”的开关。class DeviceFoldableModel extends ChangeNotifier { FoldableInfo _info ...; bool _isMockMode false; ScreenType _mockScreenType ScreenType.compact; ScreenType get screenType { if (_isMockMode) return _mockScreenType; // ... 原有的逻辑计算 } void setMockMode({required bool enabled, ScreenType? mockType}) { _isMockMode enabled; if (mockType ! null) _mockScreenType mockType; notifyListeners(); } }在开发时你可以在UI上添加一个调试面板快速切换模拟的屏幕类型从而立即看到布局变化。6.3 常见问题与排查技巧布局切换时UI闪烁或跳动原因可能是Consumer包裹的范围太大导致整个页面重建。也可能是布局计算如crossAxisCount在重建前后有微小差异。排查使用Flutter Performance面板查看重建的Widget数量。尝试缩小Consumer的作用域或将计算出的布局参数如列数缓存起来避免每次重建都重新计算。折叠状态监听不触发或延迟原因Android原生侧的WindowInfoTracker观察者可能没有正确注册或者生命周期管理有问题。也可能是平台通道通信延迟。排查首先在Android原生代码中加Log确认WindowLayoutInfo的回调是否被触发。然后在Flutter侧检查MethodChannel调用是否成功。确保监听是在initState或didChangeDependencies中启动的。铰链区域内容显示异常原因FoldingFeature的bounds获取不准确或者你的UI组件没有正确处理这个区域。排查将获取到的铰链区域坐标用Container(color: Colors.red.withOpacity(0.5))在UI上可视化出来看它是否与物理铰链位置匹配。确保可交互的按钮、文字输入框等避开这个区域。热重载/重启后状态丢失原因折叠状态是动态的但你的应用初始状态可能基于一个默认值如false。应用重启后需要异步从原生平台获取状态这中间有个时间差。解决在UI初始构建时使用一个FutureBuilder或StreamBuilder来等待折叠状态信息加载完成在这期间显示一个加载中或默认布局。或者在DeviceFoldableModel初始化时同步调用一次getFoldableInfo注意处理异步。不同厂商设备行为不一致原因虽然Android有标准API但不同厂商三星、华为、小米等的折叠屏实现可能有细微差别特别是铰链角度、折叠状态的定义。建议尽可能使用Jetpack WindowManager库它由Google维护旨在统一接口。如果条件允许在主流折叠屏真机上进行测试。对于关键功能考虑做一点厂商特定的兼容性处理通过Build.MANUFACTURER判断但这是最后的手段。折叠屏适配是一个从“平台信息获取”到“状态管理”再到“UI动态响应”的完整链条。它考验的不仅是Flutter UI编写能力更是对应用架构和状态流转的理解。