Flutter+OpenHarmony智慧养老预约挂号应用实践与踩坑复盘
1. 为什么养老场景的预约挂号Flutter和OpenHarmony会走到一起1.1 智慧养老App对“预约挂号”功能的独特要求先说我做这个项目的背景吧。我们团队接了一个社区智慧养老平台服务对象是60岁以上的老年人终端设备除了手机还有社区里部署的平板一体机和老人家里的智慧屏。业务方提的第一个核心模块就是预约挂号——让老人不用跑到医院排长队在社区的终端上就能挂到号。这个看似简单的需求放到养老场景里其实很不简单。年轻人挂号可以自己操作老人群体不一样视力下降、手指不灵活、对手机交互不熟悉。我们当时梳理出来几个硬指标正文字号最小不能低于16sp关键按钮的点击热区不能小于44x44像素整个挂号流程要控制在三次点击以内完成核心操作。这比普通App的交互要求严格得多。另一个难点是终端碎片化。小区里的平板一体机是触屏老人家里的智慧屏是遥控器焦点操作老人子女手机上有鸿蒙系统、有安卓、也有苹果。传统的做法是每个平台拉一套原生开发团队但养老项目的预算和周期撑不起这种人力投入。我们最后定了Flutter跨端方案配合OpenHarmony系统做底层能力适配一次编写、多端运行这是整个技术决策的起点。1.2 Flutter跨端能力与OpenHarmony生态的契合性我最早接触OpenHarmony的时候说实话它的应用生态还比较薄弱三方库、成熟组件、开发文档的丰富程度都不如安卓和iOS。但它的系统能力是实打实的分布式软总线、通知服务、权限管理、多设备协同这些对智慧养老场景特别有价值。比如预约挂号成功后要给老人手机发本地通知提醒或者老人的挂号信息需要同步到社区大屏上这些都是OpenHarmony擅长的事。问题在于OpenHarmony自己的原生应用开发语言是ArkTS/ArkUI这套技术栈目前只有HarmonyOS生态在用你单独学一套的成本很高。而Flutter的Dart代码是跨端的UI层一次写好最后编译成不同的目标平台产物。Flutter的OpenHarmony适配引擎会把Flutter的Skia/Impeller渲染层对接到OpenHarmony的图形栈上Dart代码本身不需要改动太多。我在项目里最直接的感受是Flutter的widget组件在OpenHarmony上渲染以后视觉还原度比想象中好很多。Flutter自己负责布局和绘制OpenHarmony只提供最底层的窗口、纹理和输入事件回调。所以你在安卓上长什么样子在OpenHarmony上基本就是什么样子只是部分涉及原生控件的能力需要走平台通道。1.3 为什么不是原生开发也不是纯Web方案团队内部其实争论过一轮。原生开发的好处是系统能力调用直接性能也稳但我们同时要覆盖OpenHarmony手机、触屏一体机、智慧屏三套设备每套设备还要考虑不同的屏幕尺寸和交互方式原生方案意味着至少三套代码并行开发排期直接炸掉。纯Web方案的方案也被否掉了。预约挂号涉及大量的异步交互、状态变更、倒计时逻辑Web页面在社区一体机那种配置不高的设备上滚动和切换动画都会掉帧。而且养老场景经常有网络不稳定的情况Web资源加载慢起来老人就会以为页面卡死了。还有一个关键问题挂号成功后需要读取设备上的通知能力和日历权限Web页面在这块的权限边界很模糊。Flutter的优势在于它既能做到UI层的统一又能通过平台通道拿到OpenHarmony的原生能力性能和权限都在可控范围内。Flutter 3.x之后默认启用Impeller渲染引擎在图形绘制效率上比早期的Skia有明显提升滚动列表的帧率稳定性足够支撑养老终端的低配硬件。2. 环境准备与工程搭建HarmonyOS下跑通Flutter的第一道坎2.1 Flutter SDK与OpenHarmony SDK的版本配比这块是我踩坑最多的地方必须单独说一下。Flutter官方对Android和iOS的支持很完善对OpenHarmony的支持是靠OpenHarmony SIG特别兴趣小组维护的flutter_flutter仓库分支和配套引擎来完成的。也就是说你不能直接用flutter官方SDK去构建OpenHarmony应用需要把Flutter SDK切到OpenHarmony适配分支。我的版本组合是这样定的大家参考的时候要以当时的最新版为准组件版本说明Flutter SDKflutter/engine的OpenHarmony适配分支对应Flutter 3.22需要从OpenHarmony的flutter_flutter仓拉取OpenHarmony SDKAPI 10及以上建议用API 11权限模型和通知服务更完整DevEco Studio4.0及以上用于编译OpenHarmony工程JDK17太高的版本会触发Gradle插件兼容性问题一个很关键的点Flutter的OpenHarmony适配分支和官方Flutter版本号不是一一对应的你在官方版本上写的代码不一定能在适配分支上编译通过。我当时把官方Flutter 3.22的代码切到OpenHarmony分支上遇到一堆编译报错最后老老实实重新拉了一个干净分支来开发。2.2 创建Flutter OHOS工程的完整流程工程创建方式也和官方流程不一样这里直接给出我验证过的步骤。第一步拉取适配分支的Flutter SDKgit clone https://gitee.com/openharmony-sig/flutter_flutter.git cd flutter_flutter git checkout master这里不建议用默认分支需要切到对应的OpenHarmony适配标签比如3.22.0-ohos-3.2之类的具体看仓库里的release记录。第二步配置环境变量export PATH$PWD/bin:$PATH export ANDROID_HOME/path/to/your/android-sdk export OHOS_SDK_HOME/path/to/openharmony/sdk注意OpenHarmony SDK的路径要单独配置不能和Android SDK混在一起。DevEco Studio安装好后内置的SDK路径通常在/Applications/DevEco-Studio.app/Contents/sdk这种地方需要把default/openharmony目录指给OHOS_SDK_HOME。第三步创建工程并添加OpenHarmony平台flutter create my_smart_care_app cd my_smart_care_app flutter pub add ohos然后需要在工程根目录执行一个适配脚本生成ohos目录结构flutter create --platforms ohos .这一步非常关键如果不执行工程里不会生成OpenHarmony的自定义工程目录。生成之后你会看到ohos目录里面是标准的OpenHarmony工程结构包含entry/src/main/ets等路径。2.3 编译期踩坑Gradle插件apply顺序引发的连锁错误动态社区的开发环境里大家搜索flutter项目编译问题时经常看到一个报错说用apply命令式地应用Flutter的Gradle插件会导致之后执行的第三方插件无法读取到flutter引擎的依赖配置。我在OpenHarmony工程里第一次把Flutter模块嵌入的时候就撞上了这个坑。工程里的ohos/build.gradle里如果用apply plugin: org.openharmony.gradle.plugin的方式导入后来又用apply plugin: flutter导入Flutter插件两个插件的装载顺序会直接影响依赖解析。正确的做法是使用plugins {}代码块声明插件依赖让Gradle自行管理加载顺序plugins { id org.openharmony.gradle.plugin version 1.0.0 id flutter }如果已经用了apply方式你会看到类似“Could not resolve all task dependencies”的编译错误定位到的还是compileDebugJavaWithJavac任务依赖不完整。这个问题在适配分支的issue列表里出现过很多次根因就是Flutter插件被apply之后无法向同工程的Java编译任务注入flutter.jar的依赖路径。解决方式就是统一改用plugins {}语法并把根工程的settings.gradle里加上插件仓库地址pluginManagement { repositories { maven { url https://mirrors.huaweicloud.com/repository/maven/ } gradlePluginPortal() } }真实的项目里大家也不要急着升级最新版的Gradle和JDK组合Flutter的OpenHarmony适配分支对Gradle版本是有兼容矩阵的官方文档里会标一个推荐版本范围超过了就会出现各种奇怪的symbol找不到和task依赖错误。3. 预约挂号页面的完整UI实现组件拆分与无障碍设计3.1 从“科室-医生-时间”三级结构中拆分Widget层级预约挂号页面的核心交互流程是选科室、选医生、选时间槽、确认挂号。很多人一上来就把页面做成了一个大ListView数据全都塞在同一个State里状态一变整个页面就重建性能差还容易出Bug。我实际的拆分方式是一个BookingPage负责整体组合下面分成三个独立的Widget模块——DepartmentPicker、DoctorList、TimeSlotGrid每个模块只接收自己的数据和回调互不干扰。class BookingPage extends StatelessWidget { final BookingCubit cubit; override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(预约挂号)), body: Column( children: [ DepartmentPicker( departments: cubit.state.departments, selectedId: cubit.state.selectedDeptId, onSelected: (deptId) cubit.selectDepartment(deptId), ), Expanded( child: DoctorList( doctors: cubit.state.doctors, onSelect: (doctor) cubit.selectDoctor(doctor), ), ), TimeSlotGrid( slots: cubit.state.timeSlots, selectedTime: cubit.state.selectedTime, onSelect: (time) cubit.selectTime(time), ), ], ), ); } }科室选择这里用了横向滚动的ChoiceChip列表每个chip的高度做到48像素文字用16sp。这里有个适老化的隐性需求老人的手指摩擦力小横向滚动容易误触所以我在DepartmentPicker外层包了一个Scrollbar并且在滚动结束回调里做了一次自动吸附保证停留位置的chip是完整的。医生列表用ListView.builder每条卡片高120像素包含医生头像占位、姓名、职称、擅长简介和一个预约按钮。这里有个细节预约按钮不能放在卡片的右上角那种小图标位而是放在整张卡片的底部通栏位置这样老人点击的时候不容易点偏。这就是前面说的“三次点击内完成核心操作”的交互设计支撑。3.2 下拉刷新、骨架屏与PlatformView的适配挂号信息是实时变化的老人在页面停留时间长了医生排班和时间槽信息可能已经过期所以页面必须具备下拉刷新能力。Flutter自带的RefreshIndicator在OpenHarmony上的表现整体可用但有几个适配点要注意。一个是在OpenHarmony的触屏设备上RefreshIndicator的默认触发高度是100像素老人的拖动速度慢经常拖不到触发阈值。我在代码里把triggerMode设置为RefreshIndicatorTriggerMode.onEdge这样在列表边缘继续下拉就会触发刷新不需要拉到固定高度。另一个是骨架屏。挂号页面首次加载时服务端接口在跑如果直接给一个空白页面老人会以为机器坏了。我用了一个简单的占位方案加载中显示灰色圆角矩形块每个块都在呼吸闪烁。这个闪烁动画在低配一体机上要控制好不能每个块都单独开一个AnimationController否则垃圾回收压力很大。正确的做法是共用一个AnimationController把透明度变化值传给所有占位块。在预约挂号的确认页我们还嵌入了一个医院位置地图。这里就涉及Flutter的PlatformView。Flutter本身不能直接渲染高德或百度的原生地图SDK必须通过UiViews把原生View嵌入到Flutter的渲染树里。在OpenHarmony适配分支上PlatformView的支持是由flutter_ohos引擎实现的需要在Dart侧注册view typefinal mapView PlatformViewLink( viewType: com.example.map_view, onCreate: (PlatformViewCreationParams params) { return PlatformViewsService.initSurfaceView( params.id, viewType: com.example.map_view, layoutDirection: TextDirection.ltr, ); }, );注意这里引擎版本在3.22之前对PlatformView用的是虚拟显示方案会有一个明显的View区域黑边升级到3.22适配分支以后改成了纹理合并方案黑边消失但会占用GPU纹理内存。在社区一体机上地图页面打开时间长了会有纹理内存释放不完全导致的花屏问题所以在地图页面销毁时调用PlatformViewsService的相关清理接口是一个必要动作。3.3 适老化交互字号、热区与TalkBack适老化不是一句“字体调大”就能解决的。我在这轮开发里专门把页面的语义标签和无障碍支持做了单独的一次技术评审这里分享几个可落地的方法。字号处理全局设定文字缩放策略用MediaQuery的textScaler属性把基础字号压到系统级1.3倍但关键操作按钮上的文字不能再跟着系统放大否则按钮热区会被顶破。比如“确认挂号”按钮上我使用textScaler: TextScaler.noScaling固定文字大小保证按钮在任何系统字号设置下都是完整通栏可点击的。点击热区用ConstraintBox把最小尺寸框定为44x44即使内部元素视觉上只有20像素高实际可点击范围也扩大到44像素。这是Material设计规范里的标准但在养老场景下仍然是最容易漏掉的基本功。无障碍服务OpenHarmony的屏幕阅读器和TalkBack类似靠的是语义树。Flutter的widget默认会把文本内容暴露给语义树但像“医生排班表”这种复合组件需要手动用Semantics包裹来告诉屏幕阅读器这是一个容器Semantics( container: true, label: 医生列表, child: DoctorList(...), )实际测试下来OpenHarmony的无障碍服务对Flutter语义树的解析效率比对Android原生View低所以语义节点的数量要控制不要在每一个时间槽上单独挂语义标签而是把整个TimeSlotGrid作为单一语义节点暴露让用户聚焦后用左右滑动来切换条目。4. 原生能力桥接EventChannel实现挂号后的通知订阅4.1 为什么预约挂号的“提交成功”需要原生通道预约挂号成功之后业务要求给用户发一个本地通知提醒“您已成功预约请提前30分钟到达医院”。同时如果挂了明天的号后天早上的提醒必须在指定时间弹出。这里有个技术事实Flutter的Dart层跑在UI线程上它的定时器在应用进程被系统杀到后台之后是不可靠的尤其不能在后台做准点通知。所以通知的调度必须交给OpenHarmony的提醒代理服务来实现。Dart和OpenHarmony原生层之间的通信就需要用到平台通道。Flutter和原生平台的通信有三种MethodChannel一次调用一次返回、EventChannel原生向Dart持续推送事件、BasicMessageChannel双向消息传递。我们的场景是挂号成功后原生侧需要订阅系统日历和通知调度的事件流把“预约创建成功”和“提醒时间已到”这两个事件推送给Dart侧处理这就用到了EventChannel。如果用MethodChannel也可以但每次都要Dart侧主动拉一次不够实时。4.2 Dart侧与ArkTS侧EventChannel的完整实现先看Dart侧在挂号提交成功之后注册一个事件流的监听import package:flutter/services.dart; class AppointmentNotificationApi { static const _eventChannel EventChannel( com.example.medical/appointment_notifications); static Futurevoid subscribeAppointmentEvents() async { _eventChannel.receiveBroadcastStream().listen( (event) { if (event is Map) { final type event[type]; if (type appointment_created) { // 刷新挂号列表 _refreshAppointments(); } else if (type appointment_reminder) { // 弹提醒 _showReminderDialog(event[title]); } } }, onError: (error) { debugPrint(EventChannel error: $error); }, ); } }然后看OpenHarmony原生侧。OpenHarmony应用的开发语言是ArkTS在EntryAbility里或者对应页面的etx文件里通过ohos.channel模块注册对应的EventChannelimport { eventChannel } from ohos.channel; import { BusinessError } from ohos.base; const channelName com.example.medical/appointment_notifications; let eventChannelInstance eventChannel.createEventChannel(channelName); eventChannelInstance.on(listen, (callback) { // 原生侧可以向Dart侧推送事件 const event { type: appointment_created, title: 您已成功预约心血管内科张医生的号, time: 2025-06-20 09:30 }; callback.send(JSON.stringify(event)); });这里要注意序列化格式。Flutter的StandardMessageCodec和OpenHarmony平台通道之间传递数据默认支持基础类型和Map但我们在ArkTS侧直接传入Date对象或者自定义类会解析失败。我踩过这个坑在ArkTS侧把API_Date对象塞进event里Dart侧收到的是一个解不开的二进制缓冲。解决方式是统一在原生侧先做JSON.stringifyDart侧再用json.decode解析这样最稳妥。另外如果希望在应用无UI界面时也能推送事件需要使用OpenHarmony的Ability生命周期配合。EventChannel的事件发送依赖Connection连接应用退到后台后连接不会被立刻回收但被系统强制杀掉后事件就会丢失。因此真正的提醒不能只靠EventChannel做还要在原生侧建立一个长效任务把提醒时间写入系统的reminderAgentManager由系统到点弹出。4.3 权限与后台提醒的配置OpenHarmony从API 10开始权限模型比较严格通知权限和后台任务权限都需要在module.json5里声明。{ module: { requestPermissions: [ { name: ohos.permission.NOTIFICATION_CONTROLLER, reason: 需要发送挂号成功通知, usedScene: { abilities: [EntryAbility] } }, { name: ohos.permission.KEEP_BACKGROUND_RUNNING, reason: 需要后台调度挂号提醒, usedScene: { abilities: [EntryAbility] } } ] } }注意NOTIFICATION_CONTROLLER是系统级权限普通应用不一定能申请到。普通的本地通知权限实际上用的是ohos.permission.NOTIFICATION这个权限在用户第一次触发订阅时弹窗让用户确认。在代码里要先调用notificationManager.isNotificationEnabled()检查用户是否授权如果没授权需要引导到系统设置页。做养老App时这里要有心理准备老人可能看不懂授权弹窗最好的方案是应用安装引导页里直接用图示告诉用户“点允许否则收不到挂号提醒”。后台提醒的调度我使用的是reminderAgentManager的publishReminder接口指定触发时间、通知标题和内容import { reminderAgentManager } from ohos.reminderAgentManager; let reminder { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER, triggerTime: new Date(2025-06-20T09:00:00).getTime(), wantAgent: { pkgName: com.example.medical, abilityName: EntryAbility }, title: 预约挂号提醒, content: 您预约的专家号将在30分钟后开始就诊请及时前往医院。, notificationId: 1001 }; reminderAgentManager.publishReminder(reminder).then((id) { console.log(Reminder created: ${id}); });这个方式绕过了应用进程存活与否的限制即使App被杀了系统到点后仍会拉起提醒。这才是真正可靠的后台提醒方案。5. 状态管理与异步流程预约提交时的防重复与回执处理5.1 状态管理选型Cubit在挂号场景中的实际问题预约挂号页面的前端状态其实不多当前选中的科室、当前显示的医生列表、当前选中的时间槽、当前提交状态。但状态之间的联动很烦人——选了科室要重新拉医生列表选完医生要重新拉时间槽提交之后要清除所有已选状态并跳转。一开始我用了Provider写起来快但三个页面模块共享同一个状态源时会互相通知因为ChangeNotifier只能整体广播每次更新都导致所有Consumer重建。在医院列表页这种频繁交互的场景实测帧率掉到40帧左右虽然不影响功能但在一体机上显得特别卡。后来换成了Bloc的Cubit它的核心优势是状态被拆成一个个不可变快照每个页面模块可以只监听自己关心的那个状态字段。比如时间槽组件只监听timeSlots字段的变化科室选择器变了不会触发它重建。class BookingCubit extends CubitBookingState { BookingCubit(this._repository) : super(BookingState.initial()); Futurevoid selectDepartment(int deptId) async { emit(state.copyWith(selectedDeptId: deptId, doctors: [], timeSlots: [])); final doctors await _repository.fetchDoctors(deptId); emit(state.copyWith(doctors: doctors)); } Futurevoid selectDoctor(int doctorId) async { emit(state.copyWith(selectedDoctorId: doctorId, timeSlots: [])); final slots await _repository.fetchTimeSlots(doctorId); emit(state.copyWith(timeSlots: slots)); } Futurevoid submitBooking() async { emit(state.copyWith(submitting: true)); try { final result await _repository.submitAppointment(state.toRequest()); emit(state.copyWith(submitting: false, bookingConfirmed: result)); } catch (e) { emit(state.copyWith(submitting: false, error: e.toString())); } } }这套状态机的核心逻辑就是每次切换上级选项下级列表先清空再重新加载避免旧数据残留。5.2 Future的then回调与微任务队列倒计时按钮为什么不会卡预约挂号里一般会有一个“获取验证码”的按钮点击后进入60秒倒计时。很多新手写法是await Future.delayed(Duration(seconds: 1))然后循环这样会把每个秒级间隔都作为一个微任务插入事件队列如果页面里同时有列表滚动动画微任务积压就会造成卡顿。Flutter里Future的then回调默认是放入微任务队列的而Timer的回调是放入事件队列的。微任务会在当前事件循环中尽快执行但如果在同帧内塞入了大量微任务就会阻塞帧渲染。所以倒计时正确做法是用Timer.periodicTimer? _countdownTimer; int _secondsLeft 60; void _startCountdown() { _countdownTimer?.cancel(); _secondsLeft 60; _countdownTimer Timer.periodic(Duration(seconds: 1), (timer) { setState(() { _secondsLeft--; if (_secondsLeft 0) { timer.cancel(); } }); }); }有一个更隐蔽的坑submitBooking这个异步方法里如果在await之后直接context.readBookingCubit()要看当前widget是否还在树上。如果用户在提交过程中切走了页面await回来时context已经解挂调用read会抛异常。安全的做法是在Cubit内部gather状态而不是在widget里await之后再重新读context。5.3 提交挂号的幂等设计与失败重试预约挂号的提交最怕重复。老年人的操作习惯是点了按钮之后如果没看到“提交成功”的提示会下意识再点一次。如果后端没有做幂等控制一次挂号请求发出两次用户就会拿到两个号。前端首先要做的是一层锁提交过程中把按钮置为不可点击状态同时把整个页面的Pointer事件拦截掉防止连环点击。但光这样不够因为网络超时后前端会释放锁用户再次点击时第一次请求可能还在服务端处理中还是会形成两笔订单。前后端协同的幂等方案是在提交请求里带上一个requestId前端每次进入确认页时生成一个UUID服务端以requestId作为唯一键做去重。如果服务端发现相同requestId已经处理过就直接返回上一次的处理结果而不是重新创建挂号单。class BookingRepository { FutureAppointmentResult submitAppointment(BookingRequest request) async { final requestId request.requestId ?? Uuid().v4(); try { return await _api.createAppointment(request); } catch (e) { // 网络超时后的重试 return _api.createAppointment(request, retryWithSameRequestId: true); } } }失败重试策略上我采用的方案是第一次失败后不自动重试而是弹出一个对话框问用户“网络异常是否重新提交”。如果用户选择重试再发第二次请求时仍带上同一个requestId。这样既避免用户以为失败而手动重进页面造成新单又能可靠地拿到最终结果。6. 实测记录与踩坑复盘从开发机到真机验证的主要问题6.1 编译期Gradle插件apply顺序引起的连锁错误前面提到过工程搭建阶段的apply问题但在真机上又遇到了一次变体。我们当时在工程里集成了推送、地图等三四个第三方插件每个插件都是一个Flutter package它们内部各自有自己的pubspec.yaml声明。OpenHarmony适配分支解析这些插件时如果主工程里用了apply方式加载Flutter插件就会出现插件依赖循环错误报错集中在:ohos:compileDebugKotlin任务。解决这个过程花了差不多两天。根因是OpenHarmony工程使用hvigor构建系统和Android的Gradle不完全一样很多三方插件在编译OpenHarmony平台时需要走一个ohosPlugin的处理流程。如果Flutter插件在Gradle阶段没有被生成对应的转换产物后续hvigor阶段就找不到依赖符号。大家的建议是不要只盯着报错信息先检查根目录下的.flutter-plugins-dependencies文件是否正常工作。这个文件里列出了所有被Flutter解析到的插件路径如果某个插件在文件里缺失说明它的Ohos支持没有被引擎识别需要检查该插件是否提供了ohos目录。6.2 运行期OpenHarmony上的滚动卡顿与PlatformView纹理问题预约挂号页面里的医生列表是一个长列表再加上时间槽网格滑动操作非常频繁。在开发机Windows模拟器上一切正常但在真机上发现滚动明显掉帧。排查后发现问题出在时间槽网格里每个slot我都加了阴影阴影使用了BoxShadow数量一多就触发了Flutter的绘制层重建。在低端设备上GPU纹理提交带宽有限大量带阴影的小矩形会造成过绘制。我把每个slot改成纯色边框去掉阴影同时列表项用RepaintBoundary隔离重绘区域后帧率稳定在50帧以上。另一个问题是PlatformView纹理。地图页面从预约详情页返回时偶尔会出现一小块黑色残留。这是因为PlatformView在OpenHarmony上的纹理生命周期没有在返回动画中被正确回收。我用了一个Workaround在Navigation.push返回时延迟200毫秒再销毁地图页面实例让返回动画先跑完再把地图原生Surface摘除。实测下来花屏率从15%降到0。6.3 逻辑期切换科室后医生列表状态残留上线前最严重的一个Bug是用户先选了“心血管内科”医生列表加载出来滚动到第20个位置然后切到“骨科”再切回来时“心血管内科”的医生列表还停留在第20个位置但数据已经刷新列表跳到第1个位置视觉上出现一帧“旧数据停留”的现象。这个问题本质上是Flutter的ListView在收到新数据源后复用了旧的滚动偏移。Cubit里已经清空了doctors但widget树上的ListView还没有重建它的ScrollController还保存着旧的偏移值。修复方式是给DoctorList加一个PageStorageKey并且切换科室时重建列表的keyDoctorList( key: PageStorageKey(doctor_list_${cubit.state.selectedDeptId}), doctors: cubit.state.doctors, )这样每个科室有自己的独立list实例不会互相复用滚动位置。这里还要提醒一句如果用了skeletal动画或者占位组件重建时要把占位组件包在AnimatedSwitcher里否则会出现一瞬间的白屏闪烁老人会对这种闪烁特别敏感。最后再分享一个实际体会做养老场景的App技术上的难点永远不在“实现功能”而在“功能实现之后怎么让老人愿意用”。预约挂号模块我做了很多轮真机测试每次让社区老人来试操作时都能发现新的交互问题。轮椅级的最优先需求是减少步骤眼神不好的用户需要大字号、高对比度手抖的用户需要足够的点击容差。这些需求不一定能通过某个技术面试题反映但在代码里它们是通过每一个Semantics标签、每一个BoxConstraints约束和每一帧滚动性能实打实地雕琢出来的。这些经验希望对正在做智慧养老方向的朋友们有用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →