# flutter-interview **Repository Path**: lin-ruiqiang/flutter-interview ## Basic Information - **Project Name**: flutter-interview - **Description**: flutter 版本 interview - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-30 - **Last Updated**: 2025-01-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Flutter-Interview ## QuestionList - TabBarView + AutomaticKeepAliveClientMixin 的结合: - TabBarView 仍然是原生的切换逻辑,保留了滑动交互的体验。 - 在 QuestionList 中使用 AutomaticKeepAliveClientMixin,确保每个页面只初始化一次,并在切换时保持其状态。 - 这样既避免了重复初始化组件,也确保了组件状态的持久性。 - 分页加载逻辑: - 使用了 _questionList 来保存已加载的数据,并通过 _currentPage 和 _pageTotal 控制分页逻辑。 - 在 _onRefresh 和 _onLoading 中调用 fetchQuestions,实现了下拉刷新和上拉加载更多的功能。 - 自动触发首次加载: - 在 initState 中通过 WidgetsBinding.instance.addPostFrameCallback 确保第一次加载在页面初始化完成后触发。 - 页面重用与性能优化: - ListView.builder 和分页的结合确保了大数据列表的渲染性能。 - SmartRefresher 用于提供下拉刷新和加载更多的体验,封装了控件的交互逻辑。 | 特性 | Flutter (AutomaticKeepAliveClientMixin) | Vue (keep-alive) | | ------------ | --------------------------------------- | ---------------------------------- | | 缓存粒度 | 页面级别,每个 StatefulWidget 独立缓存 | 组件级别 | | 实现方式 | 使用 Mixin 来告知框架保留 State | 指令 管理缓存 | | 生命周期管理 | 页面的生命周期完全由开发者控制 | 提供 activated 和 deactivated 钩子 | | 使用场景 | TabBarView 等需要频繁切换的场景 | 动态组件切换和性能优化场景 | | 性能消耗 | 较低,State 的持久性直接由框架控制 | 缓存的虚拟 DOM 占用更多内存 | ## PageView + WebView 的手势冲突 ### 1. GestureDetector 与 PageView 手势冲突 - **GestureDetector 的行为:** - `GestureDetector` 会直接 拦截手势事件,这意味着它会优先处理用户的手势,而不是将手势事件传递给其他组件。 - 在 PageView 默认的手势切换逻辑中,如果手势被 `GestureDetector` 拦截,那么 `PageView` 的滑动逻辑就不会被触发。 - 同样地,当 `PageView` 自己处理了手势事件时,会优先触发它的翻页操作,而不是把手势传递到 `WebView。` - **手势冲突的本质:** `GestureDetector` 的拦截特性加上 `PageView` 的内部手势监听机制,导致 `WebView` 的手势被“抢占”,无法生效。 ### 2. 使用 Listener 的优势 - `Listener` 的特点: - `Listener` 不拦截手势事件,而是监听原始的手势事件(如 `PointerDown` 、 `PointerMove` 、 `PointerUp` )。 - 它不像 GestureDetector 那样会打断事件传递链,因此可以让 `PageView` 和 `WebView` 都有机会处理事件。 - 结合 `PageView` 的方案: - 禁用 `PageView` 的默认切换事件(通过 physics: NeverScrollableScrollPhysics()),确保 `PageView` 不会“抢占”手势。 - 通过 `Listener` 来监听用户的滑动方向(水平还是垂直),根据滑动方向手动触发 `PageView` 翻页或将事件交给 WebView。 ### 3. 改造的逻辑分析 - 最终的改造逻辑是: - 禁用 `PageView` 的默认滑动切换行为。 - 使用 `Listener` 来捕获手势事件,而不是 `GestureDetector` ,从而避免拦截。 - 根据手势方向,判断事件的处理方式: - 如果是水平滑动( `deltaX` 大于 `deltaY` ),手动触发 `PageView` 的翻页。 - 如果是垂直滑动( `deltaY` 大于 `deltaX` ),允许 `WebView` 自然处理滑动事件。 - 实现了一个统一的逻辑控制,避免 `PageView` 和 `WebView` 因手势冲突而影响用户体验。 ### 4. 优势总结 - 解决了冲突: - 通过禁用默认切换,手动监听手势实现了对手势事件的统一管理。 - 避免了 `GestureDetector` 对手势事件的强拦截,确保 `WebView` 可以正常响应垂直滑动。 - 扩展性强: - 使用 `Listener` 的方式更灵活,可以轻松拓展为处理其他类型的手势(如双指缩放等)。 ### 5. 代码优化的方向 - 手势判断逻辑:对于复杂的滑动场景(如 `WebView` 内部缩放),可以进一步细化方向判断逻辑。 - 性能优化:`Listener` 是一个较低级的组件,建议在监听逻辑中做更多的优化,避免误触。 自定义动画:当手动切换 `PageView` 时,可以加入一些切换动画效果(例如使用 PageController 的 animateToPage 方法)。 ### 一句话总结 `GestureDetector` 会直接进行拦截手势事件 而在`PageView`默认的切换事件时 `PageView`的控件会把手势事件拦截处理 从而导致`webview`的手势无法触发 要解决`PageView` + `WebView` 目前使用方案是 禁用默认的`Pageview`切换事件 使用 `Listener` 对整体进行监听手势事件 而不是进行拦截 `webview`的滑动和`pageview`的切换逻辑全部改用手写 这样就可以实现功能 ## 响应式设计 ### setState ### getx > 在 GetX 中,响应式系统 是通过 Rx 类型(如 RxInt、RxMap 等)来实现数据的自动更新的,但其工作机制与具体的 数据引用 和 响应式对象的通知规则 有紧密的关系。 #### 三个关键点 - 响应式数据的通知机制 - 直接修改 Rx 对象中的属性值 和 整体赋值更新的区别 - 复杂数据类型(如 RxMap 或对象数组)如何触发 GetX 的响应式更新 #### 数据通知机制 在 GetX 中,响应式系统的核心原理是通过 观察者模式 监听数据变化。当你修改了 Rx 包裹的数据时,GetX 会自动通知使用了这个数据的 UI 更新。 - Rx 对象 如 RxInt、RxMap、RxList 等,本身就是响应式的。 - 赋值 或者 调用 .refresh() 会触发观察者通知,使依赖于该数据的 UI 自动更新。 **特殊情况 RxMap** - RxMap 和引用型数据的行为 - RxMap 是响应式的,但当你 直接修改其内部对象的属性 时,GetX 不会自动检测到变化。 - 这是因为 GetX 只监控对 RxMap 整体引用 的修改,而不会深入监听 RxMap 中每个对象的属性变化。 - 内部对象的属性改变无法直接触发通知,必须手动调用 refresh() #### 复杂数据类型的响应式设计 - 对于复杂数据类型(如对象数组或 RxMap),GetX 的响应式系统有以下特点: - RxMap 和 RxList 本身是响应式的: - 对整个 RxMap 或 RxList 进行赋值,会触发 UI 更新。 - 示例: ```dart questionDetails['123'] = QuestionDetail(...); // 触发响应式更新 ``` - 修改内部对象的属性不会触发响应式更新: - GetX 默认不会递归监听 RxMap 或 RxList 内部对象的属性变更。 - 手动调用 .refresh(): - 当你修改 RxMap 内部对象的属性时,需要调用 refresh() 来触发整体更新。 - 示例: ```dart questionDetails['123']!.likeCount.value += 1; questionDetails.refresh(); // 强制触发响应式更新 ``` - 使用 RxList 的操作方法触发更新: - RxList 提供了一些操作方法(如 .assignAll、.remove 等)来直接触发更新。 - 最佳实践 - 将内部属性也使用 Rx 包裹: - 确保每个需要监听的属性都是 Rx 类型。 例如: ```dart class QuestionDetail { final RxInt likeCount; final RxInt likeFlag; QuestionDetail({required this.likeCount, required this.likeFlag}); } ``` 对于对象数组或 RxMap,使用手动 refresh(): 修改对象内部属性后,调用 refresh() 触发整体更新。 - 统一管理数据变更逻辑: **封装方法修改数据并调用 refresh(),避免在多个地方重复手动刷新。** ## 持久化存储 ### permanent: true(GetX 内存持久化实例) - Get.put(controller, permanent: true) 会将 Controller 或数据实例保存在内存中,且在应用生命周期内不会被销毁。 - 优点: - 速度最快:数据存储在内存中,读写速度极快,因为内存是访问延迟最小的存储介质 - 简单易用:无需额外配置,即可通过 Get.find() 访问同一实例。 - 适合状态管理:适用于全局数据、临时状态共享,如页面间通信、用户状态等。 - 缺点 - 如果使用频繁,可能造成内存泄漏,内存过大,影响页面性能等 - 占用内存:数据量大时会持续占用内存,无法自动释放,可能导致内存占用过高 - 非持久化:数据存储在内存中,应用重启或崩溃后数据会丢失。 - 生命周期受限:仅在应用运行期间有效。 - 场景 - 页面状态管理 - 用户会话、临时缓存。 - 数据量不大,且对持久化没有要求等需要频繁读取的状态 ### GetStorage (GetX 内置本地存储) ```dart await GetStorage.init("question_details_box"); final GetStorage box = GetStorage("user_box"); ``` - 优点: - 持久化存储:数据会存储到本地文件系统,应用重启后仍能保持数据。 - 轻量级:非常适合存储小型数据(键值对),类似于 SharedPreferences。 - 速度快:通过内存缓存加速访问,同时支持文件存储,性能较高。 - 缺点 - 不适合大数据:主要用于键值对存储,处理复杂数据结构时可能需要手动序列化。 - 依赖本地存储:虽然访问速度较快,但不如内存直接操作快。 ### SharedPreferences - SharedPreferences 是原生 Android/iOS 提供的本地键值对存储方式,Flutter 中通过 shared_preferences 插件封装使用。 - 优点 - 稳定性高:作为原生 API 封装,广泛被使用,成熟稳定。 - 持久化存储:数据存储到本地文件系统,应用重启后数据不会丢失。 - 简单轻量:适合存储简单的键值对数据,如布尔值、字符串、数字等。 - 缺点 - 性能较低:读取和写入速度比内存慢,尤其在数据量大时。 - 功能有限:不支持复杂数据结构存储,需要手动序列化和反序列化(如 JSON)。 - 平台依赖:依赖平台实现,可能会有平台特定的 bug。 | 指标 | permanent: true | GetStorage | SharedPreferences | | ------------ | -------------------- | ---------------------------- | ---------------------------- | | 速度 | ⭐⭐⭐⭐(最快,内存操作 | ⭐⭐⭐(本地缓存+文件) | ⭐⭐(文件存储,速度较慢) | | 持久化能力 | 无,数据重启后丢失 | 有,应用重启数据不丢失 | 有,应用重启数据不丢失 | | 存储容量 | 受限于内存 | 适合存储小数据(KB 级) | 适合存储小数据(KB 级) | | 适合数据类型 | 任何数据结构 | 键值对(支持简单和序列化数据 | 键值对(支持简单和序列化数据 | | 生命周期 | 应用运行期间 | 持久化到文件,随时可读取 | 持久化到文件,随时可读取 | | 依赖性 | 依赖 GetX | 依赖 GetX | 依赖平台和第三方插件 | | 内存占用 | 高,数据在内存中 | 中,依赖文件缓存 | 中,依赖文件缓存 | ### 总结分析 - 频繁读取、运行时状态共享 permanent: true 内存存储速度快,适合状态管理。 - 小型持久化数据存储 GetStorage 简单易用,支持本地文件持久化。 - 稳定的键值对存储 SharedPreferences 成熟稳定,但速度略低。 - 复杂对象/大数据存储 自定义存储方案 以上三种方案均不适合处理大规模数据。 ## 输入框+长列表布局 ### 场景: >`TextField`聚焦时弹出键盘,会导致`Expanded` 和 `SmartRefresher`布局错乱,错乱表现是 > - 键盘弹出导致布局变化 flutter布局会重新计算 > - SmartRefresher 的滚动监听受到键盘干扰 > - 输入框可能引发多次状态刷新 > - 输入框与列表在同一页面竞争焦点 > 原因是弹出键盘时flutter布局会重新计算 (尤其是 MediaQuery.of(context).viewInsets.bottom 的值发生变化),直接影响 SmartRefresher 的触底机制(底部位置计算发生偏移) ### 解决方案1 - 将`TextField` 和 `Expanded`+`SmartRefresher`拆分开页面,单独用一个`SearchResult`结果页去单独展示结果列表,避免这两个组件在一个页面展示,是最安全的解决方案。 - 鉴于项目的`controller`设计,可以把`controller`初始化的时机放在 `TextField`,`SearchResult`专注于结果展示和触发下拉刷新,上拉加载更多。 ### 解决方案2 - 降低触发搜索频率,将`onChange`事件的`value`记录下来,而不是直接对`value`进行搜索,保证有输入框时,不渲染`SmartRefresher` - 在触发`onSearch`事件前手动关闭键盘,这样最大程度上保证布局不会被键盘弹出所影响。 ## 性能优化 ### 渲染层面 #### 首屏渲染 - 在应用首页需要初始化请前唤起进度条弹框并使用单独的私有变量`_isloading`进行标记(默认是`true`,请求完成且客户端处理完成后变成`false`),在`build`函数进行判断,若`_isLoading`为true时,展示一个空容器(空容器`SizedBox.shrink()` 基本不占用任何渲染资源;也可以使用骨架屏,但就不需要进度条弹框,两种方案选一种),待客户端处理接口完成后,展示真正需要渲染的`Widget`组件。 - 首页一些需要展示的静态组件,(轮播图、banner等),在进入`Home` 前就开始使用预加载,不管是静态图还是网络图片,都最大程度上保证加载速度。 #### 长列表 - 如果有分类或者其他标记点,父级组件使用`TabBar+TabBarView`;反之使用`PageView`,列表组件统一用`SmartRefresher` ; - 首先数据在`Getx`的`Controller`进行维护(不需要`getxStorage`),如果是分类那么用`RxMap`(满足列表响应式),对每个分类的数据列表进行处理。每个`Map` 是由分类的唯一标识 + `State`维护,`State`包括`list`(需要渲染的数据源)、`isLoading`(是否在加载中的标记)、`isFinished`(当前分类是否加载完毕的标记)、`currentPage`(当前页)、`pageTotal`(总页数或总数量,最好要求服务端返回)。 - 长列表数据的请求方法有两个 `refresh`刷新和`loadMore`加载,区别:`loadMore`专注于加载更多,触发时在`isLoading`或`isFinished`为`true`时都需要`return`,避免触发过于频繁,之后发起网络请求前让`isLoading`为`true`,请求完成后`isLoading`为`false`;`refresh`专注于刷新,执行前先把`list`请空、`currentPage`改为0,`isFinished`改为false,之前再发起网络请求;每次网络请求发起后,都去把对应`State`的`list`补充上,判断当前页是否是最后一页,如果不是让当前页++,如果是让`isFinished`变成`true` - 渲染时,使用`AutomaticKeepAliveClientMixin` 缓存页面状态,初始化时先去加载当前的`State`,然后渲染当前的`State`的`list`,遇到`isLoading`那么展示加载中的动画,同时处理`_refreshController`控制器,对加载完成、加载完毕,执行对应的方法,展示完全定制的`Header`和`Footer`。 #### 多图片、多视频 - 多图片场景: - 使用 CachedNetworkImage 实现图片的懒加载与缓存处理。 - 对大规模图片列表,研究使用 flutter_staggered_grid_view 等库以优化图片的布局和加载性能。 - 多视频场景: - 视频播放器推荐使用 video_player 或基于其封装的库(如 chewie),可以支持预加载、暂停、恢复等功能。 - 如果是长视频列表,可以结合 PreloadPageView 实现预加载,同时动态释放不需要的视频资源,避免内存占用过高。 #### 应用动画 - 统一应用的动画风格:页面路由动画,弹框、提示框的显示隐藏动画,删除列表项的动画 - 封装一个或多个通用的动画类,`content`参数对外暴露,可传入`Widget`, 将需要统一的风格全部统一 #### 应用反馈(loading、弹框等) - 可分为`flutter`原生和通过`Getx`衍生出的弹框,现主要讨论`flutter`原生。 - `loading`分为圆形、和进度条形有不同的使用场景。进度条用`OverlayEntry`搭建(有蒙层,展示时不允许点击其他区域),用于初进应用、初进某个页面时进行展示,短时间内不会再出现;圆形的`Loading`用`CupertinoActivityIndicator`搭建(也可以写`Lottie`动画),主要是`Widget`组件,是页面展示的一部分,主要用在加载列表、刷新页面时,可以点击其他任一区域。 - `ConFirm`弹框也进行封装,`Title` `Content` `ConfirmText` `CancalText`等都是可配置,按钮的点击回调,弹框的整体样式都可定制。 - `Toast`轻提示也进行统一封装,是否展示`Icon` `Message` `position` `duration`都对外进行暴露 - 各类按钮和按钮的点击、双击、长按、按下的反馈都封装到`RippleButton`,统一对应用所有的可点击`Widget`进行包裹,并对外暴露点击、双击、长按、按下事件 #### 应用刷新机制 - 应用中使用的场景是:`token`已经过期,用户仍请求了某些请求后,服务端返回状态码为401,该接口的数据未正常返回。 - 处理方式是将这些请求记录下来,重新登录后,再次请求。 - 还有一种方式是,通过`provider`或`getx`做一个刷新的标记,将其注入到`build`函数中,将这个标记的值变为`true`时,重新请求一次接口,注意一定要把接口单独写成一个函数。 #### WebView - 加载本地`html`,使用`evaluateJavascript`渲染接口返回的`js`代码,同时通过 `InAppWebView` 提供的回调监听 JS 执行结果 - 加载远程`url` - 混合开发`WebView`和`Flutter`应用通信,可以使用 WebView 提供的 JS Bridge(如 JavascriptChannel)实现通信。 ```dart // JS 发送消息给 Flutter window.flutter_inappwebview.callHandler('methodName', arg1, arg2); // Flutter 调用 JS controller.evaluateJavascript(source: "yourFunction()"); ``` ### 网络请求 #### 封装统一的`Http` - 请求拦截器、响应拦截器 - 特殊的状态码(401)、特殊的网络请求(无须校验token)...,都统一在`Http`类中做出封装,避免在业务代码中写 - 对于请求量大的接口(上传接口或下载大文件)时,如果后端支持`Content-Length` 那么在客户端的进度条展示,如果服务端没有该字段,客户端可以用`Steam`自行模拟。 #### 对报错进行统一处理 - 遇到和服务端约定好的状态码(401:token过期),在客户端展示的逻辑抽离到一个公共方法类中。 - 如果是用户登录凭证过期,那么重新登录后,把之前401的请求记录下来,用户重新登录后,立刻重新调用401的请求。 #### 多接口并发请求 - 使用 `Future.wait` 同时触发多个接口请求,也可以监听每个请求的状态 - 在并发请求中,使用错误捕获组(如 `try-catch` 或 `runZonedGuarded`)防止接口失败导致整体崩溃。 ### 数据模型 #### 数据响应式 - `Flutter`是单向数据流,不像`RN` `Uniapp` ,所以有些重新需要手动去实现一个数据响应式;场景是父子组件如何数据同步、祖先组件和后代组件的数据同步。比较难实现的是复杂的数据类型,如通过`RxMap`维护的长列表,里面的`List`中的某个对象里的`views`字段在子组件修改了,父组件没有更新。 - 实现的步骤需要在父组件和子组件的`controller`中分别都暴露出一个`updateViews`的方法,需要父子组件的`controller`中有一个独特的唯一标识如`typeId`,进行父子组件内修改`views`后同步到另一方。 #### 跨组件事件通信 - 两个组件、页面之间没有任何关系,完全是独立的,但是需要在一方修改了状态后,另一方需要更新或触发某方法。 - 目前用到的是`getx`,独特的标记`controller`,通过修改该标记的值,触发某方法。 - ... 可能也有事件总线的机制 #### 本地存储 - 将一些数据量大、更新频率不会非常高的组件如`PageView`依赖的数据源、搜索历史、动态添加的`tabbar`、用户信息、用户的独特配置(主题、语言)等用`GetxStoage`进行缓存,从而提高用户的体验。 - `GetxStoage`只能存储键值对,大小应当是`kb`,所以需要对存储的数据进行`json`化,如果是可修改、可删除、可添加等,还需要对`RxList`进行转义处理。 - 所有存储的列表类数据都做了条数限制(例如搜索历史累计20条后,会覆盖最先添加的数据) - `sqlLite`暂时未用到 ### 性能监测 - 对应用启动的各项指标进行标记,计算每个阶段的耗时(ms),以便开发时调试、优化。断点的阶段有入口文件——加载`config`——配置主题、语言——渲染第一帧等 - 如进入入口文件到`flutter` 框架初始化 。`mainStart to Flutter-Initialized: 133ms` - 还可以对每个页面、每个时刻的`gpu`等进行监测 ### 日志上报 - 封装统一的日志工具`Logger`,提供四个等级的日志,在输出时添加`Tag` `当前时间` `日志等级` 以及对四个等级进行颜色区分 。(`Info` `warn` `error` `debug` ) - 其中`Error`等级日志添加`stackTrace` ,提供上下文和详细调用路径。 - 开发环境不做多余处理,生成环境时,过滤掉所有的日志,除`Error`等级日志,全部过滤,并将`Error`等级日志全部强制上传到服务端。 - 一般的埋点调试都是对用户行为分析,如支付页面停留时间、哪些页面浏览时间长、哪些关键页面打开多少次,这些统计在客户端进行`GetxStoage`进行缓存,累计到一定条数(20)再进行上报服务端,同时这个方法也提供强制上报的逻辑,用来排查线上用户遇到问题,即使处理。 ## 框架对比 | 比较指标 | vue2/3(uniapp) | react(react-native) | flutter | | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 架构模式 | 单向数据流 | 单向数据流 | 单向数据流 | | 响应式原理 | vue2通过`Object.defineProperty`实现,拦截每一个对象,通过观察者模式,监听每个属性。vue3通过`proxy`监听所有的对象,深层次监听,无论是子改父还是父改子都会驱动UI更新 | 没有内建属性监听机制,依赖于状态更新,一般是父子组件通过自定义事件的传递修改状态来驱动UI更新。修改对象时,需要改变对象的引用,而不仅仅是内部属性。`setState(()=>{...data,k="key"})` | 通过`Widget`树构建UI,状态的变化触发 UI 重建,和react类似,不能仅修改对象的内部属性,要修改对象的引用,才会驱动UI更新。 | | 状态管理工具 | `vuex` `pinia` | `redux` `jotai` | `getx` | | 组件通信 | 自定义事件、自定义属性; | props传递; | 自定义事件传递; | | 架构设计 | 通过`vuex` 或`pinia` | 通过`redux` `jotai` | 使用`getx`建议每个数据模型的`controller`,统一数据控制层,所有用到该数据的页面都用同一套数据来统一管理,使用`Obx` `Rx` 实现响应式更新 | ### `react` / `flutter` #### 1. **React 的机制:状态驱动 UI** 在 React 中,UI 是由状态驱动的。无论状态如何发生变化,只要调用 `useState` 的 setter 方法(函数组件),React 会: 1. 将状态的改变标记为“脏”。 2. 根据新的状态重新渲染组件,生成新的虚拟 DOM。 3. 比较新旧虚拟 DOM(Diff 算法),只更新真正变化的部分到真实 DOM 中。 **具体流程:** - 父组件定义状态并将其传递给子组件,同时提供 `setState` 方法。 - 子组件通过调用传递下来的 `setState` 方法更新父组件的状态。 - 状态更新后,React 检测到状态变化,重新渲染父组件(以及子组件)。 - UI 根据新的状态更新。 ------ #### 2. **Flutter 的机制:Widget 树与状态驱动 UI** 在 Flutter 中,UI 由 Widget 树构建,状态变化通过 `StatefulWidget` 中的 `setState` 方法触发。和 React 类似,Flutter 也使用“状态驱动 UI”的思想,但它和 React 的根本区别在于: - **Flutter 不会主动监听对象的变化(除非使用响应式工具)**: - React 会自动检测状态变化,且状态由引用的变化触发。 - Flutter 的 `setState` 必须明确告知框架:某些状态已发生变化,重建 Widget 树。 - **Flutter 没有虚拟 DOM**: - Flutter 不需要 Diff 比较新旧状态,而是直接通过 `Widget.build()` 方法重建树。 - 但只有在调用 `setState` 或类似的方法后,Flutter 才会重新执行 `build`。 #### 问题:为什么类似 React 的做法在 Flutter 中无效? 如果在 Flutter 中,父组件将某个方法(如 `setState`)传递给子组件,并在子组件中调用该方法,这种做法**本质上只是调用了一个方法,而不会自动触发 UI 的更新**,除非你显式地调用父组件的 `setState`。 这是因为: 1. Flutter 并不像 React 那样有状态变更的监听机制。 2. Flutter 的状态更新完全依赖于显式调用 `setState` 来触发重建。 ### `vue` / `flutter` - **观察者模式**:都通过观察数据的变化来驱动 UI 更新。 - **双向绑定体验**:GetX 的响应式对象(`Rx` 系列)在使用时表现得像 Vue 的双向绑定,你可以直接修改数据,UI 就会自动更新。 **用GetX 实现 的 Vue 系列响应式** 1. **性能**: GetX 的响应式更新机制更高效,它可以精准更新受影响的组件,而不是整个页面重绘。相比之下,uni-app 的响应式机制在性能优化上较弱(尤其是数据变化较频繁时)。 2. **开发效率**: GetX 使用简洁,避免了过多的模板约束,同时对复杂业务逻辑的状态管理更友好。而 uni-app 虽然有 Vue 的开发体验,但其生态和框架的灵活性较差。 3. **原生能力**: Flutter 的原生性能无疑比 uni-app(基于 WebView 和小程序容器)更强。尤其在复杂交互、高性能场景下,Flutter + GetX 是远胜 uni-app 的组合。