【maaath】Flutter for OpenHarmony 定位服务能力集成指南

Flutter for OpenHarmony 定位服务能力集成指南

作者:maaath


欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

一、前言

Flutter for OpenHarmony(以下简称 FOH)是 OpenHarmony 生态中重要的跨平台开发框架,它允许开发者使用 Dart 语言编写一套代码,同时运行在 Android、iOS、Web 以及 OpenHarmony 等平台上。本文以集成定位服务能力为核心主题,详细介绍如何在 FOH 项目中实现 GPS 定位、网络定位与位置变化监听,并完成在开源鸿蒙模拟器上的运行验证。

在移动应用开发中,定位服务是最常见也是最核心的能力之一。OpenHarmony 提供了功能完善的 @kit.LocationKit 开发套件,为 FOH 应用的定位能力集成提供了坚实的技术基础。接下来,我们将从权限配置、模型设计、服务封装到页面实现,完整地走一遍定位服务的集成流程。

二、技术方案总览

本方案采用分层架构设计:

  • 数据模型层(LocationModels):定义位置数据的结构化表示和错误类型枚举。
  • 业务服务层(LocationManager):封装所有定位相关的原子操作,包括单次定位、连续定位、逆地理编码等。
  • 界面展示层(LocationPage):基于 ArkUI 的声明式 UI 构建定位结果展示界面和交互控件。

LocationManager 作为纯服务类,不依赖任何 UI 组件,可以轻松迁移到其他页面或模块中复用。整个方案完全基于 OpenHarmony 原生 API 实现,不依赖任何第三方定位插件,因此具备良好的兼容性和稳定性。

三、权限配置

定位服务属于敏感权限,必须在 module.json5 中声明。在 requestPermissions 数组中添加位置权限声明:

json 复制代码
{
  "name": "ohos.permission.LOCATION",
  "reason": "$string:permission_location_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}

"inuse" 策略意味着只有当应用处于前台活跃状态时才会触发权限弹窗。reason 字段用于向用户说明申请该权限的合理理由,这是 OpenHarmony 隐私合规的强制要求。

四、数据模型设计

typescript 复制代码
export class LocationData {
  latitude: number;
  longitude: number;
  altitude: number;
  accuracy: number;
  speed: number;
  timeStamp: number;

  constructor(latitude: number = 0, longitude: number = 0,
              altitude: number = 0, accuracy: number = 0,
              speed: number = 0, timeStamp: number = 0) {
    this.latitude = latitude;
    this.longitude = longitude;
    this.altitude = altitude;
    this.accuracy = accuracy;
    this.speed = speed;
    this.timeStamp = timeStamp;
  }

  isValid(): boolean {
    return this.latitude !== 0 || this.longitude !== 0;
  }

  getCoordinates(): string {
    return `${this.latitude.toFixed(6)}, ${this.longitude.toFixed(6)}`;
  }
}

export enum LocationError {
  SUCCESS = 0,
  PERMISSION_DENIED = 1,
  LOCATION_OFF = 2,
  TIMEOUT = 3,
  UNKNOWN = 4
}

isValid() 方法判断当前定位数据是否有效,getCoordinates() 将经纬度格式化为带 6 位小数的字符串。LocationError 枚举用于区分不同场景的定位失败原因。

五、核心服务封装

LocationManager 采用单例模式设计,确保全应用只存在一个定位服务实例:

typescript 复制代码
import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = 'LocationManager';
const DOMAIN = 0xFF01;

type LocationCallback = (location: LocationData) => void;
type ErrorCallback = (error: LocationError, message: string) => void;

export class LocationManager {
  private static instance: LocationManager | null = null;
  private currentLocationCallback: LocationCallback | null = null;
  private continuousLocationCallback: LocationCallback | null = null;
  private errorCallback: ErrorCallback | null = null;
  private isContinuousMode: boolean = false;

  private constructor() {}

  static getInstance(): LocationManager {
    if (LocationManager.instance === null) {
      LocationManager.instance = new LocationManager();
    }
    return LocationManager.instance;
  }

5.1 单次定位

单次定位使用 geoLocationManager.getCurrentLocation() API 实现,priority 参数控制定位模式:传入 100 表示 GPS 优先(精度高但依赖硬件),传入 102 表示网络定位优先(响应快但精度略低)。

typescript 复制代码
  getCurrentLocation(useGps: boolean, callback: LocationCallback): boolean {
    hilog.info(DOMAIN, TAG, `Get location, useGps: ${useGps}`);

    if (this.isContinuousMode) {
      hilog.warn(DOMAIN, TAG, 'Continuous mode active');
      return false;
    }

    this.currentLocationCallback = callback;
    const priority: number = useGps ? 100 : 102;
    const request: geoLocationManager.LocationRequest = {
      priority: priority,
      scenario: 0
    };

    geoLocationManager.getCurrentLocation(request).then((location: geoLocationManager.Location) => {
      hilog.info(DOMAIN, TAG, `Got location: ${location.latitude}`);
      const locData: LocationData = new LocationData(
        location.latitude, location.longitude,
        location.altitude, location.accuracy,
        location.speed, location.timeStamp
      );
      if (this.currentLocationCallback) {
        this.currentLocationCallback(locData);
        this.currentLocationCallback = null;
      }
    }).catch((err: BusinessError) => {
      hilog.error(DOMAIN, TAG, `Error: ${err.code}`);
      if (this.errorCallback) {
        this.errorCallback(LocationError.UNKNOWN, err.message);
      }
      this.currentLocationCallback = null;
    });

    return true;
  }

5.2 连续定位(位置变化监听)

连续定位适用于导航、运动轨迹记录等需要实时跟踪位置变化的场景。通过 geoLocationManager.on('locationChange', ...) 注册位置变化监听,在页面退出时必须调用 stopLocationUpdates() 释放监听以防止电量消耗。

typescript 复制代码
  startLocationUpdates(useGps: boolean, callback: LocationCallback): boolean {
    hilog.info(DOMAIN, TAG, 'Start updates');

    if (this.isContinuousMode) {
      return false;
    }

    this.continuousLocationCallback = callback;
    this.isContinuousMode = true;

    const priority: number = useGps ? 100 : 102;
    const request: geoLocationManager.LocationRequest = {
      priority: priority,
      scenario: 0
    };

    geoLocationManager.on('locationChange', request, (location: geoLocationManager.Location) => {
      hilog.debug(DOMAIN, TAG, `Update: ${location.latitude}`);
      const locData: LocationData = new LocationData(
        location.latitude, location.longitude,
        location.altitude, location.accuracy,
        location.speed, location.timeStamp
      );
      if (this.continuousLocationCallback) {
        this.continuousLocationCallback(locData);
      }
    });

    return true;
  }

  stopLocationUpdates(): void {
    if (this.isContinuousMode) {
      geoLocationManager.off('locationChange');
      this.isContinuousMode = false;
      this.continuousLocationCallback = null;
      hilog.info(DOMAIN, TAG, 'Stopped');
    }
  }

5.3 逆地理编码

将坐标转换为人类可读的地址信息,依赖网络连接:

typescript 复制代码
  getAddressFromLocation(latitude: number, longitude: number): Promise<string> {
    hilog.info(DOMAIN, TAG, `Get address: ${latitude}`);

    return new Promise<string>((resolve, reject) => {
      const request: geoLocationManager.ReverseGeoCodeRequest = {
        latitude: latitude,
        longitude: longitude
      };

      geoLocationManager.getAddressesFromLocation(request).then((result) => {
        hilog.info(DOMAIN, TAG, 'Address success');
        if (result && result.length > 0) {
          const item: geoLocationManager.GeoAddress = result[0];
          const addr: string[] = [];
          if (item.countryName) { addr.push(item.countryName); }
          const locale: string = item.locale ?? '';
          if (locale) { addr.push(locale); }
          const desc: string = item.descriptions?.[0] ?? '';
          if (desc) { addr.push(desc); }
          resolve(addr.length > 0 ? addr.join(' ') : 'Address found');
        } else {
          resolve('Address not found');
        }
      }).catch((err: BusinessError) => {
        hilog.error(DOMAIN, TAG, `Address error: ${err.code}`);
        reject(err);
      });
    });
  }

5.4 定位开关检测

typescript 复制代码
  isLocationEnabled(): boolean {
    return geoLocationManager.isLocationEnabled();
  }

  release(): void {
    this.stopLocationUpdates();
    this.currentLocationCallback = null;
    this.errorCallback = null;
    hilog.info(DOMAIN, TAG, 'Released');
  }

六、UI 页面实现

定位页面采用 ArkUI 声明式 UI 构建,界面分为状态栏、位置卡片和控制面板三个区块:

typescript 复制代码
@Entry
@Component
struct LocationPage {
  @State isLocating: boolean = false;
  @State isContinuousMode: boolean = false;
  @State locationMode: string = 'single';
  @State lastLocation: LocationData | null = null;
  @State addressInfo: string = '';
  @State updateCount: number = 0;
  @State error: string = '';

  private locationManager: LocationManager = LocationManager.getInstance();

  aboutToAppear(): void {
    this.animateEntrance();
  }

  aboutToDisappear(): void {
    this.locationManager.release();
  }

页面入口动画使用 animateTo 实现淡入上滑效果:

typescript 复制代码
  animateEntrance(): void {
    animateTo({
      duration: 400,
      curve: Curve.FastOutSlowIn,
      delay: 100,
      iterations: 1,
      playMode: PlayMode.Normal
    }, () => {
      this.cardOpacity = 1;
      this.cardTranslateY = 0;
    });
  }

单次定位与连续定位通过不同按钮触发,定位成功后自动调用逆地理编码接口获取地址信息:

typescript 复制代码
  getSingleLocation(useGps: boolean): void {
    if (this.isLocating || this.isContinuousMode) { return; }
    this.isLocating = true;
    this.locationMode = 'single';
    this.error = '';

    this.locationManager.setErrorCallback((error: LocationError, message: string) => {
      hilog.error(DOMAIN, TAG, `Error: ${message}`);
      this.error = message;
      this.isLocating = false;
    });

    const success: boolean = this.locationManager.getCurrentLocation(useGps, (locationData: LocationData) => {
      this.lastLocation = locationData;
      this.isLocating = false;
      this.getAddressInfo(locationData.latitude, locationData.longitude);
    });

    if (!success) { this.isLocating = false; }
  }

  startContinuousLocation(useGps: boolean): void {
    if (this.isContinuousMode) {
      this.stopContinuousLocation();
      return;
    }
    this.isContinuousMode = true;
    this.locationMode = useGps ? 'gps' : 'network';
    this.updateCount = 0;
    this.error = '';

    this.locationManager.startLocationUpdates(useGps, (locationData: LocationData) => {
      this.lastLocation = locationData;
      this.updateCount++;
    });
  }

页面底部控制面板提供 GPS 和网络两种定位模式的单次/连续切换按钮,同一按钮承担"启动"和"停止"双重职责。状态栏实时显示定位模式、更新次数和错误信息,位置卡片展示经纬度、高程、精度、速度及地址详情。

七、路由配置

main_pages.json 中注册路由:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/LocationPage"
  ]
}

在首页导航入口中通过 router.pushUrl() 跳转:

typescript 复制代码
Text('  Location >')
  .fontSize(14)
  .fontWeight(FontWeight.Medium)
  .fontColor('#2196F3')
  .onClick(() => {
    router.pushUrl({ url: 'pages/LocationPage' });
  })

八、运行验证

在 OpenHarmony 模拟器上进行了实际运行验证。应用基于 Flutter for OpenHarmony 框架构建,使用 @kit.LocationKit 完成定位服务的底层调用。

模拟器环境中,定位服务提供虚拟位置数据。应用启动后,点击首页 "Location" 进入定位服务页面,页面加载时播放淡入动画。点击 "GPS" 按钮触发单次定位,状态由 "Ready" 变为 "Locating...",成功后显示经纬度(31.976300, 118.792900)、海拔(23.0m)、精度(10.0m)、速度(0.0m/s),并自动展示逆地理编码后的地址信息。点击 "Network" 按钮触发网络定位模式。连续定位模式下,每次位置更新时计数器递增,再次点击同一按钮即可停止。

九、总结与扩展

本文完整介绍了在 Flutter for OpenHarmony 项目中集成定位服务能力的全过程:

  • 可复用性LocationManager 单例封装,定位逻辑与 UI 完全解耦,可通过 LocationManager.getInstance() 在任意页面复用。
  • 多模式支持:同时支持 GPS 和网络定位,覆盖高精度和快速响应两种使用场景。
  • 优雅的错误处理LocationError 枚举配合错误回调机制,支持精准的错误提示。
  • ArkUI 声明式 UI:动画效果配合卡片布局,视觉效果现代且交互流畅。

后续扩展方向包括:地理围栏(Geofencing)能力,当用户进入或离开特定区域时触发通知;后台定位支持,使应用在后台时仍能持续获取位置更新;以及与地图服务集成,在地图上可视化展示用户位置和移动轨迹。

感谢各位阅读!

相关推荐
maaath1 小时前
【maaath】Flutter for OpenHarmony分类筛选与标签匹配深度剖析
flutter·华为·harmonyos
isyangli_blog3 小时前
华为企业级虚拟化解决方案
华为
说再见再也见不到3 小时前
华为交换机QoS配置一条龙:从基础模型到MQC实战
华为·交换机·qos·端口限速
耳東陈3 小时前
Flutter开箱即用一站式解决方案5.0-ComDraggable悬浮拖拽
flutter
Lanren的编程日记3 小时前
Flutter 鸿蒙应用快捷操作功能实战:快捷菜单+快捷手势+快捷键支持,打造高效操作体验
flutter·华为·harmonyos
memoryjs3 小时前
鸿蒙系统进一步学习(二):ArkUI底层原理揭秘
学习·华为·harmonyos
木斯佳4 小时前
HarmonyOS 本地存储实战:用一个记账本案例吃透 RDB 与 KVStore
harmonyos·存储
苗俊祥4 小时前
纯AI打造沐界输入法--简洁、流畅、实用的 HarmonyOS 中文输入法
华为·harmonyos
MonkeyKing4 小时前
蓝牙GAP通用访问协议详解:从原理到多平台实战代码
flutter·蓝牙