Flutter for OpenHarmony 公交地铁应用开发实战

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

一、引言

随着开源鸿蒙生态的快速发展,Flutter 作为一套高性能的跨平台 UI 框架,在 OpenHarmony 上的适配日趋成熟。本文将带领读者从零构建一个功能完整的公交地铁出行应用,涵盖线路查询、实时到站预报、换乘方案推荐、收藏线路、扫码乘车、公交卡余额查询、拥挤度显示和路线规划八大核心功能。

本文所有代码均基于 Flutter 框架编写,使用 Dart 语言,并已通过验证可在 OpenHarmony 设备上正常运行。项目完整代码已托管至 AtomGit 平台,仓库链接为:https://atomgit.com。

二、应用架构设计

在开始编码之前,我们先梳理应用的架构。整体采用分层设计:

  • 数据模型层(Model):定义公交线路、站点、换乘方案等数据结构
  • 服务层(Service):提供数据获取和业务逻辑处理,采用单例模式
  • 页面层(Page):负责 UI 展示和用户交互

这种分层架构的好处是职责清晰、易于维护,服务层可以方便地从 Mock 数据切换到真实 API。

三、数据模型设计

数据模型是整个应用的基石。我们需要定义公交线路、站点、实时到站信息、换乘方案、拥挤度等核心实体。以下是关键模型的定义:

// 公交/地铁类型
enum TransitType { bus, subway }

// 拥挤度等级
enum CrowdingLevel { empty, comfortable, normal, crowded, veryCrowded }

// 公交站点
class BusStation {
  final String id;
  final String name;
  final double latitude;
  final double longitude;
  final List<String> passingLines;

  const BusStation({
    required this.id,
    required this.name,
    required this.latitude,
    required this.longitude,
    this.passingLines = const [],
  });
}

// 公交线路
class BusLine {
  final String id;
  final String name;
  final TransitType type;
  final String startStation;
  final String endStation;
  final String firstTime;
  final String lastTime;
  final double price;
  final List<BusStation> stations;

  const BusLine({
    required this.id,
    required this.name,
    required this.type,
    required this.startStation,
    required this.endStation,
    required this.firstTime,
    required this.lastTime,
    required this.price,
    required this.stations,
  });
}

// 实时到站信息
class RealTimeArrival {
  final String lineId;
  final String lineName;
  final String stationName;
  final List<ArrivalInfo> arrivals;
  final DateTime updateTime;

  const RealTimeArrival({
    required this.lineId,
    required this.lineName,
    required this.stationName,
    required this.arrivals,
    required this.updateTime,
  });
}

// 到站详情
class ArrivalInfo {
  final String busNumber;
  final int remainingStations;
  final int estimatedMinutes;
  final double distance;
  final CrowdingLevel crowding;

  const ArrivalInfo({
    required this.busNumber,
    required this.remainingStations,
    required this.estimatedMinutes,
    required this.distance,
    this.crowding = CrowdingLevel.normal,
  });
}

模型设计遵循不可变原则,所有字段均为 final,配合 const 构造方法,确保数据安全性和性能优化。

四、服务层实现

服务层采用单例模式,内部维护 Mock 数据,模拟真实 API 的行为。这样做的好处是开发阶段无需依赖后端接口,后续切换到真实 API 只需替换服务层实现即可。

class TransitService {
  static final TransitService _instance = TransitService._();
  factory TransitService() => _instance;
  TransitService._() { _initMockData(); }

  final List<BusLine> _busLines = [];
  final List<BusStation> _allStations = [];
  final List<FavoriteRoute> _favoriteRoutes = [];
  final List<TransitCard> _transitCards = [];
  final Map<String, List<RealTimeArrival>> _realTimeData = {};
  final Map<String, CrowdingInfo> _crowdingData = {};

  // 搜索公交线路
  List<BusLine> searchBusLines(String keyword) {
    if (keyword.isEmpty) return _busLines;
    final kw = keyword.toLowerCase();
    return _busLines.where((line) {
      return line.name.toLowerCase().contains(kw) ||
          line.startStation.toLowerCase().contains(kw) ||
          line.endStation.toLowerCase().contains(kw) ||
          line.stations.any((s) => s.name.toLowerCase().contains(kw));
    }).toList();
  }

  // 获取实时到站信息
  List<RealTimeArrival> getRealTimeArrival(String lineId, String stationId) {
    final key = '${lineId}_$stationId';
    _refreshRealTimeData(lineId, stationId);
    return _realTimeData[key] ?? [];
  }

  // 获取换乘方案
  List<TransferPlan> getTransferPlans(String from, String to) {
    // 返回多种方案:直达、少换乘、最省钱
    return [
      TransferPlan(id: 'TP001', fromStation: from, toStation: to,
          totalDuration: 25, totalPrice: 4.0, transferCount: 0,
          tags: ['直达', '推荐'], segments: [...]),
      TransferPlan(id: 'TP002', fromStation: from, toStation: to,
          totalDuration: 35, totalPrice: 3.0, transferCount: 1,
          tags: ['少换乘'], segments: [...]),
    ];
  }

  // 获取拥挤度信息
  CrowdingInfo? getCrowdingInfo(String lineId, String stationId) {
    final key = '${lineId}_$stationId';
    _refreshCrowdingData(lineId, stationId);
    return _crowdingData[key];
  }
}

服务层中特别实现了实时数据的动态刷新机制。每次调用 getRealTimeArrival 时,到站时间会递减,模拟车辆不断接近站点的真实场景;拥挤度数据也会随机波动,模拟客流变化。

五、页面实现

5.1 主页面

主页面采用卡片式布局,顶部是路线查询卡片,中间是功能服务网格,下方展示附近线路和常用路线。

class TransitHomePage extends StatefulWidget {
  
  State<TransitHomePage> createState() => _TransitHomePageState();
}

class _TransitHomePageState extends State<TransitHomePage> {
  final _transitService = TransitService();
  final _fromController = TextEditingController();
  final _toController = TextEditingController();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('公交地铁')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            _buildSearchCard(context),
            _buildQuickServices(context),
            _buildNearbyLines(context),
            _buildFavoriteRoutes(context),
          ],
        ),
      ),
    );
  }
}

5.2 实时到站页面

实时到站页面是用户最常用的功能之一。用户选择线路和站点后,页面会显示即将到站的车辆信息,包括剩余站数、预计分钟数和拥挤度。

Widget _buildArrivalCard(ArrivalInfo info) {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(14),
      child: Row(
        children: [
          // 预计时间
          Container(
            width: 50, height: 50,
            decoration: BoxDecoration(
              color: info.estimatedMinutes <= 3
                  ? Colors.red.withOpacity(0.1)
                  : Colors.green.withOpacity(0.1),
              borderRadius: BorderRadius.circular(10),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('${info.estimatedMinutes}',
                    style: TextStyle(fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: info.estimatedMinutes <= 3
                            ? Colors.red : Colors.green)),
                const Text('分钟', style: TextStyle(fontSize: 10)),
              ],
            ),
          ),
          const SizedBox(width: 12),
          // 车辆信息和标签
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(info.busNumber,
                    style: const TextStyle(fontSize: 15,
                        fontWeight: FontWeight.w600)),
                const SizedBox(height: 4),
                Row(
                  children: [
                    _buildTag('距${info.remainingStations}站', Colors.blue),
                    const SizedBox(width: 6),
                    _buildTag('${info.distance.toStringAsFixed(1)}km',
                        Colors.grey),
                    const SizedBox(width: 6),
                    _buildTag(info.crowding.label, info.crowding.levelColor),
                  ],
                ),
              ],
            ),
          ),
          // 拥挤度图标
          Icon(info.crowding.icon, color: info.crowding.levelColor, size: 28),
        ],
      ),
    ),
  );
}

页面每隔 10 秒自动刷新数据,用户无需手动刷新即可获取最新到站信息。

5.3 换乘方案推荐

换乘方案推荐是出行规划的核心功能。系统会根据出发站和目的站,生成多种换乘方案供用户选择。

Widget _buildPlanCard(TransferPlan plan, int index) {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 方案标签
          Row(
            children: [
              if (index == 0)
                Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 8, vertical: 3),
                  decoration: BoxDecoration(
                    color: Colors.orange,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: const Text('推荐',
                      style: TextStyle(color: Colors.white,
                          fontSize: 11, fontWeight: FontWeight.bold)),
                ),
              ...plan.tags.map((tag) => Padding(
                padding: const EdgeInsets.only(right: 6),
                child: Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 8, vertical: 3),
                  decoration: BoxDecoration(
                    color: Colors.blue.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: Text(tag, style: const TextStyle(fontSize: 11)),
                ),
              )),
            ],
          ),
          const SizedBox(height: 12),
          // 统计信息
          Row(
            children: [
              _buildStatBox(Icons.timer, '${plan.totalDuration}分钟',
                  '预计耗时', Colors.orange),
              _buildStatBox(Icons.attach_money,
                  ${plan.totalPrice.toStringAsFixed(1)}',
                  '预计费用', Colors.green),
              _buildStatBox(Icons.transfer_within_a_station,
                  '${plan.transferCount}次', '换乘次数', Colors.blue),
            ],
          ),
        ],
      ),
    ),
  );
}

每个方案都清晰展示了耗时、费用和换乘次数,并标注"推荐"、“直达”、"最省钱"等标签,帮助用户快速决策。

5.4 拥挤度实时显示

拥挤度页面通过进度条和颜色直观展示各站点的客流情况,数据每 8 秒自动刷新。

Widget _buildCrowdingCard(CrowdingInfo info) {
  final rate = info.occupancyRate;
  final color = info.levelColor;

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(14),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Text(info.stationName,
                  style: const TextStyle(fontSize: 15,
                      fontWeight: FontWeight.w600)),
              const Spacer(),
              Icon(info.level.icon, color: color, size: 20),
              const SizedBox(width: 4),
              Text(info.levelText,
                  style: TextStyle(fontSize: 13,
                      fontWeight: FontWeight.w600, color: color)),
            ],
          ),
          const SizedBox(height: 10),
          // 拥挤度进度条
          ClipRRect(
            borderRadius: BorderRadius.circular(6),
            child: LinearProgressIndicator(
              value: rate,
              backgroundColor: Colors.grey.shade200,
              valueColor: AlwaysStoppedAnimation<Color>(color),
              minHeight: 10,
            ),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Text('${info.passengerCount}/${info.capacity}人'),
              Text('${(rate * 100).toStringAsFixed(1)}%'),
              const Spacer(),
              _buildTrendChip(info.trend),
            ],
          ),
        ],
      ),
    ),
  );
}

拥挤度分为五个等级:空闲(绿色)、舒适(浅绿)、一般(黄色)、拥挤(橙色)、非常拥挤(红色),并配有趋势箭头指示客流变化方向。

六、运行效果截图

以下是公交地铁应用在 OpenHarmony 设备上的运行效果截图:

6.1 主页面

主页面展示了路线查询入口、八大功能服务网格、附近线路列表和常用路线收藏,布局清晰,操作便捷。
在这里插入图片描述

6.2 线路查询页面

用户输入线路名称或站点关键词即可搜索,点击线路可查看详细信息,包括首末班时间、票价、站点列表等。
在这里插入图片描述

6.3 实时到站页面

选择线路和站点后,页面实时显示即将到站的车辆信息,包括预计到达时间、剩余站数和拥挤度,数据自动刷新。
在这里插入图片描述

6.4 换乘方案页面

输入出发站和目的地后,系统生成多种换乘方案,支持一键收藏常用路线。
在这里插入图片描述

6.5 拥挤度页面

以进度条形式直观展示全线各站点的拥挤程度,帮助用户选择更舒适的出行时段。
在这里插入图片描述

6.6 公交卡页面

支持多卡管理,展示余额和交易记录,提供充值入口。
在这里插入图片描述

6.7 扫码乘车页面

选择线路和站点后生成乘车二维码,5 分钟有效期内可扫码过闸。
在这里插入图片描述

6.8 路线规划页面

支持公交、地铁、打车、步行、骑行多种出行方式对比,提供详细的路线指引。
在这里插入图片描述

七、总结

本文详细介绍了如何使用 Flutter for OpenHarmony 开发一个功能完整的公交地铁出行应用。通过分层架构设计、不可变数据模型、单例服务模式和 Material Design 3 风格 UI,我们实现了八大核心功能。

Flutter 在 OpenHarmony 上的表现令人满意,无论是页面切换流畅度还是数据刷新响应速度,都能满足日常使用需求。开发者可以基于本文的代码框架,快速接入真实 API 数据,打造生产级别的公交地铁应用。

项目完整代码已托管至 AtomGit 平台,欢迎访问 https://atomgit.com 获取源码。也欢迎加入开源鸿蒙跨平台社区(https://openharmonycrossplatform.csdn.net)交流讨论,共同推动 Flutter for OpenHarmony 生态发展。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐