Flutter旅游足迹记录本应用开发教程
项目简介
旅游足迹记录本是一款专为旅行爱好者设计的移动应用,帮助用户记录和管理自己的旅行经历。应用采用Flutter框架开发,支持跨平台运行,提供直观的界面和丰富的功能。
运行效果图



核心功能
- 旅行记录管理:添加、查看、编辑旅行记录
- 多维度分类:支持休闲旅游、商务出行、探险旅行等类型
- 统计分析:提供旅行类型分布、月度统计、花费分析
- 足迹地图:展示旅行地点分布(基础版本)
- 精彩回忆:高评分旅行记录筛选展示
技术特点
- 单文件架构,代码结构清晰
- Material Design 3设计风格
- 响应式布局,适配不同屏幕尺寸
- 丰富的交互动画和视觉效果
- 完整的数据模型和状态管理
项目架构设计
整体架构
TravelJournalApp
TravelJournalHomePage
足迹页面
统计页面
地图页面
AddRecordDialog
RecordDetailsDialog
数据模型
TravelRecord
TravelType
TravelTypeConfig
页面结构
应用采用底部导航栏设计,包含三个主要页面:
- 足迹页面:展示所有旅行记录,支持全部足迹和精彩回忆两个标签
- 统计页面:提供旅行类型分布、月度统计、花费分析
- 地图页面:展示足迹分布和地点统计
数据模型设计
TravelRecord 旅行记录模型
dart
class TravelRecord {
final String id; // 唯一标识
final String title; // 旅行标题
final String location; // 旅行地点
final DateTime startDate; // 开始日期
final DateTime endDate; // 结束日期
final String description; // 旅行描述
final List<String> photos; // 照片列表
final double? rating; // 评分(1-5星)
final String weather; // 天气情况
final double? cost; // 花费金额
final List<String> companions; // 同行人员
final List<String> highlights; // 精彩瞬间
final TravelType type; // 旅行类型
}
TravelType 旅行类型枚举
dart
enum TravelType {
leisure, // 休闲旅游
business, // 商务出行
adventure, // 探险旅行
cultural, // 文化之旅
family, // 家庭旅行
}
TravelTypeConfig 类型配置
dart
class TravelTypeConfig {
final String name; // 类型名称
final IconData icon; // 图标
final Color color; // 主题色
}
每种旅行类型都有对应的配置,包括显示名称、图标和主题色,用于在界面中进行视觉区分。
核心功能实现
1. 主页面结构
主页面使用StatefulWidget实现,包含底部导航栏和对应的页面内容:
dart
class _TravelJournalHomePageState extends State<TravelJournalHomePage>
with TickerProviderStateMixin {
int _selectedIndex = 0;
late TabController _tabController;
final List<TravelRecord> _records = [];
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
_buildRecordsPage(), // 足迹页面
_buildStatsPage(), // 统计页面
_buildMapPage(), // 地图页面
][_selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: const [
NavigationDestination(icon: Icon(Icons.book_outlined), label: '足迹'),
NavigationDestination(icon: Icon(Icons.analytics_outlined), label: '统计'),
NavigationDestination(icon: Icon(Icons.map_outlined), label: '地图'),
],
),
);
}
}
2. 足迹页面实现
足迹页面包含头部统计信息和记录列表:
dart
Widget _buildRecordsPage() {
return Column(
children: [
_buildRecordsHeader(), // 头部统计
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildRecordsList(false), // 全部记录
_buildRecordsList(true), // 收藏记录
],
),
),
],
);
}
3. 记录卡片设计
每个旅行记录以卡片形式展示,包含类型图标、基本信息、评分等:
dart
Widget _buildRecordCard(TravelRecord record) {
final typeConfig = _typeConfigs[record.type]!;
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () => _showRecordDetails(record),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部:类型图标 + 标题 + 评分
Row(
children: [
Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: typeConfig.color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(typeConfig.icon, color: typeConfig.color),
),
// 标题和地点信息
Expanded(child: /* 标题和地点 */),
// 评分显示
if (record.rating != null) /* 评分徽章 */,
],
),
// 日期、天数、花费信息
Row(children: [/* 详细信息 */]),
// 描述和精彩瞬间标签
if (record.description.isNotEmpty) /* 描述文本 */,
if (record.highlights.isNotEmpty) /* 标签列表 */,
],
),
),
),
);
}
4. 统计分析功能
统计页面提供三种分析维度:
旅行类型分布
dart
Widget _buildTypeStats() {
final typeStats = <TravelType, int>{};
for (final record in _records) {
typeStats[record.type] = (typeStats[record.type] ?? 0) + 1;
}
return Card(
child: Column(
children: typeStats.entries.map((entry) {
final config = _typeConfigs[entry.key]!;
final percentage = (entry.value / _records.length) * 100;
return Row(
children: [
Icon(config.icon, color: config.color),
Expanded(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(config.name),
Text('${entry.value}次 (${percentage.toStringAsFixed(1)}%)'),
],
),
LinearProgressIndicator(
value: percentage / 100,
valueColor: AlwaysStoppedAnimation<Color>(config.color),
),
],
),
),
],
);
}).toList(),
),
);
}
月度旅行统计
dart
Widget _buildMonthlyStats() {
final monthlyStats = <String, int>{};
for (final record in _records) {
final monthKey = '${record.startDate.year}-${record.startDate.month.toString().padLeft(2, '0')}';
monthlyStats[monthKey] = (monthlyStats[monthKey] ?? 0) + 1;
}
final sortedMonths = monthlyStats.entries.toList()
..sort((a, b) => b.key.compareTo(a.key));
return Card(
child: Column(
children: sortedMonths.take(6).map((entry) {
final parts = entry.key.split('-');
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${parts[0]}年${parts[1]}月'),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.teal.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text('${entry.value}次'),
),
],
);
}).toList(),
),
);
}
花费统计分析
dart
Widget _buildCostStats() {
final totalCost = _records.fold(0.0, (sum, record) => sum + (record.cost ?? 0));
final avgCost = _records.isNotEmpty ? totalCost / _records.length : 0.0;
final maxCost = _records.fold(0.0, (max, record) =>
(record.cost ?? 0) > max ? (record.cost ?? 0) : max);
return Card(
child: Row(
children: [
Expanded(child: _buildCostStatItem('总花费', totalCost, Colors.red)),
Expanded(child: _buildCostStatItem('平均花费', avgCost, Colors.blue)),
Expanded(child: _buildCostStatItem('最高花费', maxCost, Colors.green)),
],
),
);
}
5. 添加记录对话框
添加记录功能通过模态对话框实现,包含完整的表单验证:
dart
class _AddRecordDialog extends StatefulWidget {
final Map<TravelType, TravelTypeConfig> typeConfigs;
final Function(TravelRecord) onSave;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('添加旅行记录'),
content: SizedBox(
width: 400, height: 600,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: [
// 标题输入框
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '旅行标题 *',
prefixIcon: Icon(Icons.title),
),
validator: (value) => value?.trim().isEmpty == true ? '请输入旅行标题' : null,
),
// 其他表单字段...
],
),
),
),
),
);
}
}
UI组件设计
1. 渐变头部设计
应用使用渐变色头部提升视觉效果:
dart
Container(
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.teal.shade600, Colors.teal.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
children: [
// 标题行
Row(
children: [
const Icon(Icons.travel_explore, color: Colors.white, size: 32),
const SizedBox(width: 12),
const Text('旅游足迹记录本', style: TextStyle(fontSize: 24, color: Colors.white)),
],
),
// 统计卡片
Row(
children: [
Expanded(child: _buildSummaryCard('旅行次数', '$totalRecords', '次', Icons.flight_takeoff)),
Expanded(child: _buildSummaryCard('旅行天数', '$totalDays', '天', Icons.calendar_today)),
Expanded(child: _buildSummaryCard('总花费', '${totalCost.toStringAsFixed(0)}', '元', Icons.attach_money)),
],
),
],
),
)
2. 统计卡片组件
dart
Widget _buildSummaryCard(String title, String value, String unit, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 20),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.white70)),
Text(title, style: const TextStyle(fontSize: 12, color: Colors.white70)),
],
),
);
}
3. 评分显示组件
dart
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, size: 16, color: Colors.amber),
const SizedBox(width: 4),
Text(
record.rating!.toStringAsFixed(1),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.amber),
),
],
),
)
4. 标签组件设计
精彩瞬间使用标签形式展示:
dart
Wrap(
spacing: 6, runSpacing: 4,
children: record.highlights.take(3).map((highlight) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(highlight, style: TextStyle(fontSize: 12, color: Colors.grey.shade700)),
);
}).toList(),
)
工具方法实现
1. 日期格式化
dart
String _formatDate(DateTime date) {
return '${date.month}/${date.day}'; // 简短格式:月/日
}
String _formatFullDate(DateTime date) {
return '${date.year}年${date.month}月${date.day}日'; // 完整格式
}
2. 数据计算方法
dart
// 计算旅行天数
int get duration => endDate.difference(startDate).inDays + 1;
// 计算总花费
double get totalCost => _records.fold(0.0, (sum, record) => sum + (record.cost ?? 0));
// 计算平均花费
double get avgCost => _records.isNotEmpty ? totalCost / _records.length : 0.0;
3. 数据筛选方法
dart
// 筛选精彩回忆(高评分记录)
List<TravelRecord> get favoriteRecords =>
_records.where((record) => (record.rating ?? 0) >= 4.5).toList();
// 按日期排序
List<TravelRecord> get sortedRecords =>
_records..sort((a, b) => b.startDate.compareTo(a.startDate));
4. 表单验证方法
dart
String? validateTitle(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入旅行标题';
}
return null;
}
String? validateLocation(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入旅行地点';
}
return null;
}
状态管理
1. 本地状态管理
应用使用setState进行简单的本地状态管理:
dart
class _TravelJournalHomePageState extends State<TravelJournalHomePage> {
int _selectedIndex = 0; // 当前选中的页面索引
final List<TravelRecord> _records = []; // 旅行记录列表
// 添加记录
void _addRecord(TravelRecord record) {
setState(() {
_records.add(record);
});
}
// 删除记录
void _deleteRecord(String id) {
setState(() {
_records.removeWhere((record) => record.id == id);
});
}
// 更新记录
void _updateRecord(TravelRecord updatedRecord) {
setState(() {
final index = _records.indexWhere((r) => r.id == updatedRecord.id);
if (index != -1) {
_records[index] = updatedRecord;
}
});
}
}
2. 数据持久化扩展
虽然当前版本使用内存存储,但可以轻松扩展为持久化存储:
dart
// 保存到本地存储
Future<void> _saveRecords() async {
final prefs = await SharedPreferences.getInstance();
final recordsJson = _records.map((r) => r.toJson()).toList();
await prefs.setString('travel_records', jsonEncode(recordsJson));
}
// 从本地存储加载
Future<void> _loadRecords() async {
final prefs = await SharedPreferences.getInstance();
final recordsString = prefs.getString('travel_records');
if (recordsString != null) {
final recordsList = jsonDecode(recordsString) as List;
_records.addAll(recordsList.map((json) => TravelRecord.fromJson(json)));
}
}
功能扩展建议
1. 照片管理功能
dart
// 添加照片选择功能
Future<void> _pickImages() async {
final ImagePicker picker = ImagePicker();
final List<XFile> images = await picker.pickMultiImage();
setState(() {
_selectedImages.addAll(images.map((image) => image.path));
});
}
// 照片展示组件
Widget _buildPhotoGrid(List<String> photos) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: photos.length,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(File(photos[index]), fit: BoxFit.cover),
);
},
);
}
2. 地图集成功能
dart
// 集成地图显示
Widget _buildMapView() {
return GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(39.9042, 116.4074), // 北京坐标
zoom: 5,
),
markers: _records.map((record) {
return Marker(
markerId: MarkerId(record.id),
position: _getLocationCoordinates(record.location),
infoWindow: InfoWindow(
title: record.title,
snippet: record.location,
),
);
}).toSet(),
);
}
3. 数据导出功能
dart
// 导出为CSV格式
Future<void> _exportToCSV() async {
final csv = const ListToCsvConverter();
final List<List<dynamic>> rows = [
['标题', '地点', '开始日期', '结束日期', '天数', '类型', '评分', '花费'],
..._records.map((record) => [
record.title,
record.location,
record.startDate.toIso8601String(),
record.endDate.toIso8601String(),
record.duration,
_typeConfigs[record.type]!.name,
record.rating ?? '',
record.cost ?? '',
]),
];
final csvString = csv.convert(rows);
// 保存文件逻辑...
}
4. 搜索和筛选功能
dart
// 搜索功能
List<TravelRecord> _searchRecords(String query) {
if (query.isEmpty) return _records;
return _records.where((record) {
return record.title.toLowerCase().contains(query.toLowerCase()) ||
record.location.toLowerCase().contains(query.toLowerCase()) ||
record.description.toLowerCase().contains(query.toLowerCase());
}).toList();
}
// 筛选功能
List<TravelRecord> _filterRecords({
TravelType? type,
DateTimeRange? dateRange,
double? minRating,
}) {
return _records.where((record) {
if (type != null && record.type != type) return false;
if (dateRange != null &&
(record.startDate.isBefore(dateRange.start) ||
record.endDate.isAfter(dateRange.end))) return false;
if (minRating != null && (record.rating ?? 0) < minRating) return false;
return true;
}).toList();
}
5. 社交分享功能
dart
// 分享旅行记录
Future<void> _shareRecord(TravelRecord record) async {
final text = '''
🌍 ${record.title}
📍 ${record.location}
📅 ${_formatDate(record.startDate)} - ${_formatDate(record.endDate)}
⭐ ${record.rating?.toStringAsFixed(1) ?? '未评分'}
💰 ${record.cost != null ? '¥${record.cost!.toStringAsFixed(0)}' : ''}
${record.description}
#旅行记录 #${_typeConfigs[record.type]!.name}
''';
await Share.share(text);
}
性能优化策略
1. 列表性能优化
dart
// 使用ListView.builder进行懒加载
ListView.builder(
itemCount: filteredRecords.length,
itemBuilder: (context, index) {
final record = filteredRecords[index];
return _buildRecordCard(record);
},
// 添加缓存范围
cacheExtent: 1000,
)
// 使用AutomaticKeepAliveClientMixin保持页面状态
class _RecordsPageState extends State<RecordsPage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return /* 页面内容 */;
}
}
2. 图片加载优化
dart
// 使用cached_network_image缓存网络图片
CachedNetworkImage(
imageUrl: record.photos[index],
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
fit: BoxFit.cover,
)
// 图片压缩
Future<File> _compressImage(File file) async {
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
'${file.parent.path}/compressed_${file.path.split('/').last}',
quality: 70,
minWidth: 800,
minHeight: 600,
);
return result ?? file;
}
3. 内存管理
dart
// 及时释放资源
@override
void dispose() {
_titleController.dispose();
_locationController.dispose();
_descriptionController.dispose();
_tabController.dispose();
super.dispose();
}
// 使用弱引用避免内存泄漏
class RecordManager {
final List<WeakReference<TravelRecord>> _records = [];
void addRecord(TravelRecord record) {
_records.add(WeakReference(record));
}
List<TravelRecord> get activeRecords {
return _records
.map((ref) => ref.target)
.where((record) => record != null)
.cast<TravelRecord>()
.toList();
}
}
测试指南
1. 单元测试
dart
// test/models/travel_record_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:travel_journal/models/travel_record.dart';
void main() {
group('TravelRecord Tests', () {
test('should calculate duration correctly', () {
final record = TravelRecord(
id: '1',
title: 'Test Trip',
location: 'Test Location',
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 1, 3),
type: TravelType.leisure,
);
expect(record.duration, equals(3));
});
test('should create copy with updated values', () {
final original = TravelRecord(
id: '1',
title: 'Original',
location: 'Original Location',
startDate: DateTime.now(),
endDate: DateTime.now(),
type: TravelType.leisure,
);
final updated = original.copyWith(title: 'Updated');
expect(updated.title, equals('Updated'));
expect(updated.location, equals('Original Location'));
});
});
}
2. Widget测试
dart
// test/widgets/record_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:travel_journal/main.dart';
void main() {
testWidgets('RecordCard displays correct information', (tester) async {
final record = TravelRecord(
id: '1',
title: 'Test Trip',
location: 'Test Location',
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 1, 3),
rating: 4.5,
type: TravelType.leisure,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RecordCard(record: record),
),
),
);
expect(find.text('Test Trip'), findsOneWidget);
expect(find.text('Test Location'), findsOneWidget);
expect(find.text('4.5'), findsOneWidget);
});
}
3. 集成测试
dart
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:travel_journal/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Travel Journal App Tests', () {
testWidgets('should add new travel record', (tester) async {
app.main();
await tester.pumpAndSettle();
// 点击添加按钮
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
// 填写表单
await tester.enterText(find.byKey(const Key('title_field')), 'Test Trip');
await tester.enterText(find.byKey(const Key('location_field')), 'Test Location');
// 保存记录
await tester.tap(find.text('保存'));
await tester.pumpAndSettle();
// 验证记录已添加
expect(find.text('Test Trip'), findsOneWidget);
});
testWidgets('should navigate between tabs', (tester) async {
app.main();
await tester.pumpAndSettle();
// 点击统计标签
await tester.tap(find.text('统计'));
await tester.pumpAndSettle();
expect(find.text('旅行统计'), findsOneWidget);
// 点击地图标签
await tester.tap(find.text('地图'));
await tester.pumpAndSettle();
expect(find.text('足迹地图'), findsOneWidget);
});
});
}
部署指南
1. Android部署
bash
# 构建APK
flutter build apk --release
# 构建App Bundle(推荐用于Google Play)
flutter build appbundle --release
# 安装到设备
flutter install
2. iOS部署
bash
# 构建iOS应用
flutter build ios --release
# 使用Xcode打开项目进行签名和发布
open ios/Runner.xcworkspace
3. Web部署
bash
# 构建Web版本
flutter build web --release
# 部署到Firebase Hosting
firebase deploy --only hosting
4. 桌面应用部署
bash
# Windows
flutter build windows --release
# macOS
flutter build macos --release
# Linux
flutter build linux --release
项目总结
技术亮点
- 单文件架构:代码结构清晰,易于维护和理解
- Material Design 3:现代化的UI设计,用户体验优秀
- 响应式布局:适配不同屏幕尺寸和设备类型
- 丰富的交互:流畅的动画和直观的操作体验
- 完整的功能:涵盖记录管理、统计分析、数据展示等核心需求
学习价值
- Flutter基础:Widget组合、状态管理、导航等核心概念
- UI设计:Material Design组件使用和自定义样式
- 数据处理:模型设计、列表操作、统计计算
- 表单处理:输入验证、数据收集、用户交互
- 项目架构:代码组织、功能模块划分、扩展性设计
这个旅游足迹记录本应用展示了Flutter开发的完整流程,从需求分析到功能实现,从UI设计到性能优化,为学习Flutter开发提供了很好的实践案例。通过这个项目,开发者可以掌握Flutter应用开发的核心技能,为后续的项目开发打下坚实基础。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net