Flutter for OpenHarmony 导航地图应用开发实战

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

作者:maaath

一、引言

随着 OpenHarmony 生态的快速发展,跨平台开发框架在鸿蒙设备上的适配成为开发者关注的焦点。Flutter 作为业界领先的跨平台 UI 框架,已经在 OpenHarmony 上实现了良好的运行支持。本文将基于 Flutter for OpenHarmony 跨平台技术,带领读者从零构建一个功能完整的导航地图应用,涵盖实时导航、POI 搜索、离线地图等核心功能。

本文所有代码均已在鸿蒙设备上验证通过,读者可前往 AtomGit(https://atomgit.com)获取完整项目源码。

二、项目架构设计

导航地图应用采用分层架构设计,分为数据模型层、服务层和 UI 层三层结构:

  • 数据模型层:定义导航相关的数据结构,如路线、POI 点、电子眼、交通路况等
  • 服务层:提供导航业务逻辑,包括路线规划、POI 搜索、收藏管理、离线地图下载等
  • UI 层:基于 Flutter Material Design 3 构建用户界面,包含地图展示、搜索面板、导航控制等组件

三、核心数据模型设计

首先,我们需要定义导航应用的核心数据类型。以下是导航路线模型的关键代码:

enum TrafficLevel {
  smooth,
  slow,
  congested,
  blocked,
}

extension TrafficLevelExtension on TrafficLevel {
  String get levelText {
    switch (this) {
      case TrafficLevel.smooth:
        return '畅通';
      case TrafficLevel.slow:
        return '缓行';
      case TrafficLevel.congested:
        return '拥堵';
      case TrafficLevel.blocked:
        return '严重拥堵';
    }
  }
}

class NavigationRoute {
  final String id;
  final String name;
  final GeoPoint origin;
  final GeoPoint destination;
  final String originName;
  final String destinationName;
  final double totalDistance;
  final int totalDuration;
  final double tollFee;
  final List<RouteStep> steps;
  final List<TrafficSegment> trafficSegments;
  final List<CameraInfo> cameras;
  final List<VoiceGuidance> voiceGuidances;
  final String routeType;

  const NavigationRoute({
    required this.id,
    required this.name,
    required this.origin,
    required this.destination,
    required this.originName,
    required this.destinationName,
    required this.totalDistance,
    required this.totalDuration,
    this.tollFee = 0,
    this.steps = const [],
    this.trafficSegments = const [],
    this.cameras = const [],
    this.voiceGuidances = const [],
    this.routeType = 'fastest',
  });

  String get distanceText {
    if (totalDistance < 1) {
      return '${(totalDistance * 1000).toInt()}米';
    }
    return '${totalDistance.toStringAsFixed(1)}公里';
  }

  String get durationText {
    if (totalDuration < 60) {
      return '$totalDuration分钟';
    }
    final hours = totalDuration ~/ 60;
    final minutes = totalDuration % 60;
    if (minutes == 0) return '$hours小时';
    return '$hours小时$minutes分钟';
  }
}

该模型包含了导航路线的全部核心信息:起终点坐标、总距离/时长、分段导航指令、沿途交通路况、电子眼信息以及语音播报提示。distanceTextdurationText 计算属性将原始数据转换为用户友好的显示格式。

四、导航服务层实现

服务层是整个应用的业务逻辑核心。我们采用单例模式实现 NavigationService,提供路线规划、POI 搜索、收藏管理等功能:

class NavigationService {
  static final NavigationService _instance = NavigationService._();
  factory NavigationService() => _instance;
  NavigationService._();

  NavigationRoute planRoute(GeoPoint origin, GeoPoint destination,
      {String routeType = 'fastest'}) {
    final distance = _calculateDistance(
        origin.latitude, origin.longitude,
        destination.latitude, destination.longitude);
    final duration = (distance / 40 * 60).toInt();
    final steps = _generateRouteSteps(origin, destination, distance);
    final trafficSegments = _generateTrafficSegments(
        origin, destination, distance);
    final cameras = _generateCameras(origin, destination, distance);
    final voiceGuidances = _generateVoiceGuidances(steps);

    return NavigationRoute(
      id: 'R${DateTime.now().millisecondsSinceEpoch}',
      name: routeType == 'fastest' ? '最快路线' : '推荐路线',
      origin: origin,
      destination: destination,
      originName: '我的位置',
      destinationName: '目的地',
      totalDistance: distance,
      totalDuration: duration,
      steps: steps,
      trafficSegments: trafficSegments,
      cameras: cameras,
      voiceGuidances: voiceGuidances,
      routeType: routeType,
    );
  }

  double _calculateDistance(
      double lat1, double lng1, double lat2, double lng2) {
    const double r = 6371;
    final dLat = _toRadians(lat2 - lat1);
    final dLng = _toRadians(lng2 - lng1);
    final a = sin(dLat / 2) * sin(dLat / 2) +
        cos(_toRadians(lat1)) * cos(_toRadians(lat2)) *
        sin(dLng / 2) * sin(dLng / 2);
    final c = 2 * atan2(sqrt(a), sqrt(1 - a));
    return double.parse((r * c).toStringAsFixed(1));
  }
}

路线规划的核心是 Haversine 公式,用于计算地球表面两点间的球面距离。在此基础上,我们生成分段导航指令、模拟交通路况和电子眼分布,为导航体验提供完整的数据支撑。

五、实时导航页面实现

实时导航页面是应用的核心交互界面。它包含路线选择、导航过程控制、语音播报和电子眼提醒等功能:

class NavigationRoutePage extends StatefulWidget {
  final GeoPoint origin;
  final GeoPoint destination;
  final String destinationName;

  const NavigationRoutePage({
    super.key,
    required this.origin,
    required this.destination,
    required this.destinationName,
  });

  
  State<NavigationRoutePage> createState() => _NavigationRoutePageState();
}

class _NavigationRoutePageState extends State<NavigationRoutePage> {
  final _navigationService = NavigationService();
  late NavigationRoute _currentRoute;
  List<NavigationRoute> _allRoutes = [];
  int _selectedRouteIndex = 0;
  bool _isNavigating = false;
  int _currentStepIndex = 0;
  double _progress = 0;
  Timer? _navigationTimer;
  bool _voiceEnabled = true;
  bool _trafficOverlay = true;

  void _startNavigation() {
    setState(() {
      _isNavigating = true;
      _currentStepIndex = 0;
      _progress = 0;
    });

    _navigationTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
      if (!mounted) {
        timer.cancel();
        return;
      }
      setState(() {
        _progress += 0.04;
        if (_progress >= 1.0) {
          _progress = 1.0;
          _stopNavigation();
          _showArrivalDialog();
          return;
        }
        final stepProgress = _progress * _currentRoute.steps.length;
        final newStepIndex =
            stepProgress.floor().clamp(0, _currentRoute.steps.length - 1);
        if (newStepIndex != _currentStepIndex) {
          _currentStepIndex = newStepIndex;
          if (_voiceEnabled) {
            _triggerVoiceGuidance();
          }
        }
        _updateNearestCamera();
      });
    });
  }

  void _triggerVoiceGuidance() {
    if (_currentStepIndex < _currentRoute.steps.length) {
      final step = _currentRoute.steps[_currentStepIndex];
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Row(
            children: [
              const Icon(Icons.volume_up, color: Colors.white, size: 18),
              const SizedBox(width: 8),
              Expanded(
                child: Text(step.instruction,
                    style: const TextStyle(fontSize: 14)),
              ),
            ],
          ),
          duration: const Duration(seconds: 3),
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10)),
          backgroundColor: Colors.blueGrey.shade800,
        ),
      );
    }
  }
}

导航过程通过 Timer 定时器模拟位置更新,每 2 秒推进一次进度。当进入新的导航步骤时,自动触发语音播报提示。同时,系统会实时检测前方电子眼,当距离电子眼小于 500 米时弹出提醒。

六、POI 搜索与收藏管理

POI 搜索功能支持关键词检索和分类浏览,覆盖美食、加油站、停车场、医院等 7 大类别:

class POISearchPage extends StatefulWidget {
  final POICategory? initialCategory;
  const POISearchPage({super.key, this.initialCategory});
  // ...
}

// 搜索核心逻辑
List<POIInfo> searchPOIs(String keyword, {POICategory? category}) {
  final query = keyword.toLowerCase();
  return _pois.where((poi) {
    final matchKeyword =
        poi.name.toLowerCase().contains(query) ||
        poi.address.toLowerCase().contains(query);
    final matchCategory =
        category == null || poi.category == category;
    return matchKeyword && matchCategory;
  }).toList();
}

收藏地点管理支持添加、删除、标签分类和滑动删除操作,方便用户快速访问常用地点:

Widget _buildFavoriteCard(FavoriteLocation fav, ThemeData theme) {
  return Dismissible(
    key: Key(fav.id),
    direction: DismissDirection.endToStart,
    background: Container(
      alignment: Alignment.centerRight,
      padding: const EdgeInsets.only(right: 20),
      margin: const EdgeInsets.only(bottom: 10),
      decoration: BoxDecoration(
        color: Colors.red,
        borderRadius: BorderRadius.circular(14),
      ),
      child: const Icon(Icons.delete, color: Colors.white),
    ),
    onDismissed: (_) {
      _navigationService.removeFavoriteLocation(fav.id);
      setState(() {});
    },
    child: Container(
      margin: const EdgeInsets.only(bottom: 10),
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(14),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.05),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      // ... 收藏卡片 UI
    ),
  );
}

七、离线地图下载

离线地图功能让用户在没有网络的情况下也能使用导航。我们模拟了下载进度管理,支持开始下载、取消下载、删除已下载城市等操作:

void _startDownload(String cityId) {
  _navigationService.startDownloadCity(cityId);
  _downloadingCityId = cityId;
  setState(() {});

  double progress = 0;
  _downloadTimer = Timer.periodic(
    const Duration(milliseconds: 300),
    (timer) {
      progress += 0.05;
      if (progress >= 1.0) {
        progress = 1.0;
        timer.cancel();
        _downloadingCityId = null;
      }
      _navigationService.updateDownloadProgress(cityId, progress);
      if (mounted) setState(() {});
    },
  );
}

八、运行效果截图

以下是在鸿蒙设备上运行导航地图应用的实际效果截图:

截图一:导航首页

在这里插入图片描述

应用首页展示地图背景、搜索入口、快捷分类和底部功能面板

截图二:路线规划

在这里插入图片描述

多路线方案选择,展示距离、时长和费用对比

截图三:实时导航

在这里插入图片描述

导航过程中显示剩余距离、预计到达时间和转向指令

截图四:POI搜索

在这里插入图片描述

搜索附近地点,支持分类浏览和关键词检索

截图五:离线地图管理

在这里插入图片描述

城市列表展示、下载进度管理和存储空间统计

截图六:收藏地点

在这里插入图片描述

收藏地点列表,支持添加、删除和标签分类

九、总结

本文基于 Flutter for OpenHarmony 跨平台技术,实现了一个功能完整的导航地图应用。通过分层架构设计、数据模型抽象和服务封装,我们成功在鸿蒙设备上实现了实时导航路线、语音播报提示、电子眼提醒、离线地图下载、实时路况显示、POI 搜索、收藏地点管理和导航路线分享等 8 大核心功能。

Flutter for OpenHarmony 为开发者提供了强大的跨平台开发能力,一套代码即可同时运行在 Android、iOS 和 OpenHarmony 设备上,大幅降低了开发成本。随着 OpenHarmony 生态的不断完善,Flutter 跨平台方案将在鸿蒙应用开发中发挥越来越重要的作用。

完整项目源码已托管至 AtomGit:https://atomgit.com,欢迎开发者下载体验。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐