恭喜你走到最后一课!🎓 毕业综合项目将整合前29课的所有核心知识——状态管理、网络请求、本地存储、动画、主题、国际化、平台适配——构建一个旅行日记App。这不是简单的Demo,而是一个功能完整、设计精良、可发布的应用。
// 功能模块
// ┌─────────────────────────────────────────────┐
// │ 旅行日记 App │
// ├──────────┬──────────┬──────────┬──────────────┤
// │ 首页 │ 探索 │ 我的 │ 设置 │
// │ Feed流 │ 地图 │ 个人信息 │ 主题切换 │
// │ 推荐 │ 热门地点 │ 我的日记 │ 语言切换 │
// │ 搜索 │ 附近 │ 收藏 │ 数据管理 │
// ├──────────┴──────────┴──────────┴──────────────┤
// │ 核心能力层 │
// │ State(Provider) | Network(Dio) | DB(Sqflite) │
// │ Auth(Firebase) | Maps(Google) | i18n(intl) │
// └──────────────────────────────────────────────┘
// 项目结构
lib/
├── main.dart # 入口 + 多Provider包裹
├── l10n/ # 国际化
│ ├── app_zh.arb
│ └── app_en.arb
├── models/ # 数据模型
│ ├── trip.dart
│ ├── diary_entry.dart
│ ├── location.dart
│ └── user_profile.dart
├── providers/ # 状态管理
│ ├── auth_provider.dart
│ ├── trip_provider.dart
│ ├── map_provider.dart
│ └── settings_provider.dart
├── services/ # 服务层
│ ├── api_service.dart
│ ├── database_service.dart
│ ├── location_service.dart
│ ├── photo_service.dart
│ └── notification_service.dart
├── pages/ # 页面
│ ├── home_page.dart
│ ├── explore_page.dart
│ ├── profile_page.dart
│ ├── settings_page.dart
│ ├── trip_detail_page.dart
│ ├── create_diary_page.dart
│ └── map_page.dart
├── widgets/ # 通用组件
│ ├── trip_card.dart
│ ├── diary_card.dart
│ ├── photo_grid.dart
│ ├── rating_stars.dart
│ └── adaptive_scaffold.dart
└── utils/ # 工具类
├── breakpoints.dart
├── formatters.dart
└── constants.dart
// lib/models/trip.dart
class Trip {
final String id;
final String title;
final String? coverUrl;
final String destination;
final LatLng location;
final DateTime startDate;
final DateTime endDate;
final String description;
final List<DiaryEntry> entries;
final double rating;
final List<String> tags;
final String userId;
final int likes;
final bool isPublic;
final DateTime createdAt;
const Trip({
required this.id, required this.title, this.coverUrl,
required this.destination, required this.location,
required this.startDate, required this.endDate,
required this.description, this.entries = const [],
this.rating = 0, this.tags = const [], required this.userId,
this.likes = 0, this.isPublic = true, required this.createdAt,
});
int get durationDays => endDate.difference(startDate).inDays + 1;
bool get isOngoing => DateTime.now().isAfter(startDate)
&& DateTime.now().isBefore(endDate);
bool get isPast => DateTime.now().isAfter(endDate);
bool get isFuture => DateTime.now().isBefore(startDate);
}
// lib/models/diary_entry.dart
class DiaryEntry {
final String id;
final String tripId;
final String title;
final String content;
final List<String> imageUrls;
final LatLng? location;
final String? locationName;
final double? rating; // 当日评分
final String mood; // 心情emoji
final String weather; // 天气描述
final DateTime date;
final DateTime createdAt;
const DiaryEntry({
required this.id, required this.tripId, required this.title,
required this.content, this.imageUrls = const [],
this.location, this.locationName, this.rating,
this.mood = '😊', this.weather = '☀️',
required this.date, required this.createdAt,
});
}
// lib/pages/home_page.dart
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
actions: [
// 搜索
IconButton(
icon: const Icon(Icons.search),
onPressed: () => showSearch(
context: context,
delegate: TripSearchDelegate(),
),
),
// 通知
NotificationBadge(),
],
),
body: Consumer<TripProvider>(
builder: (context, provider, _) {
if (provider.isLoading && provider.trips.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () => provider.refresh(),
child: CustomScrollView(
slivers: [
// 精选旅行(横向滚动)
if (provider.featuredTrips.isNotEmpty)
SliverToBoxAdapter(
child: _FeaturedSection(trips: provider.featuredTrips),
),
// 推荐旅行(纵向列表)
SliverPadding(
padding: ResponsiveHelper.bodyPadding(context),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: ResponsiveHelper.gridColumns(context),
childAspectRatio: 0.72,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == provider.trips.length) {
return const Center(child: CircularProgressIndicator());
}
return TripCard(
trip: provider.trips[index],
onTap: () => _navigateToDetail(context, provider.trips[index]),
);
},
childCount: provider.trips.length + (provider.hasMore ? 1 : 0),
),
),
),
],
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _navigateToCreate(context),
icon: const Icon(Icons.add),
label: Text(l10n.createNewTrip),
),
);
}
}
// lib/pages/map_page.dart
class MapPage extends StatefulWidget {
final List<Trip> trips;
@override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
GoogleMapController? _mapController;
Set<Marker> _markers = {};
@override
void initState() {
super.initState();
_buildMarkers();
}
void _buildMarkers() {
_markers = widget.trips.map((trip) => Marker(
markerId: MarkerId(trip.id),
position: LatLng(trip.location.latitude, trip.location.longitude),
infoWindow: InfoWindow(
title: trip.title,
snippet: '${trip.destination} · ${trip.startDate.year}',
onTap: () => _navigateToDetail(trip),
),
icon: BitmapDescriptor.defaultMarkerWithHue(
_getMarkerHue(trip),
),
)).toSet();
}
double _getMarkerHue(Trip trip) {
if (trip.isOngoing) return BitmapDescriptor.hueGreen;
if (trip.isFuture) return BitmapDescriptor.hueAzure;
return BitmapDescriptor.hueViolet;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('旅行地图')),
body: GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(35.86, 104.19), // 中国中心
zoom: 4,
),
markers: _markers,
onMapCreated: (controller) => _mapController = controller,
onCameraMove: _onCameraMove,
myLocationEnabled: true,
myLocationButtonEnabled: true,
),
);
}
}
// lib/pages/create_diary_page.dart
class CreateDiaryPage extends StatefulWidget {
final String tripId;
@override
State<CreateDiaryPage> createState() => _CreateDiaryPageState();
}
class _CreateDiaryPageState extends State<CreateDiaryPage> {
final _titleController = TextEditingController();
final _contentController = TextEditingController();
final _formKey = GlobalKey<FormState>();
List<File> _selectedImages = [];
LatLng? _selectedLocation;
String _selectedMood = '😊';
double _rating = 3;
bool _isSaving = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('写日记'),
actions: [
FilledButton(
onPressed: _isSaving ? null : _save,
child: _isSaving
? const SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: const Text('发布'),
),
const SizedBox(width: 8),
],
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// 日期选择
ListTile(
leading: const Icon(Icons.calendar_today),
title: Text(DateFormat('yyyy年MM月dd日').format(DateTime.now())),
trailing: const Icon(Icons.chevron_right),
onTap: _selectDate,
),
const Divider(),
// 标题
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
hintText: '日记标题',
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
validator: (v) => v?.trim().isEmpty == true ? '请输入标题' : null,
),
const SizedBox(height: 12),
// 心情 + 天气
Row(
children: [
_MoodSelector(
selected: _selectedMood,
onSelected: (m) => setState(() => _selectedMood = m),
),
const Spacer(),
_RatingStars(
rating: _rating,
onRatingChanged: (r) => setState(() => _rating = r),
),
],
),
const SizedBox(height: 12),
// 内容
TextFormField(
controller: _contentController,
decoration: const InputDecoration(
hintText: '记录今天的旅程...',
border: InputBorder.none,
),
maxLines: null,
minLines: 5,
validator: (v) => v?.trim().isEmpty == true ? '请输入内容' : null,
),
const SizedBox(height: 16),
// 图片选择
_ImagePicker(
images: _selectedImages,
onAdd: _pickImages,
onRemove: (i) => setState(() => _selectedImages.removeAt(i)),
),
const SizedBox(height: 16),
// 位置标记
ListTile(
leading: const Icon(Icons.location_on),
title: Text(_selectedLocation != null ? '已标记位置' : '标记位置'),
trailing: const Icon(Icons.chevron_right),
onTap: _pickLocation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Theme.of(context).colorScheme.outline),
),
),
],
),
),
);
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isSaving = true);
try {
// 上传图片
final imageUrls = await Future.wait(
_selectedImages.map((f) => PhotoService().upload(f)),
);
// 创建日记
final entry = DiaryEntry(
id: const Uuid().v4(),
tripId: widget.tripId,
title: _titleController.text.trim(),
content: _contentController.text.trim(),
imageUrls: imageUrls,
location: _selectedLocation,
rating: _rating,
mood: _selectedMood,
date: DateTime.now(),
createdAt: DateTime.now(),
);
await context.read<TripProvider>().addDiaryEntry(entry);
if (mounted) Navigator.pop(context, true);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败:$e')),
);
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
}
// lib/main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化服务
await dotenv.load(fileName: '.env');
final db = await DatabaseService().initialize();
final prefs = await SharedPreferences.getInstance();
runApp(TravelDiaryApp(db: db, prefs: prefs));
}
class TravelDiaryApp extends StatelessWidget {
final Database db;
final SharedPreferences prefs;
const TravelDiaryApp({required this.db, required this.prefs});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeManager(prefs)),
ChangeNotifierProvider(create: (_) => LocaleProvider(prefs)),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => TripProvider(db)),
ChangeNotifierProvider(create: (_) => MapProvider()),
ChangeNotifierProvider(create: (_) => NotificationProvider()),
],
child: Consumer2<ThemeManager, LocaleProvider>(
builder: (context, themeMgr, localeMgr, _) {
return MaterialApp(
// 国际化
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: localeMgr.locale,
// 主题
theme: themeMgr.lightTheme,
darkTheme: themeMgr.darkTheme,
themeMode: themeMgr.mode,
// 路由
home: const AuthWrapper(),
debugShowCheckedModeBanner: false,
);
},
),
);
}
}
// 认证包装器
class AuthWrapper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, auth, _) {
if (auth.isLoading) {
return const SplashScreen();
}
if (auth.isAuthenticated) {
return const MainShell(); // 登录后主界面
}
return const LoginPage(); // 未登录
},
);
}
}
// 主Shell - 自适应导航
class MainShell extends StatelessWidget {
const MainShell();
@override
Widget build(BuildContext context) {
return AdaptiveScaffold(
selectedIndex: _currentIndex,
onDestinationSelected: (i) => _navigate(i),
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: '首页'),
NavigationDestination(icon: Icon(Icons.explore), label: '探索'),
NavigationDestination(icon: Icon(Icons.map), label: '地图'),
NavigationDestination(icon: Icon(Icons.person), label: '我的'),
],
body: _pages[_currentIndex],
);
}
}
flutter_map+离线瓦片🎉 恭喜你完成了全部30课的学习!
你从一个Flutter新手成长为能独立开发完整App的开发者。回顾你的旅程:
你的Flutter之旅才刚刚开始。保持学习,持续实践,构建出改变世界的App!💪