
最近在做 Flutter for OpenHarmony 的音乐播放器 App 适配第一版功能先跑登录流程。原本以为把 Android 上的那套代码搬过去就能跑实际上手才发现OpenHarmony 对 Flutter 的支持还不够“无脑”尤其是涉及原生能力调用、安全存储、Token 刷新这些环节得多绕几个弯。这篇就围绕登录实现把我在实际开发中的思路、踩坑和最终落地的方案写清楚。如果你正准备把 Flutter 应用迁移到 OpenHarmony或者刚开始用 Flutter 开发鸿蒙应用这篇应该能帮你少走不少弯路。登录功能看着简单真做起来牵扯到 UI 状态管理、接口鉴权、本地存储、原生通信、生命周期处理一整套下来不少细节要抠。1. 登录模块的整体设计与关键抉择1.1 登录功能要解决的几个核心问题一开始我先不急着写代码把登录这块拆成了几个必须解决的问题后面所有工作都是围绕这张清单展开的。第一是身份认证。用户输入账号密码之后客户端要能安全地把凭证传给服务端换成后续请求用的令牌。第二是会话管理。拿到令牌之后客户端要记录登录态并且要能自动续期不能让用户用着用着突然被踢下线。第三是安全存储。令牌、刷新令牌、用户信息这类敏感数据不能明文放在本地OpenHarmony 的轻量化存储虽然方便但安全级别不够。第四是退出登录。清理本地数据、通知服务端吊销令牌、回到未登录入口这一套流程要完整闭环。这四件事里身份认证和会话管理可以完全在 Flutter 层解决安全存储和能力调用则必须借助 OpenHarmony 原生能力第四件事属于业务逻辑和状态管理。想清楚这些边界以后架构方案就自然浮现了UI 和业务逻辑全部待在 Flutter 侧只把“必须要碰原生”的能力用 Channel 接下去。1.2 为什么选择 JWT 而不是传统 Session做登录方案的时候很多人第一反应是沿用服务端 Session 的思路——登录成功后由服务端生成一个 Session ID存在服务端内存或 Redis客户端只保存这个 ID。这套方案在纯 Web 端很成熟但放到 Flutter for OpenHarmony 这个场景里有几个绕不开的痛点。OpenHarmony 生态的服务端配套还不算丰富而且这个 App 后续要做碰一碰、多设备协同这类特性很可能需要把登录态从一个设备传到另一个设备。Session 是服务端状态跨设备迁移时要处理 Session 同步、粘性会话一堆问题。JWT 是自包含令牌用户信息直接编码在 Token 里服务端不用保存状态Devices 之间天然支持迁移。另一个原因是适配成本。这个 App 除了 OpenHarmony 端还有 Android 和 iOS 端三端共用同一套业务 API。如果服务端为每一端分别维护 Session后端同事会被运维搞崩溃。JWT 这套“客户端拿 Token、服务端验签名”的模式天然适合多端统一鉴权。当然 JWT 也有代价最明显的是 Token 泄露后有效期内没法主动失效服务端不存状态。实际项目中我用短 Token30 分钟有效 长 Refresh Token7 天有效 配合用户主动退出时请求服务端加入黑名单来缓解这个问题。1.3 为什么登录页用 Flutter 实现而不是原生这个项目里有个争议点登录页要不要用 OpenHarmony 的 ArkUI 写一部分观点觉得登录页是 App 的门面应该用原生组件保证流畅度和兼容性。我最后拍板用 Flutter 统一实现理由是三个。第一代码复用率拉满。Flutter 的 UI 代码在 Android、iOS、OpenHarmony 三个平台几乎零改动如果登录页单独用 ArkUI 写这一整块代码就是重复工作量。第二维护成本低。登录流程会频繁改需求——比如加个第三方登录入口、改个按钮样式、调一轮 UI 走查Flutter 的热重载在改 UI 这个场景下效率远高于原生的编译-安装-运行循环。第三这个 App 的核心业务是播放器登录只是流程的一部分把登录页单独拆成原生是一笔不划算的投资。唯一需要借助原生能力的是安全存储、获取设备唯一标识、拉起系统授权这类操作这些通过 MethodChannel 来桥接即可。2. 登录页面的 UI 设计与表单校验逻辑2.1 登录页布局与基础组件选型登录页我采用经典的上下结构上半部分是品牌区和 Logo下半部分是表单区和操作按钮底部有注册入口和第三方登录入口的占位。Flutter 里实现这个布局用 Scaffold SingleChildScrollView Column 就足够了。表单区域用 TextFormField 承载账号输入框和密码输入框Form GlobalKey 管理表单状态和校验逻辑。代码结构大致长这样class LoginPage extends StatefulWidget { const LoginPage({super.key}); override StateLoginPage createState() _LoginPageState(); } class _LoginPageState extends StateLoginPage { final _formKey GlobalKeyFormState(); final _accountController TextEditingController(); final _passwordController TextEditingController(); bool _obscurePassword true; bool _isSubmitting false; override void dispose() { _accountController.dispose(); _passwordController.dispose(); super.dispose(); } override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32), child: Form( key: _formKey, child: Column( // 表单控件 ), ), ), ), ); } }这里有个容易忽略的点StatefulWidget 里的 TextEditingController 必须在 dispose 方法里释放否则会内存泄漏。Flutter 调试模式不容易看出来但线上高频率开关登录页内存会悄悄上涨。我在项目中统一用一个 mixin 或基类封装 Controller 的生命周期成了一种团队规范。2.2 表单校验规则的前后端协同表单校验不能只做前端。前端校验是为了用户体验——输错了马上提示不用等网络请求转圈完才知道结果。后端校验才是安全底线前端校验规则和后端要同步否则就会出现“前端能过、后端拒绝”的体验断层。我在这个项目里定了一套校验规则账号只允许 6 到 20 位字母数字组合密码要求 8 到 20 位且至少包含字母和数字。前端校验用 Flutter 内置 validator 实现String? _validateAccount(String? value) { if (value null || value.isEmpty) { return 请输入账号; } if (!RegExp(r^[a-zA-Z0-9]{6,20}$).hasMatch(value)) { return 账号为6-20位字母或数字; } return null; } String? _validatePassword(String? value) { if (value null || value.isEmpty) { return 请输入密码; } if (value.length 8 || value.length 20) { return 密码长度需在8到20位之间; } if (!RegExp(r[a-zA-Z]).hasMatch(value) || !RegExp(r[0-9]).hasMatch(value)) { return 密码需包含字母和数字; } return null; }规则本身不复杂但校验逻辑要放在独立的 validator 函数里不要在 build 方法里写一大坨 if-else否则代码读起来很累。后续要给某个字段加“不允许包含空格”之类的规则直接改函数即可不用动整个组件。密码明文传输的问题也要在登录这个环节就考虑。我见过很多项目把密码直接明文 POST 给服务端这在内部测试没问题但生产环境一旦抓包密码全裸奔。这个项目里我对密码做了二次处理客户端先用 SHA-256 对密码加盐哈希再把哈希后的结果作为密码字段传给服务端服务端再做一次加盐哈希落库。这样即使请求被截获也拿不到原始密码。这个方案不复杂但效果实打实。2.3 用状态管理串联登录 UI 和业务逻辑登录按钮点击后要经历“校验表单 → 发送请求 → 等待响应 → 处理成功/失败 → 跳转首页”一整串流程。这里的 UI 状态切换很关键提交过程中按钮要置灰和显示 loading不能允许用户重复点击失败时要弹 SnackBar 或者用轻提示展示原因成功后要先清空导航栈再跳转到主页面。我用的是 Provider ChangeNotifier 的组合这个方案在 Flutter 社区最普及团队上手成本低也不引入太重的东西。登录页面里面需要拆出两部分状态表单控件自身的状态input 内容、焦点由 StatefulWidget 管理登录业务逻辑的状态loading、error、已登录用户信息由 AuthViewModel 管理。ViewModel 的核心里面有一个 login 方法它对页面暴露一个回调登录成功后调用页面跳转。这样页面只管渲染和反馈具体登录成功之后要往哪跳、要不要记录埋点全部交给页面去路由层消费逻辑更清晰。关键点是loading 状态在按钮上要用 AnimatedSwitcher 之类的组件包一下让按钮文本从“登录”切换到转圈动画时有过渡效果。这个细节对体验的提升非常明显成本极低强烈建议加上。3. Flutter 与 OpenHarmony 原生通信的那点事3.1 该选 MethodChannel 还是 EventChannel做 Flutter 和 OpenHarmony 原生通信时大家最容易纠结的是 MethodChannel 和 EventChannel 的区别和选择。MethodChannel 是双向的“调用-返回”模式Flutter 侧发起调用、传参数原生侧处理完返回结果。适合一次性请求类型的功能比如存一个 token、获取设备型号、调用一次系统弹窗。EventChannel 是“订阅-推送”模式Flutter 侧订阅某个通道原生侧持续往这个通道发事件。适合流式的、订阅式的功能比如监听系统电量变化、网络状态切换、播放器的播放进度回调。我这个项目里登录功能涉及的原生调用都是请求-响应类型比如“帮我保存 token”“帮我获取设备唯一标识”“帮我读取安全存储里的刷新令牌”所以主用 MethodChannel。但有一个地方用到了 EventChannel登录过程中如果监听到了系统异常比如用户强制切走网络、系统存储服务不可用原生侧需要主动通知 Flutter 层这个时候 EventChannel 是天然的选择。不过要提醒一点EventChannel 的订阅者生命周期要管理好。我在项目里见过同事忘记取消订阅导致页面销毁后原生还在持续往 Flutter 发事件轻则日志刷屏重则内存泄漏。正确做法是在 State 的 dispose 方法里调用eventChannel.receiveBroadcastStream().listen(...)返回的 StreamSubscription 的cancel()。3.2 用 MethodChannel 桥接 OpenHarmony 安全存储Token 不能直接放在 SharedPreferences 或 Flutter 的 shared_preferences 插件里这个坑我希望大家一开始就绕过去。OpenHarmony 系统提供了安全控件和安全存储能力密钥、关键业务数据要放到受系统级保护的存储区域去普通 preference 存储等于把令牌直接写在明文件里被恶意应用读取只差一个 root 权限。登录环节的“保存 Token”和“获取 Token”这两个能力通过自定义 MethodChannel 来桥接。通道名称我定为com.example.musicplayer/auth。Flutter 侧代码如下class AuthNativeBridge { static const _channel MethodChannel(com.example.musicplayer/auth); static Futurevoid saveToken(String token, String refreshToken) async { await _channel.invokeMethod(saveToken, { token: token, refreshToken: refreshToken, }); } static FutureMapString, String readToken() async { final result await _channel.invokeMethodMap(readToken); if (result null) return {}; return result.castString, String(); } static Futurevoid clearToken() async { await _channel.invokeMethod(clearToken); } }OpenHarmony 侧用 ArkTS 实现与这个通道对应的 Handler。Flutter 引擎在 OpenHarmony 上启动后原生工程里需要注册一个 MethodChannel 处理器import { FlutterPlugin, MethodCall, MethodChannel } from ohos/flutter_ohos; const CHANNEL_NAME com.example.musicplayer/auth; export class AuthChannelHandler implements FlutterPlugin { private channel: MethodChannel | null null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME); this.channel.setMethodCallHandler((call: MethodCall) { // 分发调用 }); } onDetachedFromEngine(binding: FlutterPluginBinding): void { this.channel?.setMethodCallHandler(null); this.channel null; } }实现 Handler 时有几个细节要注意通道名要和 Flutter 侧完全一致一个字符都不能差方法名也要保持统一建议在两端各维护一份字符串常量避免手滑写错原生侧回传的参数类型要严格对应Flutter 侧的invokeMethod泛型参数写Map就收MapString, Object不要返回一个自定义对象类型。3.3 设备唯一标识的获取与登录链路登录时通常会带上设备标识用于服务端做多端登录管理、风险评估和 Token 绑定。这个字段从原生获取比较稳妥因为 Flutter 层拿到的可能是模拟器伪造的值。OpenHarmony 侧获取设备唯一标识的代码思路大概这样通过系统 API 读取设备 UUID 或设备属性生成一个稳定标识返回给 Flutter 层。注意这个值不能每次都变否则服务端会把同一个用户识别成多个设备直接影响“踢下线”和“多设备管理”功能。实测的时候发现一个坑在 OpenHarmony 模拟器上获取设备标识有时会返回全零或者空串导致登录请求参数不合法。后来我在 Flutter 侧做了兜底如果原生返回的标识为空就改用安装后随机生成的 UUID 存到安全存储里保证每个端至少有一个全局唯一的设备标识。3.4 原生和 Flutter 通信之间的时序问题Flutter 和 OpenHarmony 引擎建立连接是有时序的。如果在引擎还没 ready 的时候就调用 MethodChannel会直接抛异常或者静默失败。这个问题在 App 冷启动后立即自动登录的场景下特别容易出现。我的做法是做个通道就绪的检查逻辑App 启动后先等待 Flutter 引擎完成初始化再发起存储读取。实际项目里封装了一个await ensureNativeBridgeReady()的 Future内部用 Completer 管理状态在didChangeAppLifecycleState或者引擎启动回调里置为完成。这样自动登录流程不会因为时序问题导致偶发失败。4. 登录流程的完整实操与代码落地4.1 项目环境准备Flutter SDK 与 OpenHarmony 工程结构动手写代码前先确认环境。我当前用的 Flutter SDK 是支持 OpenHarmony 的分支版本通过 OpenHarmony 官方提供的 Flutter SDK 适配仓库拉取然后配置环境变量。创建 OpenHarmony 的 Flutter 工程有两种方式。一种是直接用 DevEco Studio 创建一个标准的 OpenHarmony 工程然后把 Flutter 模块作为插件集成进去另一种是直接用 Flutter 命令行创建工程再用 DevEco Studio 打开生成的工程目录。我建议用第二种Flutter 的工程结构会更标准后续加插件方便。工程目录大致是project/ ├── flutter/ # Flutter 业务代码 │ ├── lib/ │ │ ├── main.dart │ │ ├── pages/ │ │ │ └── login/ │ │ └── services/ │ └── pubspec.yaml ├── entry/ # OpenHarmony 应用入口ArkTS │ └── src/main/ │ ├── ets/ │ │ ├── entryability/ │ │ └── pages/ │ └── module.json5 └── build-profile.json5搭建环境这一步最容易出问题的是版本对齐。OpenHarmony 的 SDK 版本、Flutter 适配版本、DevEco Studio 版本三者要匹配我踩过“DevEco 更新后旧 Flutter 适配包编译报错”的坑后来固定了一套版本组合并写在团队的 README 里后续新人过来拉代码直接按 README 配环境半小时就能跑起来。4.2 登录状态的本地管理与自动登录实现登录成功之后拿到两个令牌accessToken短时有效30 分钟和 refreshToken长时有效7 天。accessToken 用于每次接口请求的 Bearer 鉴权refreshToken 专门用来换新 accessToken。自动登录的场景是这样App 冷启动后先去安全存储读取 refreshToken 和用户信息。如果 refreshToken 存在且未过期就直接用 refreshToken 换一个 accessToken 然后跳进首页如果 refreshtoken 不存在停留在登录页。Token 是否过期的判断不能只靠客户端本地时间戳判断因为用户可以改系统时间。我这边维护了一个本地“过期时间”字段启动时如果本地时间已经超过过期时间就进入静默刷新流程刷新失败则跳登录页重新输入密码。静默刷新失败后不要马上踢人这点特别重要。网络抖动、服务端临时不可用都有可能导致刷新失败。我做了“刷新失败重试两次 失败后再引导重新登录”的策略。第一次失败后等待 3 秒重试第二次失败后等待 5 秒再试。还不行才提示用户“登录已过期请重新登录”。这样用户体验会好很多。4.3 注册、登录、下线功能的全流程闭环登录模块不只有“登录”一个按钮。注册、登录、下线三件事是一个完整闭环要一次性设计好别做登录的时候忘了注册和退出。注册页面的逻辑和登录页类似只是额外多一个“确认密码”输入框和“同意用户协议”的勾选。注册成功后直接跳到登录页让用户用刚才的账号主动登录一次这个交互设计比“自动登录”更稳妥用户也更有安全感。下线的流程我要多说一句。很多新手做退出登录只做了“清掉本地 Token”忽略了服务端吊销。如果用户的 Token 被偷了本地清了但 Token 还有效风险很大。正确流程是弹出确认框用户确认要退出调服务端登出接口传入当前 accessToken服务端侧把 Token 标记失效或加入黑名单清理本地安全存储中的 Token 和用户信息清空导航栈回到登录页如果服务端登出接口失败也要清理本地 Token并提示“网络异常已退出本地登录”第五步很容易被忽略。一定要保证“本地清理”无论如何都执行否则用户点了退出但 Token 还在下次启动自动登录成功等于没退出去。代码里我把下线的清理动作放在 finally 块里面Futurevoid logout() async { try { await _apiService.logout(accessToken); } catch (e) { // 网络异常服务端吊销失败但不影响本地退出 } finally { await _authStorage.clearToken(); await _userStorage.clearUserInfo(); } }4.4 网络请求封装与 Token 注入拦截器登录之后的所有业务请求都要带上 accessToken 作为认证凭证。这个逻辑不要散落在每个页面里而是封装在 Dio 的拦截器里面统一处理。Dio 拦截器的核心逻辑是class AuthInterceptor extends Interceptor { override Futurevoid onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { final token await AuthStorage.readAccessToken(); if (token ! null) { options.headers[Authorization] Bearer $token; } handler.next(options); } override Futurevoid onError( DioException err, ErrorInterceptorHandler handler, ) async { if (err.response?.statusCode 401) { // accessToken 过期尝试用 refreshToken 刷新 final refreshed await _refreshToken(); if (refreshed) { // 重放一次原始请求 handler.resolve(await _dio.fetch(err.requestOptions)); return; } } handler.next(err); } }这个拦截器的巧妙之处在于当请求因 Token 过期返回 401它自动用 refreshToken 换新 Token然后重放原始请求。用户几乎感知不到“过期再刷新”这个过程体验非常顺滑。要留一个心眼如果是用 refreshToken 去调刷新接口时也返回了 401那就说明 refreshToken 也失效了要立即触发清理本地数据 跳登录页绝不能进入“用 refreshToken 刷新 refreshToken”的死循环。我加了个标记变量_isRefreshing刷新期间其他并发请求进入时等这个 Future 完成而不是各自发起一次刷新否则服务端会被并发刷新请求打爆。4.5 登录接口的联调细节与服务端返回约定联调阶段最容易扯皮的是接口返回格式不统一。我建议在项目早期就把接口规范定死包括成功和失败时的 HTTP 状态码和响应体结构。我们这个项目的规范是成功返回 HTTP 200响应体为{ code: 0, message: success, data: { accessToken: ..., refreshToken: ..., expiresIn: 1800, userInfo: { userId: 123456, nickname: 淘淘, avatar: https://..., level: 2 } } }业务失败返回 HTTP 200 非零 code比如{ code: 10001, message: 账号或密码错误, data: null } } code 是业务错误码HTTP 状态码永远 200。这样区分的好处是网络层不用对每个错误码做映射Dio 的 onError 只处理真正的网络异常和 401业务错误直接在业务层通过 code 判断。坏处是调试时不能只靠 HTTP 状态一眼看出问题但项目内统一了这个约定工具链都配套了效率更高。 ## 5. 登录实现中遇到的典型问题与排查思路 ### 5.1 自动登录时页面闪跳、白屏 自动登录场景下启动后先读本地 Token同时显示一个 Splash 页。读取 Token 是异步操作如果 Splash 页还没等异步结果回来就跳转路由界面会闪一下再跳到另一个页面看起来很掉价。 我的处理是Splash 页用一个 FutureBuilder 包裹等 Token 读取、刷新如果需要全部结束后再决定跳登录页还是首页。在这期间 Splash 页保持品牌静态页不让它闪走。这个方案简单可靠不用引入复杂的路由守卫。 做完之后我测试了模拟器和真机的冷启动路径基本没有闪跳问题。偶尔在低端机器上出现 Splash 页短暂空白排查下来是引擎初始化耗时导致增加一个 loading 动画后用户感知正常。 ### 5.2 Token 存不进去或读出来是空的 这个问题在刚开始接 OpenHarmony 安全存储时让我头疼了一阵。后来发现原因是原生侧 Channel Handler 在 Flutter 调用时还没注册完。因为 OpenHarmony 的 FlutterPlugin 生命周期和 Activity/Ability 的启动时机有关必须在 Ability 的 onCreate 里完成注册不能在全局静态初始化里注册否则 Flutter 侧调 MethodChannel 会找不到 handlerinvokeMethod 直接抛 MissingPluginException。 排查这种问题有个高效的方法Flutter 侧在调用前先判断通道是否可用。 dart if (await _channel.checkPlatformInterfaceIsAvailable()) { // 安全的调用 } else { // 等待或走 fallback }事后我在工程里做了个辅助封装把“通道是否 ready”的状态用一个静态变量缓存避免每次调用都去查一遍系统。5.3 登录请求偶发卡在 loading 状态用户反馈时不时会出现“点登录按钮转了五六秒没反应”的情况。查了日志发现是网络请求超时时间设太长了而且没有统一的 loading 超时兜底。我的方案是给 Dio 的 connectTimeout 设为 10 秒receiveTimeout 设为 15 秒登录按钮的 loading 状态最多显示 20 秒到点还没结果就自动恢复可点击状态并弹提示“网络似乎不太顺畅请稍后重试”。这样即使用户网络环境差也不会一直卡在无尽 loading 里。关键是通过Timer在 UI 层加了个兜底不等 Dio 的回调超时直接恢复 UI、通知用户重试。5.4 多设备登录被踢下线服务端逻辑是所有端共享同一个账号体系一个端登录另一个端会被踢。被踢的一端要能在用户正在听歌时也能给出提示这就用到了前面说的 EventChannel 能力。服务端通过长连接推送一条“账号在其他设备登录”的消息OpenHarmony 原生层收到之后通过 EventChannel 推给 Flutter 层。Flutter 层弹一个全屏 Dialog提示“你的账号在其他设备登录如非本人操作请修改密码”给出两个按钮“重新登录”和“重新加载”。这个提醒即使 App 在前台放歌也能弹出来比纯静默处理体验好很多。实现这块时要注意线程。我在原生层让推送消息直接回调长连接所在的线程直接调 Flutter 侧 Channel 会导致线程异常。最后是切到主线程再发布事件问题才解决。这个经验贴出来因为遇到同样场景的人应该不少。5.5 登录界面被软键盘顶得乱七八糟音乐播放器 App 有搜索和登录两类需要软键盘的场景而登录页面的输入框很容易被软键盘挡住。这属于 Flutter UI 的经典问题。我的处理是根布局用 Scaffold它默认就带resizeToAvoidBottomInset属性当软键盘弹出时整个页面会被压缩到键盘上方配合 SingleChildScrollView 就能保证焦点输入框可见。还需要给登录按钮加一个padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom)这类逻辑确保键盘弹出后底部按钮不会被遮住。特别注意如果某天引入了原生嵌入视图PlatformView承载广告或验证码组件软键盘和 PlatformView 的交互在 OpenHarmony 上可能会打折扣。这块目前还在验证阶段暂时先绕开把第三方登录的能力延后上线等适配更成熟了再补齐。6. 实测下来的心得与后续优化方向登录实现到目前这个版本我已经在真机上完整跑通了“注册 → 登录 → 自动登录 → 刷新 Token → 退出登录 → 重复登录”的全链路稳定性还算满意。有一点值得单独说OpenHarmony 对 Flutter 的适配整体已经能用但 Plugin 生态远不如 Android 丰富遇到问题不要指望直接找现成库大概率要自己写薄薄的一层桥接。团队里至少要有一个懂 ArkTS 和 OpenHarmony 应用开发的人否则 Flutter 业务开发会卡在原生桥接上很久。这个登录模块后续还有一些值得优化的方向第一从账号密码登录扩展到验证码登录。短信验证码在 OpenHarmony 上需要对接推送服务适配成本比 Android 高一些可以放到第二个迭代。第二接入指纹或面部识别替代密码输入。OpenHarmony 的生物识别 API 可以走原生侧做那 Flutter 侧只需要调一个 MethodChannel 并支持回调就可以了体验升级明显。第三Token 的离线使用与多设备协同登录。这个项目后续要做多设备协同播放音乐登录态同步和跨端续播是一个很有意思的课题等真正开工的时候再更一篇。就写到这里吧希望对正在做 Flutter for OpenHarmony 适配的朋友有帮助。登录只是万里长征第一步后面播放器核心播放链路、音频焦点管理、后台播放还有更多硬仗要打下次有机会再继续分享。