Flutter 框架跨平台鸿蒙开发 - 共享单车位置查询:智能定位附近车辆
共享单车位置查询是一款专为骑行用户打造的Flutter应用,帮助用户快速找到附近的共享单车,实时查看车辆状态和骑行费用。通过智能筛选和城市切换功能,让出行变得更加便捷高效。运行效果图模型字段说明:支持城市列表:模型字段说明:计算属性:品牌与颜色映射:车辆类型:车辆状态:模型字段说明:费用计算方法:计费规则示例:数据生成逻辑:筛选条件:筛选流程:交互设计:StatefulBuilder的使用:卡片布
Flutter共享单车位置查询:智能定位附近车辆
项目简介
共享单车位置查询是一款专为骑行用户打造的Flutter应用,帮助用户快速找到附近的共享单车,实时查看车辆状态和骑行费用。通过智能筛选和城市切换功能,让出行变得更加便捷高效。
运行效果图




核心功能
- 城市选择:支持8个主要城市切换
- 车辆查询:显示附近共享单车列表
- 实时状态:查看车辆可用/使用中/维护中状态
- 品牌筛选:美团、哈啰、青桔三大品牌
- 类型筛选:单车、电单车、助力车分类
- 距离筛选:自定义最大搜索距离
- 电量显示:电动车辆实时电量查看
- 费用查询:详细的计费规则说明
- 费用预估:不同时长的费用计算
- 数据刷新:实时更新车辆状态
- 详情查看:单车详细信息页面
技术特点
- Material Design 3设计风格
- 多城市数据模拟
- 智能筛选排序
- 费用自动计算
- 响应式卡片布局
- ModalBottomSheet交互
- 实时状态更新
- 模拟数据生成
核心代码实现
1. 城市数据模型
class City {
String name; // 城市名称
String code; // 城市代码
double latitude; // 纬度
double longitude; // 经度
City({
required this.name,
required this.code,
required this.latitude,
required this.longitude,
});
Map<String, dynamic> toJson() => {
'name': name,
'code': code,
'latitude': latitude,
'longitude': longitude,
};
factory City.fromJson(Map<String, dynamic> json) => City(
name: json['name'],
code: json['code'],
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
);
}
模型字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| name | String | 城市名称 |
| code | String | 城市代码(如BJ、SH) |
| latitude | double | 纬度坐标 |
| longitude | double | 经度坐标 |
支持城市列表:
- 北京(BJ):39.9042, 116.4074
- 上海(SH):31.2304, 121.4737
- 广州(GZ):23.1291, 113.2644
- 深圳(SZ):22.5431, 114.0579
- 杭州(HZ):30.2741, 120.1551
- 成都(CD):30.5728, 104.0668
- 武汉(WH):30.5928, 114.3055
- 西安(XA):34.3416, 108.9398
2. 共享单车数据模型
class SharedBike {
String id; // 车辆编号
String brand; // 品牌
String type; // 类型
double latitude; // 纬度
double longitude; // 经度
double battery; // 电量
String status; // 状态
double distance; // 距离
String address; // 地址
// 品牌颜色
Color get brandColor {
switch (brand) {
case '美团单车':
return Colors.yellow[700]!;
case '哈啰单车':
return Colors.blue;
case '青桔单车':
return Colors.green;
default:
return Colors.grey;
}
}
// 类型图标
IconData get typeIcon {
switch (type) {
case '单车':
return Icons.pedal_bike;
case '电单车':
return Icons.electric_bike;
case '助力车':
return Icons.electric_scooter;
default:
return Icons.directions_bike;
}
}
// 状态文本
String get statusText {
switch (status) {
case 'available':
return '可用';
case 'in_use':
return '使用中';
case 'maintenance':
return '维护中';
default:
return '未知';
}
}
// 状态颜色
Color get statusColor {
switch (status) {
case 'available':
return Colors.green;
case 'in_use':
return Colors.orange;
case 'maintenance':
return Colors.red;
default:
return Colors.grey;
}
}
}
模型字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 唯一标识符 |
| brand | String | 品牌名称 |
| type | String | 车辆类型 |
| latitude | double | 纬度坐标 |
| longitude | double | 经度坐标 |
| battery | double | 电量百分比 |
| status | String | 车辆状态 |
| distance | double | 距离(km) |
| address | String | 详细地址 |
计算属性:
brandColor:根据品牌返回对应颜色typeIcon:根据类型返回对应图标statusText:返回状态中文描述statusColor:返回状态对应颜色
品牌与颜色映射:
- 美团单车:黄色(Colors.yellow[700])
- 哈啰单车:蓝色(Colors.blue)
- 青桔单车:绿色(Colors.green)
车辆类型:
- 单车:普通自行车,无电量显示
- 电单车:电动自行车,显示电量
- 助力车:电动助力车,显示电量
车辆状态:
- available:可用(绿色)
- in_use:使用中(橙色)
- maintenance:维护中(红色)
3. 骑行费用数据模型
class RidingPrice {
String brand; // 品牌
String type; // 类型
double startPrice; // 起步价
int freeMinutes; // 免费时长
double pricePerMinute; // 每分钟价格
double pricePerHour; // 每小时价格
String description; // 描述
// 计算费用
double calculateCost(int minutes) {
if (minutes <= freeMinutes) {
return startPrice;
}
final extraMinutes = minutes - freeMinutes;
return startPrice + (extraMinutes * pricePerMinute);
}
}
模型字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| brand | String | 品牌名称 |
| type | String | 车辆类型 |
| startPrice | double | 起步价(元) |
| freeMinutes | int | 免费时长(分钟) |
| pricePerMinute | double | 超时单价(元/分钟) |
| pricePerHour | double | 小时单价(元/小时) |
| description | String | 计费说明 |
费用计算方法:
double calculateCost(int minutes) {
if (minutes <= freeMinutes) {
return startPrice;
}
final extraMinutes = minutes - freeMinutes;
return startPrice + (extraMinutes * pricePerMinute);
}
计费规则示例:
| 品牌 | 类型 | 起步价 | 免费时长 | 超时单价 | 小时单价 |
|---|---|---|---|---|---|
| 美团单车 | 单车 | ¥1.5 | 15分钟 | ¥0.5/分钟 | ¥3.0/小时 |
| 美团单车 | 电单车 | ¥2.0 | 20分钟 | ¥1.0/分钟 | ¥5.0/小时 |
| 哈啰单车 | 单车 | ¥1.5 | 15分钟 | ¥0.5/分钟 | ¥3.0/小时 |
| 哈啰单车 | 电单车 | ¥2.5 | 20分钟 | ¥1.0/分钟 | ¥5.0/小时 |
| 青桔单车 | 单车 | ¥1.5 | 15分钟 | ¥0.5/分钟 | ¥3.0/小时 |
| 青桔单车 | 电单车 | ¥2.0 | 20分钟 | ¥1.0/分钟 | ¥5.0/小时 |
4. 车辆数据生成
void _generateBikes() {
if (_selectedCity == null) return;
final random = Random();
final brands = ['美团单车', '哈啰单车', '青桔单车'];
final types = ['单车', '电单车', '助力车'];
final statuses = ['available', 'in_use', 'maintenance'];
_bikes = List.generate(50, (index) {
final brand = brands[random.nextInt(brands.length)];
final type = types[random.nextInt(types.length)];
// 70%概率为可用状态
final status = statuses[
random.nextInt(10) < 7 ? 0 : random.nextInt(statuses.length)];
// 在城市中心附近随机生成坐标
final lat = _selectedCity!.latitude + (random.nextDouble() - 0.5) * 0.05;
final lng = _selectedCity!.longitude + (random.nextDouble() - 0.5) * 0.05;
final distance = random.nextDouble() * 3;
return SharedBike(
id: '${brand[0]}${type[0]}${(index + 1).toString().padLeft(4, '0')}',
brand: brand,
type: type,
latitude: lat,
longitude: lng,
battery: type == '单车' ? 100 : (50 + random.nextDouble() * 50),
status: status,
distance: distance,
address: '${_selectedCity!.name}市某某区某某路${index + 1}号附近',
);
});
_applyFilters();
}
数据生成逻辑:
- 每个城市生成50辆共享单车
- 品牌随机分配(美团、哈啰、青桔)
- 类型随机分配(单车、电单车、助力车)
- 70%概率为可用状态
- 坐标在城市中心±0.05度范围内
- 距离在0-3公里范围内
- 单车电量固定100%,电动车50-100%
- 车辆编号格式:品牌首字母+类型首字母+4位数字
5. 智能筛选功能
void _applyFilters() {
setState(() {
_filteredBikes = _bikes.where((bike) {
// 仅显示可用车辆
if (_onlyAvailable && bike.status != 'available') {
return false;
}
// 品牌筛选
if (_filterBrand != '全部' && bike.brand != _filterBrand) {
return false;
}
// 类型筛选
if (_filterType != '全部' && bike.type != _filterType) {
return false;
}
// 距离筛选
if (bike.distance > _maxDistance) {
return false;
}
return true;
}).toList();
// 按距离排序
_filteredBikes.sort((a, b) => a.distance.compareTo(b.distance));
});
}
筛选条件:
| 筛选项 | 选项 | 说明 |
|---|---|---|
| 品牌 | 全部/美团/哈啰/青桔 | 按品牌筛选 |
| 类型 | 全部/单车/电单车/助力车 | 按类型筛选 |
| 距离 | 0.5-5.0km | 滑动条选择 |
| 可用性 | 开关 | 仅显示可用车辆 |
筛选流程:
- 检查可用性开关
- 检查品牌匹配
- 检查类型匹配
- 检查距离范围
- 按距离升序排序
6. 城市选择对话框
void _selectCity() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择城市',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: _cities.map((city) {
return ChoiceChip(
label: Text(city.name),
selected: _selectedCity?.code == city.code,
onSelected: (selected) {
setState(() {
_selectedCity = city;
_generateBikes();
});
Navigator.pop(context);
},
);
}).toList(),
),
],
),
),
);
}
交互设计:
- 使用ModalBottomSheet展示城市列表
- ChoiceChip实现单选效果
- 选中城市后自动关闭对话框
- 重新生成该城市的车辆数据
7. 筛选对话框
void _showFilterDialog() {
showModalBottomSheet(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setModalState) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('筛选条件', style: TextStyle(fontSize: 18)),
// 品牌筛选
Wrap(
spacing: 8,
children: ['全部', '美团单车', '哈啰单车', '青桔单车'].map((brand) {
return ChoiceChip(
label: Text(brand),
selected: _filterBrand == brand,
onSelected: (selected) {
setModalState(() => _filterBrand = brand);
setState(() => _filterBrand = brand);
_applyFilters();
},
);
}).toList(),
),
// 类型筛选
Wrap(
spacing: 8,
children: ['全部', '单车', '电单车', '助力车'].map((type) {
return ChoiceChip(
label: Text(type),
selected: _filterType == type,
onSelected: (selected) {
setModalState(() => _filterType = type);
setState(() => _filterType = type);
_applyFilters();
},
);
}).toList(),
),
// 距离滑块
Slider(
value: _maxDistance,
min: 0.5,
max: 5.0,
divisions: 9,
label: '${_maxDistance.toStringAsFixed(1)}km',
onChanged: (value) {
setModalState(() => _maxDistance = value);
setState(() => _maxDistance = value);
_applyFilters();
},
),
// 可用性开关
SwitchListTile(
title: const Text('仅显示可用车辆'),
value: _onlyAvailable,
onChanged: (value) {
setModalState(() => _onlyAvailable = value);
setState(() => _onlyAvailable = value);
_applyFilters();
},
),
],
),
),
),
);
}
StatefulBuilder的使用:
- 在ModalBottomSheet中实现局部状态更新
- setModalState更新对话框内部UI
- setState更新主页面数据
- 实时应用筛选条件
8. 车辆卡片UI
Widget _buildBikeCard(SharedBike bike) {
final price = _prices.firstWhere(
(p) => p.brand == bike.brand && p.type == bike.type,
orElse: () => _prices.first,
);
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BikeDetailPage(bike: bike, price: price),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 车辆基本信息
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: bike.brandColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
bike.typeIcon,
color: bike.brandColor,
size: 32,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
bike.brand,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: bike.brandColor,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
bike.type,
style: const TextStyle(fontSize: 12),
),
),
],
),
const SizedBox(height: 4),
Text(
bike.id,
style: const TextStyle(color: Colors.grey),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: bike.statusColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
bike.statusText,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
// 距离和时间信息
Row(
children: [
const Icon(Icons.navigation, size: 16, color: Colors.blue),
const SizedBox(width: 4),
Text(
'${(bike.distance * 1000).toStringAsFixed(0)}米',
style: const TextStyle(color: Colors.blue),
),
const SizedBox(width: 16),
const Icon(Icons.access_time, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Text(
'步行约${(bike.distance * 12).toStringAsFixed(0)}分钟',
style: const TextStyle(color: Colors.grey),
),
],
),
const SizedBox(height: 8),
// 地址信息
Row(
children: [
const Icon(Icons.location_on, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
bike.address,
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
],
),
// 电量信息(仅电动车)
if (bike.type != '单车') ...[
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.battery_charging_full,
size: 16, color: Colors.green),
const SizedBox(width: 4),
Expanded(
child: LinearProgressIndicator(
value: bike.battery / 100,
backgroundColor: Colors.grey[200],
color: bike.battery > 50 ? Colors.green : Colors.orange,
),
),
const SizedBox(width: 8),
Text(
'${bike.battery.toStringAsFixed(0)}%',
style: TextStyle(
color: bike.battery > 50 ? Colors.green : Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
),
],
const SizedBox(height: 12),
// 费用信息
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'起步价 ¥${price.startPrice.toStringAsFixed(1)}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'前${price.freeMinutes}分钟',
style: const TextStyle(color: Colors.grey),
),
Text(
'¥${price.pricePerMinute.toStringAsFixed(1)}/分钟',
style: const TextStyle(color: Colors.red),
),
],
),
),
],
),
),
),
);
}
卡片布局结构:
- 顶部:品牌图标、名称、类型、状态标签
- 中部:距离、步行时间、地址信息
- 电量:电动车显示电量进度条
- 底部:费用信息卡片
视觉设计要点:
- 品牌颜色作为主题色
- 状态标签使用对应颜色
- 电量进度条颜色根据电量变化
- 费用信息使用浅蓝色背景突出显示
9. 汇总卡片
Widget _buildSummaryCard(int availableCount) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem(
Icons.pedal_bike,
'附近车辆',
'${_filteredBikes.length}辆',
),
_buildSummaryItem(
Icons.check_circle,
'可用',
'$availableCount辆',
color: Colors.green,
),
_buildSummaryItem(
Icons.navigation,
'最近',
_filteredBikes.isEmpty
? '-'
: '${_filteredBikes.first.distance.toStringAsFixed(0)}m',
color: Colors.blue,
),
],
),
),
);
}
Widget _buildSummaryItem(IconData icon, String label, String value,
{Color? color}) {
return Column(
children: [
Icon(icon, size: 32, color: color ?? Colors.grey),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
汇总信息:
- 附近车辆:筛选后的总数量
- 可用车辆:状态为available的数量
- 最近距离:第一辆车的距离(已排序)
10. 单车详情页
class BikeDetailPage extends StatelessWidget {
final SharedBike bike;
final RidingPrice price;
const BikeDetailPage({
super.key,
required this.bike,
required this.price,
});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${bike.brand} - ${bike.id}'),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBikeInfo(),
_buildLocationInfo(),
_buildPriceInfo(),
_buildCostCalculator(),
],
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: FilledButton.icon(
onPressed: bike.status == 'available'
? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('开始骑行')),
);
}
: null,
icon: const Icon(Icons.pedal_bike),
label: Text(bike.status == 'available' ? '开始骑行' : bike.statusText),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
),
);
}
}
详情页结构:
- 车辆信息卡片:品牌、类型、编号、状态、电量
- 位置信息卡片:距离、步行时间、详细地址
- 费用信息卡片:起步价、免费时长、计费规则
- 费用预估卡片:15/30/60分钟费用示例
- 底部按钮:开始骑行(仅可用状态)
11. 费用列表页
class PriceListPage extends StatelessWidget {
final List<RidingPrice> prices;
const PriceListPage({super.key, required this.prices});
Widget build(BuildContext context) {
final groupedPrices = <String, List<RidingPrice>>{};
for (var price in prices) {
if (!groupedPrices.containsKey(price.brand)) {
groupedPrices[price.brand] = [];
}
groupedPrices[price.brand]!.add(price);
}
return Scaffold(
appBar: AppBar(
title: const Text('骑行费用'),
),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: groupedPrices.length,
itemBuilder: (context, index) {
final brand = groupedPrices.keys.elementAt(index);
final brandPrices = groupedPrices[brand]!;
return _buildBrandCard(brand, brandPrices);
},
),
);
}
}
费用页面设计:
- 按品牌分组显示
- 每个品牌显示所有车型费用
- 包含起步价、免费时长、超时计费
- 显示计费说明文字
12. 数据刷新功能
void _refreshBikes() {
setState(() {
final random = Random();
for (var bike in _bikes) {
// 30%概率更新状态
if (random.nextDouble() < 0.3) {
final statuses = ['available', 'in_use', 'maintenance'];
bike.status = statuses[
random.nextInt(10) < 7 ? 0 : random.nextInt(statuses.length)];
}
// 20%概率更新电量
if (bike.type != '单车' && random.nextDouble() < 0.2) {
bike.battery =
(bike.battery + random.nextDouble() * 10 - 5).clamp(0, 100);
}
}
});
_applyFilters();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('数据已刷新')),
);
}
刷新逻辑:
- 30%概率更新车辆状态
- 20%概率更新电动车电量
- 电量变化±5%,范围0-100%
- 刷新后重新应用筛选条件
- 显示刷新成功提示
技术要点详解
1. ModalBottomSheet的使用
ModalBottomSheet是Flutter中常用的底部弹出对话框组件,适合展示选项列表和筛选条件。
基本用法:
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 内容
],
),
),
);
关键属性:
context:上下文对象builder:构建器函数isScrollControlled:是否可滚动shape:形状定义backgroundColor:背景颜色
使用场景:
- 城市选择
- 筛选条件设置
- 快速操作菜单
2. ChoiceChip的使用
ChoiceChip是Material Design中的选择芯片组件,适合单选或多选场景。
基本用法:
ChoiceChip(
label: Text('选项'),
selected: _selectedValue == '选项',
onSelected: (selected) {
setState(() {
_selectedValue = '选项';
});
},
)
关键属性:
label:标签文本selected:是否选中onSelected:选择回调selectedColor:选中颜色backgroundColor:背景颜色
使用场景:
- 城市选择
- 品牌筛选
- 类型筛选
3. Slider滑块组件
Slider用于在范围内选择数值,适合距离、价格等连续值的选择。
基本用法:
Slider(
value: _currentValue,
min: 0.5,
max: 5.0,
divisions: 9,
label: '${_currentValue.toStringAsFixed(1)}km',
onChanged: (value) {
setState(() {
_currentValue = value;
});
},
)
关键属性:
value:当前值min:最小值max:最大值divisions:分段数label:标签文本onChanged:值变化回调
使用场景:
- 距离筛选
- 价格范围
- 时间选择
4. LinearProgressIndicator进度条
LinearProgressIndicator用于显示线性进度,适合电量、进度等百分比显示。
基本用法:
LinearProgressIndicator(
value: 0.75,
backgroundColor: Colors.grey[200],
color: Colors.green,
minHeight: 8,
)
关键属性:
value:进度值(0-1)backgroundColor:背景颜色color:进度颜色minHeight:最小高度
动态颜色:
color: bike.battery > 50 ? Colors.green : Colors.orange,
使用场景:
- 电量显示
- 下载进度
- 任务完成度
5. 计算属性的使用
计算属性(getter)可以根据对象状态动态返回值,避免存储冗余数据。
示例:
class SharedBike {
String status;
String get statusText {
switch (status) {
case 'available':
return '可用';
case 'in_use':
return '使用中';
case 'maintenance':
return '维护中';
default:
return '未知';
}
}
Color get statusColor {
switch (status) {
case 'available':
return Colors.green;
case 'in_use':
return Colors.orange;
case 'maintenance':
return Colors.red;
default:
return Colors.grey;
}
}
}
优势:
- 减少数据冗余
- 保持数据一致性
- 简化代码逻辑
- 便于维护
6. 列表筛选与排序
使用Dart的集合操作实现高效的数据筛选和排序。
筛选:
_filteredBikes = _bikes.where((bike) {
if (_onlyAvailable && bike.status != 'available') {
return false;
}
if (_filterBrand != '全部' && bike.brand != _filterBrand) {
return false;
}
if (_filterType != '全部' && bike.type != _filterType) {
return false;
}
if (bike.distance > _maxDistance) {
return false;
}
return true;
}).toList();
排序:
_filteredBikes.sort((a, b) => a.distance.compareTo(b.distance));
性能优化:
- 使用where进行链式筛选
- 避免多次遍历列表
- 在setState中更新状态
7. 费用计算算法
实现灵活的费用计算逻辑,支持起步价和超时计费。
计算公式:
总费用 = { 起步价 if t ≤ t free 起步价 + ( t − t free ) × p if t > t free \text{总费用} = \begin{cases} \text{起步价} & \text{if } t \leq t_{\text{free}} \\ \text{起步价} + (t - t_{\text{free}}) \times p & \text{if } t > t_{\text{free}} \end{cases} 总费用={起步价起步价+(t−tfree)×pif t≤tfreeif t>tfree
其中:
- t t t 为骑行时长(分钟)
- t free t_{\text{free}} tfree 为免费时长(分钟)
- p p p 为超时单价(元/分钟)
代码实现:
double calculateCost(int minutes) {
if (minutes <= freeMinutes) {
return startPrice;
}
final extraMinutes = minutes - freeMinutes;
return startPrice + (extraMinutes * pricePerMinute);
}
示例计算:
- 骑行15分钟:¥1.5(起步价)
- 骑行30分钟:¥1.5 + (30-15) × 0.5 = ¥9.0
- 骑行60分钟:¥1.5 + (60-15) × 0.5 = ¥24.0
8. 距离计算
根据经纬度坐标计算两点之间的距离。
简化计算(适用于短距离):
double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
final dLat = (lat2 - lat1) * 111.0; // 纬度1度约111km
final dLon = (lon2 - lon1) * 111.0 * cos(lat1 * pi / 180);
return sqrt(dLat * dLat + dLon * dLon);
}
Haversine公式(精确计算):
double haversineDistance(double lat1, double lon1, double lat2, double lon2) {
const R = 6371; // 地球半径(km)
final dLat = (lat2 - lat1) * pi / 180;
final dLon = (lon2 - lon1) * pi / 180;
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(lat1 * pi / 180) * cos(lat2 * pi / 180) *
sin(dLon / 2) * sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
步行时间估算:
// 假设步行速度为5km/h
final walkingMinutes = distance * 12; // 1km约12分钟
9. 状态管理
使用setState进行局部状态更新,保持UI与数据同步。
状态变量:
City? _selectedCity; // 选中城市
List<SharedBike> _bikes = []; // 所有车辆
List<SharedBike> _filteredBikes = []; // 筛选后车辆
String _filterBrand = '全部'; // 品牌筛选
String _filterType = '全部'; // 类型筛选
bool _onlyAvailable = false; // 仅显示可用
double _maxDistance = 2.0; // 最大距离
更新流程:
- 用户操作触发事件
- 调用setState更新状态
- 重新构建受影响的Widget
- UI自动刷新
最佳实践:
- 只在setState中修改状态
- 避免在build方法中修改状态
- 合理拆分Widget减少重建范围
10. 空状态处理
当筛选结果为空时,显示友好的空状态提示。
_filteredBikes.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pedal_bike,
size: 80, color: Colors.grey[300]),
const SizedBox(height: 16),
Text('附近暂无车辆',
style: TextStyle(color: Colors.grey[600])),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: () {
setState(() {
_filterBrand = '全部';
_filterType = '全部';
_onlyAvailable = false;
_maxDistance = 5.0;
});
_applyFilters();
},
icon: const Icon(Icons.refresh),
label: const Text('重置筛选'),
),
],
),
)
: ListView.builder(...)
空状态设计要点:
- 使用大图标吸引注意
- 提供清晰的说明文字
- 给出解决方案(重置筛选)
- 保持视觉一致性
功能扩展方向
1. 地图集成
集成高德地图或百度地图SDK,在地图上显示车辆位置。
实现思路:
- 集成地图SDK(amap_flutter_map)
- 在地图上标注车辆位置
- 使用不同颜色标记不同品牌
- 点击标记显示车辆信息
- 支持地图缩放和拖动
- 显示用户当前位置
- 规划步行路线
代码示例:
AMapWidget(
markers: _bikes.map((bike) => Marker(
position: LatLng(bike.latitude, bike.longitude),
icon: BitmapDescriptor.fromAssetImage(
'assets/bike_${bike.brand}.png',
),
)).toSet(),
onMapCreated: (controller) {
_mapController = controller;
},
)
2. 导航功能
提供从当前位置到车辆位置的导航功能。
实现思路:
- 获取用户当前位置
- 调用地图导航API
- 支持步行、骑行导航
- 显示预计到达时间
- 实时更新导航路线
- 语音播报导航信息
技术方案:
- 使用geolocator获取位置
- 使用url_launcher打开地图导航
- 或集成地图SDK的导航功能
3. 车辆预约
支持提前预约车辆,避免被他人使用。
实现思路:
- 添加预约按钮
- 设置预约时长(5-15分钟)
- 预约期间锁定车辆
- 倒计时显示剩余时间
- 超时自动取消预约
- 预约记录查询
数据模型:
class BikeReservation {
String bikeId;
String userId;
DateTime reserveTime;
int duration; // 分钟
String status; // reserved/expired/cancelled
}
4. 骑行历史
记录用户的骑行历史,包括路线、时长、费用等。
实现思路:
- 记录每次骑行数据
- 显示骑行轨迹
- 统计骑行里程
- 计算总费用
- 生成骑行报告
- 支持数据导出
数据模型:
class RideHistory {
String id;
String bikeId;
String brand;
DateTime startTime;
DateTime endTime;
double distance;
double cost;
List<LatLng> route;
}
5. 收藏功能
收藏常用的车辆停放点,快速查找。
实现思路:
- 添加收藏按钮
- 保存收藏位置
- 显示收藏列表
- 快速导航到收藏点
- 支持备注信息
- 收藏点排序
数据模型:
class FavoriteLocation {
String id;
String name;
double latitude;
double longitude;
String note;
DateTime createTime;
}
6. 价格对比
对比不同品牌在相同时长下的费用差异。
实现思路:
- 输入骑行时长
- 计算各品牌费用
- 柱状图对比显示
- 推荐最优选择
- 显示节省金额
- 支持多种车型对比
UI设计:
Column(
children: [
TextField(
decoration: InputDecoration(labelText: '骑行时长(分钟)'),
onChanged: (value) => _calculateAllPrices(int.parse(value)),
),
..._priceComparisons.map((item) => ListTile(
leading: Icon(Icons.pedal_bike, color: item.brandColor),
title: Text(item.brand),
trailing: Text('¥${item.cost.toStringAsFixed(1)}'),
)),
],
)
7. 通知提醒
设置各种通知提醒,提升用户体验。
实现思路:
- 附近有车提醒
- 电量不足提醒
- 费用超标提醒
- 预约到期提醒
- 优惠活动通知
- 使用flutter_local_notifications
通知类型:
- 本地通知:定时提醒
- 推送通知:实时消息
- 应用内通知:弹窗提示
8. 数据统计
统计用户的骑行数据,生成可视化报表。
实现思路:
- 骑行次数统计
- 总里程统计
- 总费用统计
- 品牌偏好分析
- 时段分布图
- 月度报告生成
图表展示:
// 使用fl_chart绘制图表
LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: _rideData.map((d) => FlSpot(d.day, d.count)).toList(),
isCurved: true,
colors: [Colors.blue],
),
],
),
)
常见问题解答
1. 如何实现实时位置更新?
问题:车辆位置需要实时更新,如何实现?
解答:
在实际应用中,需要集成定位SDK和后端API:
// 使用geolocator获取位置
import 'package:geolocator/geolocator.dart';
Future<Position> _getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('位置服务未开启');
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('位置权限被拒绝');
}
}
return await Geolocator.getCurrentPosition();
}
// 监听位置变化
StreamSubscription<Position> _positionStream =
Geolocator.getPositionStream().listen((Position position) {
setState(() {
_currentPosition = position;
_updateBikeDistances();
});
});
注意事项:
- 需要在AndroidManifest.xml和Info.plist中添加权限
- 考虑电量消耗,合理设置更新频率
- 处理权限被拒绝的情况
2. 如何处理车辆状态的实时同步?
问题:多个用户同时查看,如何保证状态一致?
解答:
需要建立WebSocket连接或使用轮询机制:
// WebSocket方式
import 'package:web_socket_channel/web_socket_channel.dart';
class BikeService {
late WebSocketChannel _channel;
void connect() {
_channel = WebSocketChannel.connect(
Uri.parse('ws://api.example.com/bikes'),
);
_channel.stream.listen((message) {
final data = jsonDecode(message);
_updateBikeStatus(data);
});
}
void _updateBikeStatus(Map<String, dynamic> data) {
final bikeId = data['bikeId'];
final status = data['status'];
// 更新本地数据
}
}
// 轮询方式
Timer.periodic(Duration(seconds: 30), (timer) {
_fetchBikeUpdates();
});
推荐方案:
- 实时性要求高:使用WebSocket
- 实时性要求一般:使用轮询(30-60秒)
- 结合本地缓存减少网络请求
3. 如何集成支付功能?
问题:如何实现骑行费用的在线支付?
解答:
集成第三方支付SDK(支付宝、微信支付):
// 使用tobias集成支付宝
import 'package:tobias/tobias.dart';
Future<void> _payWithAlipay(double amount) async {
// 从服务器获取订单信息
final orderInfo = await _getOrderInfo(amount);
// 调起支付宝
final result = await aliPay(orderInfo);
if (result['resultStatus'] == '9000') {
// 支付成功
_showSuccessDialog();
} else {
// 支付失败
_showErrorDialog(result['memo']);
}
}
// 使用fluwx集成微信支付
import 'package:fluwx/fluwx.dart';
Future<void> _payWithWechat(double amount) async {
// 从服务器获取预支付信息
final prepayInfo = await _getPrepayInfo(amount);
// 调起微信支付
await sendWeChatPayment(
appId: prepayInfo['appId'],
partnerId: prepayInfo['partnerId'],
prepayId: prepayInfo['prepayId'],
packageValue: prepayInfo['package'],
nonceStr: prepayInfo['nonceStr'],
timeStamp: prepayInfo['timeStamp'],
sign: prepayInfo['sign'],
);
// 监听支付结果
weChatResponseEventHandler.listen((response) {
if (response is WeChatPaymentResponse) {
if (response.errCode == 0) {
_showSuccessDialog();
} else {
_showErrorDialog(response.errStr);
}
}
});
}
注意事项:
- 支付信息必须从服务器获取
- 不要在客户端存储支付密钥
- 做好支付结果的验证
4. 如何实现车辆搜索功能?
问题:用户想搜索特定编号或位置的车辆?
解答:
添加搜索框和搜索逻辑:
class BikeSearchDelegate extends SearchDelegate<SharedBike?> {
final List<SharedBike> bikes;
BikeSearchDelegate(this.bikes);
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => query = '',
),
];
}
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => close(context, null),
);
}
Widget buildResults(BuildContext context) {
final results = bikes.where((bike) {
return bike.id.toLowerCase().contains(query.toLowerCase()) ||
bike.address.toLowerCase().contains(query.toLowerCase());
}).toList();
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
final bike = results[index];
return ListTile(
leading: Icon(bike.typeIcon, color: bike.brandColor),
title: Text(bike.id),
subtitle: Text(bike.address),
trailing: Text(bike.statusText),
onTap: () => close(context, bike),
);
},
);
}
Widget buildSuggestions(BuildContext context) {
return buildResults(context);
}
}
// 使用搜索
IconButton(
icon: const Icon(Icons.search),
onPressed: () async {
final result = await showSearch(
context: context,
delegate: BikeSearchDelegate(_bikes),
);
if (result != null) {
_showBikeDetail(result);
}
},
)
搜索功能:
- 按车辆编号搜索
- 按地址搜索
- 按品牌搜索
- 支持模糊匹配
- 显示搜索历史
5. 如何添加用户评价功能?
问题:用户想对骑行体验进行评价?
解答:
添加评价对话框和评价列表:
class BikeReview {
String bikeId;
String userId;
double rating;
String comment;
DateTime createTime;
}
Future<void> _showReviewDialog(SharedBike bike) async {
double rating = 5.0;
String comment = '';
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('评价车辆'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
index < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
),
onPressed: () {
setState(() => rating = index + 1.0);
},
);
}),
),
TextField(
decoration: const InputDecoration(
labelText: '评价内容',
hintText: '分享您的骑行体验',
),
maxLines: 3,
onChanged: (value) => comment = value,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
FilledButton(
onPressed: () {
_submitReview(bike.id, rating, comment);
Navigator.pop(context);
},
child: const Text('提交'),
),
],
),
);
}
void _submitReview(String bikeId, double rating, String comment) {
final review = BikeReview(
bikeId: bikeId,
userId: 'current_user_id',
rating: rating,
comment: comment,
createTime: DateTime.now(),
);
// 保存到本地或上传到服务器
}
评价功能:
- 星级评分(1-5星)
- 文字评价
- 图片上传
- 评价列表展示
- 评价统计分析
项目总结
核心功能流程
数据流转
技术架构
项目特色
- 多城市支持:覆盖8个主要城市,数据独立管理
- 智能筛选:品牌、类型、距离、可用性多维度筛选
- 实时更新:模拟车辆状态和电量的实时变化
- 费用透明:详细的计费规则和费用预估
- 用户体验:Material Design 3设计,交互流畅
- 数据可视化:电量进度条、汇总卡片等可视化元素
- 响应式布局:适配不同屏幕尺寸
- 空状态处理:友好的空状态提示和引导
学习收获
通过本项目,你将掌握:
- Flutter基础:Widget组合、状态管理、导航路由
- Material Design:卡片、芯片、进度条等组件使用
- 数据处理:列表筛选、排序、计算属性
- 交互设计:ModalBottomSheet、对话框、手势操作
- UI设计:颜色搭配、布局设计、视觉层次
- 业务逻辑:费用计算、距离计算、状态更新
- 代码组织:模型设计、方法封装、代码复用
性能优化建议
- 列表优化:使用ListView.builder实现懒加载
- 状态管理:合理使用setState,避免不必要的重建
- 图片优化:使用Icon代替图片,减少资源占用
- 数据缓存:缓存筛选结果,避免重复计算
- 异步操作:使用Future和async/await处理耗时操作
- 内存管理:及时释放不用的资源和监听器
后续优化方向
- 真实数据:接入后端API,获取真实车辆数据
- 地图显示:集成地图SDK,可视化车辆位置
- 导航功能:提供到车辆的导航路线
- 支付集成:接入支付宝、微信支付
- 用户系统:登录注册、个人中心、骑行记录
- 推送通知:车辆提醒、优惠活动通知
- 数据统计:骑行数据分析、图表展示
- 社交功能:分享骑行、好友互动
本项目提供了一个完整的共享单车查询应用框架,你可以在此基础上继续扩展功能,打造更加完善的共享出行应用。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)