ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Flutter纯Dart库鸿蒙化:Google Maps Web服务适配实战

Flutter纯Dart库鸿蒙化:Google Maps Web服务适配实战 接手鸿蒙HarmonyOS NEXT适配那会我第一反应是先扒一遍 Flutter 项目里的依赖树看有没有那种卡在手里根本绕不开的原生模块。结果翻到flutter_google_maps_webservices这个库的时候松了口气它是个纯 Dart 包走的 HTTP JSON 路线封装的是 Google Maps Web Services地理编码、方向、距离矩阵、地点、时区这些核心能力并不依赖 Android/iOS 的原生 Google Maps SDK。这意味着鸿蒙化时不需要重写原生代码适配重点直接转移到权限声明、密钥管理、网络错误处理和调用治理上。如果你也在把出海 Flutter 应用迁到鸿蒙恰好在用或者准备用这套地图 Web 能力这篇文章应该能帮你少走几周的弯路。我用一句话概括这个库的定位它把后端地图计算能力搬到了 Flutter 客户端让移动应用可以直接调 Google 的地图 Web API拿到地址坐标、路线几何和点位信息。适合的场景包括外卖配送的距离计算、门店选址分析、路线规划、地址自动补全这些功能在鸿蒙 App 里同样需要因此适配这件事本身就有实打实的业务价值。1. 项目背景与适配思路为什么这个库的鸿蒙化没那么吓人1.1 出海应用迁移鸿蒙时地图 Web 服务为什么不能砍鸿蒙 NEXT 不再兼容 Android 安装包这就意味着原来跑在 Android 上的 Flutter 应用要真正重新落地一遍。对出海产品来说地图能力里最容易出问题的不是地图 UI 渲染而是背后已经跑了很多年的业务逻辑地址解析、门店排序、运费计算、ETA 预估。举个例子外卖 App 里用户地址和商家距离小于 3 公里才配送这个规则如果在旧 Android 版里用的是 Google Distance Matrix API那这套逻辑会散落在好几个地方客户端发请求、后端缓存结果、数据库里存距离数据。若因为地图 SDK 不好迁就把整个 Web 服务层换掉意味着之前所有基于 Google 坐标体系的缓存、历史订单数据、计价策略全部失效这是极其昂贵的工程事故。所以在鸿蒙化初期我定了条原则先保 Web 服务再谈地图渲染。只要 Google Maps Web Services 能在鸿蒙设备上发出 HTTPS 请求并拿到 JSON 响应业务核心就不会塌。地图 UI 部分可以后续通过其他渲染引擎接上但 Web 服务是数据层迁移动不得。1.2 拆解 flutter_google_maps_webservices 的模块边界这个库的爹是 fluttercommunity它把 Google Maps Web Services 里最常用的几个 API 全部封装成了 Dart 类GoogleGeocoding正向地理编码地址转坐标和反向地理编码坐标转地址。GoogleDirections路线规划返回折线点列表、距离、时长。GoogleDistanceMatrix批量计算多个起点到多个终点的距离和时长很适合配送计价。GooglePlaces地点搜索、自动补全、地点详情。GoogleTimeZone根据经纬度和时间戳返回时区 ID。核心请求链路是Dart 层构造 URL比如https://maps.googleapis.com/maps/api/geocode/json - http 包发送 GET - 解析 JSON - 返回强类型模型。因为所有能力都是通过 Web API 实现的而不是通过 Google 官方地图 SDK 的原生方法安卓的 GoogleMap 对象、iOS 的 MKMapView所以鸿蒙的 Flutter SDK 只要保证dart:io的 socket 能力映射到鸿蒙协议栈这个库就能跑。1.3 鸿蒙化的本质不是重写是打通三层我在项目里反复强调一个观点第三方库鸿蒙化先判断它是纯 Dart 库还是平台插件。纯 Dart 库的鸿蒙化重点是三层第一层是编译链路。确认 pub 依赖在鸿蒙 Flutter SDK 下能正常解析、编译产物能打进鸿蒙应用。第二层是网络路径。鸿蒙没有默认给非系统应用放行网络权限需要在 module.json5 里声明ohos.permission.INTERNET还要处理 DNS、TLS、超时等实际工程问题。第三层是服务端鉴权。Google API 的 Key 不能硬编码在客户端密钥管理策略在鸿蒙上尤其重要因为鸿蒙生态的应用审核和加固体系跟 Android 并不完全一致。把这三点处理完剩下的就是业务代码层面的包装了。这远比写一个原生鸿蒙 Module 要省事得多。2. 适配前的静态检查把依赖、权限和调试环境一次备齐2.1 先看依赖树再做最小编译验证正式开始动代码之前我建议先跑一遍依赖检查。我自己习惯用这条命令flutter pub deps --stylecompact重点看两个东西第一flutter_google_maps_webservices依赖了哪些包。它的生态里最核心的依赖是http、json_annotation、json_serializable这些都是纯 Dart 包不涉及原生平台通道第二这些包的版本在鸿蒙 Flutter SDK 上是否彼此兼容。有时候单独某个包没问题但锁定的版本跟鸿蒙 SDK 的 Dart 版本冲突就会在编译期直接崩掉。我在验证的时候是这么操作的先用鸿蒙分支的 Flutter SDK 创建一个空工程直接在这个工程里添加依赖不写任何业务代码只写一个初始化GoogleGeocoding的 Dart 文件编译到鸿蒙真机上。这一步过了再往里面叠加业务层。千万别上来就搬整个项目否则出错时根本分不清是鸿蒙适配问题还是业务代码问题。2.2 module.json5 权限清单INTERNET 是最低要求在 Android 项目里网络权限是在 AndroidManifest.xml 里写uses-permission android:nameandroid.permission.INTERNET/。鸿蒙不一样HarmonyOS NEXT 应用模块的权限配置在module.json5里面有一个requestPermissions数组。最小配置长这样{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.INTERNET } ] } }这里有个很容易忽略的点如果后续你会用到当前位置附近的门店这类功能除了 INTERNET 权限还需要加定位权限。鸿蒙的定位权限分两个级别{ name: ohos.permission.APPROXIMATELY_LOCATION }以及精确位置权限ohos.permission.LOCATION。申请定位权限时系统会走动态授权弹窗这一点跟 Android 的运行时权限逻辑类似。但定位权限不是随便申请的审核时如果发现权限和功能不匹配会被打回来。我只申请和应用场景强相关的权限避免为了省事一把梭。2.3 调试链路模拟器、真机和抓包工具开发期我用鸿蒙模拟器做第一轮验证跑通后再挪到真机上。这里要单独说下抓包调试 Google Maps Web Services 时抓包几乎是必做的因为你得确认 HTTPS 请求是否真的发出去、响应体里到底返回了什么错误。鸿蒙真机抓包有一个坑我记得在 4.0 系统上特别明显本地抓包工具打开后App 里所有 HTTPS 请求都会证书校验失败。原因是抓包工具用的证书没有安装到鸿蒙系统信任区。解决办法是手动安装并信任抓包工具的调试证书然后重启 App。如果遇到CERTIFICATE_VERIFY_FAILED先别急着怀疑代码大概率就是证书没装好。我自己调试时会在 Dart 侧加一个环境开关只有 debug 模式才设置自定义的 HttpClientrelease 模式坚决不碰证书逻辑避免把调试通道带进生产包。3. 实操记录把 flutter_google_maps_webservices 跑通鸿蒙设备3.1 环境准备鸿蒙 Flutter SDK 与工程落地鸿蒙应用开发目前要用 DevEco Studio HarmonyOS SDK而 Flutter 侧的鸿蒙适配不是谷歌官方 Flutter 仓库直接支持的它基于 OpenHarmony 的分支。我用的方案是安装 DevEco Studio装好 HarmonyOS SDK。配置 Flutter 鸿蒙分支 SDK并把flutter命令路径切到该分支。用flutter doctor -v检查 SDK 是否被正确识别。工程落地有两种方式。第一种是直接用 Flutter 鸿蒙分支提供的模板工程它自带ohos目录可以在 DevEco Studio 里打开。第二种是把现有 Flutter 项目的ohos目录补齐。对已经存在的项目我更喜欢第二种因为业务代码不用动只要把鸿蒙壳工程加上去。创建完工程后第一件事是跑flutter build harmonyos确认基础构建链路没问题。此刻先不管 Google 地图先把空 Flutter App 能跑在鸿蒙 Simulator 上这个里程碑落地。3.2 最小验证用 Geocoding 打通 API 通路在工程里添加依赖dependencies: flutter_google_maps_webservices: ^0.1.1然后写一个最小页面把 Geocoding 请求跑起来。我选 Geocoding 作为第一个验证对象因为它的入参最简单一个地址字符串返回坐标链路最短最容易定位问题。import package:flutter/material.dart; import package:flutter_google_maps_webservices/geocoding.dart; class GeocodingDemo extends StatefulWidget { const GeocodingDemo({super.key}); override StateGeocodingDemo createState() _GeocodingDemoState(); } class _GeocodingDemoState extends StateGeocodingDemo { String _result 等待请求; override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(Geocoding 验证)), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: _callGeocoding, child: const Text(发起地址解析), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.all(16), child: Text(_result), ), ], ), ), ); } Futurevoid _callGeocoding() async { final geocoding GoogleGeocoding(_readApiKey()); final response await geocoding.geocode(1600 Amphitheatre Parkway, Mountain View, CA); if (response.status GeocodingStatus.ok) { final first response.results.first; setState(() { _result first.formattedAddress ?? ; }); } else { setState(() { _result 请求失败: ${response.errorMessage}; }); } } }这里有个重要的实践经验GoogleGeocoding构造时接受apiKey但我强烈不建议把 Key 以字符串常量写在上面的代码里。后面我会专门讲密钥管理但现在先拿一个临时 Key 验证通路。当你在鸿蒙真机上看到请求成功返回地址时说明三件事已经成立鸿蒙 Flutter 网络的 socket 能力正常、Google API 域名可达、库的 JSON 解析逻辑没在鸿蒙运行时上出问题。3.3 API Key 注入dotenv、打包与后端转发API Key 就像银行卡客户端直连 Google 相当于把银行卡密码写在手机壳背面。鸿蒙应用相比 Android 有一个更麻烦的地方鸿蒙生态的加固、混淆、包提取防护方案跟 Android 不完全一样密钥藏在客户端里的安全性更难保证。所以我给团队的方案分两级第一级是至少要做到编译期注入而不是硬编码。用 flutter_dotenvimport package:flutter_dotenv/flutter_dotenv.dart; final apiKey dotenv.env[GOOGLE_MAPS_API_KEY] ?? ;.env文件加进.gitignore不出现在版本库里。第二级是业务量大的项目直接走后端转发。Flutter 端只请求自己后端GET /api/maps/geocode?address...后端拿到请求后再拼上 Google API Key 去访问真实服务。这样客户端永远不接触 Key配额、计费、缓存、降级全部收敛在后端。代价是多一跳延迟但配合后端缓存整体速度未必比客户端直连差。这个方案尤其适合鸿蒙上架后的监管合规要求因为很多企业有密钥审计需求后端转发是最好的兜底实践。3.4 更多 API 的调用示例Directionss、DistanceMatrix 和 PlacesGeocoding 通了之后其余 API 基本是复制粘贴级别。我直接贴几个我实测过的调用片段Directions 路线规划import package:flutter_google_maps_webservices/directions.dart; final directions GoogleDirections(_readApiKey()); final response await directions.directions( origin: Location(lat: 37.4224764, lng: -122.0842499), destination: Location(lat: 37.7749295, lng: -122.4194155), travelMode: TravelMode.driving, );Distance Matrix 距离矩阵import package:flutter_google_maps_webservices/distance_matrix.dart; final matrix GoogleDistanceMatrix(_readApiKey()); final response await matrix.distance( origins: [Location(lat: 37.4224764, lng: -122.0842499)], destinations: [Location(lat: 37.7749295, lng: -122.4194155)], travelMode: TravelMode.driving, );注意Location类来自这个库的geocoding.dart或独立的location.dart在鸿蒙上没有任何原生依赖放心用。Places 自动补全import package:flutter_google_maps_webservices/places.dart; final places GooglePlaces( _readApiKey(), language: en, region: us, ); final result await places.autocomplete(Mountain View);这两个参数值得解释一下language控制返回结果的语言region控制地点排序的偏向。比如同样是输入 New Yorkregionus会优先纽约州的结果而regiongb可能优先英国同名地点。这个参数对海外多区域运营很重要。4. 排坑实录鸿蒙化路上我踩过的 7 个问题4.1 请求超时默认 http.Client 没有超时第一个坑最隐蔽。代码写好后在地铁里随手点了几次接口发现请求经常挂起两分钟才报错。查了下源码发现flutter_google_maps_webservices内部使用的http.Client()没有默认超时时间。这在网络环境良好的办公室没问题但鸿蒙设备在弱网环境比如双卡切换、公用 Wi-Fi中非常容易卡死。我的解决办法是给每个请求统一设置超时。但库的构造器不一定暴露了所有请求的超时入口所以更稳妥的做法是在工程层包装一层http.Clientimport package:http/http.dart as http; class TimeoutClient extends http.BaseClient { TimeoutClient(this._inner, {this.timeout const Duration(seconds: 15)}); final http.Client _inner; final Duration timeout; override Futurehttp.StreamedResponse send(http.BaseRequest request) { return _inner.send(request).timeout(timeout); } }然后尽量通过库的httpClient参数传入。如果某些 API 类不支持注入就用Completer包裹请求做超时控制。核心思想是绝对允许超时终端用户能接受失败但不能接受无响应。4.2 模拟器上 DNS 解析异常真机却正常这大概是鸿蒙模拟器的老毛病。调试时发现模拟器上所有 Google API 请求都失败错误是SocketException: Failed host lookup。但同样的代码放到真机上立刻就好。排查思路是先确认是不是 DNS 的锅。我写了段临时代码在 Dart 里手动解析域名import dart:io; final addresses await InternetAddress.lookup(maps.googleapis.com); print(addresses);在模拟器上看到返回的全部是 IPv6 地址而模拟器网络环境里 IPv6 路由根本不通所以请求全部超时。解决方式很直接在模拟器设置里把网络切到桥接模式或者改用真机调试。做网络类库适配时真机优先级应该高于模拟器。4.3 REQUEST_DENIED一半以上是配置问题鸿蒙上跑起来以后最常见的业务错误是REQUEST_DENIED。出现这个错误我建议按这个顺序排查先在 Google Cloud Console 确认你要用的 API比如 Geocoding API、Places API确实已经开启再检查 API Key 的类型和限制如果 Key 设置了 HTTP 引用来源限制但客户端请求的 User-Agent 不符合预期就会被拒绝最后检查是否绑定到了正确的项目别把 Key 从 A 项目抄到 B 项目然后对着 B 项目排查半天。鸿蒙环境下尤其要注意因为鸿蒙浏览器的容器和常见 Android WebView 在标识上不同如果你限制了 referer 白名单可能误伤正常的 Flutter 请求。这种问题在日志里看不到明显特征最好的方式是先用一个无限制的临时 Key 验证通路确认能通后再逐步收紧。4.4 OVER_QUERY_LIMIT配额不是无限刷的业务上线后第一个星期就开始收到OVER_QUERY_LIMIT的错误上报。这个错误分成两种情况超过了每日配额或者超过了每秒请求数限制。Google Maps Web Services 对每个 API 都有每日免费额度比如 Geocoding API 大约是每天 10000 次请求Distance Matrix API 按单次请求里起止点对数量计费。如果在鸿蒙端同时有多个页面并发调用每秒 QPS 很容易打爆。解决办法是后端缓存 队列。我后面第 6 章会细讲这里先说原则所有高频、数据相对静态的请求全部用 Redis 或本地数据库缓存不要让同一地址的编码请求反复打到 Google。4.5 抓包工具介入后所有请求突然证书失败调试 Google API 的响应内容离不开抓包。但鸿蒙真机开抓包工具后我遇到一个非常迷惑的现象只有用了抓包工具证书校验就失败关掉抓包工具立刻恢复。原因不复杂抓包工具在中间做 TLS 解密需要 iOS/Android/HarmonyOS 信任它的根证书否则客户端校验服务器证书时发现链路不完整直接拒绝。鸿蒙的证书信任机制比 Android 更严格普通方式安装的调试证书只能影响部分应用。我的实践方式是只在专用的测试机上操作安装抓包工具证书到系统信任区并且每一次装完证书后强制杀掉 App 进程再重新打开。不要试图在生产机上搞这个操作得不偿失。4.6 定位数据接不进 FlutterEventChannel 的正确姿势很多地图 Web 服务没有定位能力本身但业务入口往往需要当前经纬度。鸿蒙原生的定位结果要送给 Flutter 层绕不开平台通道通信。鸿蒙侧通过geoLocationManager注册位置监听把经纬度打包后通过eventChannel发送给 Dart// ets 端示例 import { geoLocationManager } from kit.LocationKit; eventChannel.send({ latitude: currentLocation.latitude, longitude: currentLocation.longitude, });Dart 侧用 EventChannel 接收import package:flutter/services.dart; const channel EventChannel(location/updates); channel.receiveBroadcastStream().listen((event) { final map MapString, dynamic.from(event as Map); // 拿到经纬度后传给 GoogleGeocoding 做反向地址解析 });这里有个易错点鸿蒙的定位权限请求是异步的必须在 Dart 侧先调用 MethodChannel 触发权限弹窗等用户授权后再注册 EventChannel 监听否则数据收不到。4.7 混淆与序列化release 包偶发解析全空开发环境跑起来一切正常打 release 包之后Geocoding 的返回对象.results经常为空或者字段全空。这个问题一度让我怀疑是鸿蒙的系统字体或者编码问题查到最后发现是打包时的混淆策略在作祟。flutter_google_maps_webservices的模型大量使用json_serializable自动生成的 fromJson/toJson。如果打包时开启过激进的混淆、字段名被重写或者生成代码里通过 getter 取字段的方式被破坏都会导致运行时拿到的是空值。我的建议是如果业务对包体大小不是极度敏感先不要对依赖库做混淆如果一定要混淆release 包出来后完整过一遍所有 API 的回归用例不要只看编译产物能否生成。5. 常用错误码速查表排查 Google Maps Web Services 时把错误码背下来是没用的但做成速查表贴在文档里非常有效。这是我在鸿蒙项目里沉淀下来的表格错误码含义鸿蒙场景下的排查建议INVALID_REQUEST请求参数缺失或不合法检查经纬度是否传反、格式是否错误检查路线规划的起终点是否有 nullZERO_RESULTS查无结果地址拼写不对region 区域偏向导致尝试缩短地址文本NOT_FOUND路径规划中无法定位起终点确认坐标在 Google 支持的范围内REQUEST_DENIED请求被拒绝检查 API 是否开启、Key 是否有效、是否受限用临时无限制 Key 排除问题OVER_QUERY_LIMIT超出配额或 QPS 限制看 Cloud Console 用量曲线后端加缓存和队列UNKNOWN_ERROR服务器内部错误通常等几秒重试可恢复需要在客户端做重试网络异常SocketException、TimeoutException、DNS 解析失败鸿蒙模拟器优先切换真机确认 INTERNET 权限检查企业路由器是否拦截 HTTPS这类表格最好放进团队内部的 Wiki 或者代码仓库的 README 里遇到问题先查表比临时翻官方文档快得多。6. 进阶治理限速、缓存与错误提示的统一收口6.1 并发控制把自由请求改成受限队列Google Maps Web Services 不是无限并发服务。距离矩阵这类 API 是按元素数计费的如果业务上每分钟发起上千个点对计算后端要能压得住。我在鸿蒙适配里采用了一种很朴素的限速方案Dart 侧维护一个全局请求队列控制每 200ms 最多发一个请求。简单实现如下class RateLimiter { RateLimiter({this.minInterval const Duration(milliseconds: 200)}); final Duration minInterval; DateTime _lastRequestAt DateTime.fromMillisecondsSinceEpoch(0); FutureT runT(FutureT Function() action) async { final now DateTime.now(); final waitTime minInterval - (now.difference(_lastRequestAt)); if (waitTime Duration.zero) { await Future.delayed(waitTime); } _lastRequestAt DateTime.now(); return action(); } }不要小看这个 200ms它能帮你把瞬时 QPS 从几十压到个位数极大降低OVER_QUERY_LIMIT的概率。当然更精细的做法是使用rate_limiter包或者后端令牌桶但绝大多数客户端场景下这个简单实现已经够用。6.2 缓存策略让重复请求不再打爆 Google 配额地址解析的结果在短期内是稳定的北京市朝阳区某大厦今天解析出来的坐标和明天不会有区别。这意味着 Geocoding 是缓存收益最高的接口。我建议按请求参数 region language拼缓存 key把响应 JSON 存到本地数据库或者 Hive 里缓存有效期可以放到 30 天。Places 自动补全的缓存时间可以短一些因为商家数据可能变化。Distance Matrix 的缓存更特殊它跟路况、交通模式相关建议默认缓存 1 小时高峰期的路线结果只缓存 5 分钟。我在鸿蒙项目里用hive做本地缓存Dart 侧封装一层MapsCache每次请求前先查缓存命中则直接返回未命中再走网络。一套下来Google 的配额消耗能降低六成以上。6.3 用户侧的错误提示别把内部错误码裸露给用户鸿蒙应用上架审核对崩溃率、无响应率抓得很严。如果用户在弱网下看到OVER_QUERY_LIMIT或者REQUEST_DENIED这种错误码会觉得很不专业而且这种页面很容易被用户投诉。我把业务侧的错误提示全部收口成一个统一的ApiExceptionclass ApiException implements Exception { ApiException(this.message, {this.rawCode}); final String message; final String? rawCode; }UI 层只根据ApiException.message展示比如网络暂时繁忙请稍后再试。原始错误码只在日志和监控平台里透出方便自己排查不让用户看到。这样一个看似简单的收口实际上对上线后的用户评价影响非常大。鸿蒙应用商店的内测用户对崩溃和异常的容忍度比老安卓用户低得多。错误提示打磨得干净一点审核通过率和用户留存都会受益。7. 写在最后的个人经验真正把flutter_google_maps_webservices迁到鸿蒙之后我最大的体悟是鸿蒙化不等于重写关键在于分类判断。纯 Dart 库就是适配成本最低的那一类它不碰原生 SDK只是需要你把网络权限、密钥管理、错误处理和缓存治理这层外围工事做好。最后再分享一个小技巧如果后续你的鸿蒙应用想彻底摆脱地图渲染层面的历史包袱可以只保留这个库的 Web 服务能力用于路线计算和地址解析地图 UI 部分改用鸿蒙原生地图 SDK 的 TileOverlay 渲染。这样从数据到渲染全部原生化海外版和国内版可以在同一套 Flutter 业务代码下分别对接不同的地图服务商运维起来会舒服很多。
返回列表