Flutter for OpenHarmony 实战:旅游攻略应用开发指南
前言
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
在移动应用开发领域,Flutter 以其高效的跨平台能力和精美的 UI 表现赢得了广大开发者的青睐。而 Flutter for OpenHarmony 的出现,让 Flutter 应用能够运行在鸿蒙设备上,打破了平台壁垒。本文将以一个旅游攻略应用为例,详细介绍如何使用 Flutter 开发兼容 OpenHarmony 的应用,并提供完整的代码实现。
一、项目概述
本文要实现的旅游攻略应用具备以下核心功能:
- 首页轮播图展示
- 景点列表展示与详情页
- 下拉刷新与上拉加载更多
- 目的地城市分类浏览
- 游记列表与详情
技术栈
- 框架: Flutter 3.x
- 语言: Dart
- 状态管理: StatefulWidget
- 网络图片: cached_network_image
- 目标平台: OpenHarmony
二、项目结构设计
一个规范的项目结构对于代码维护至关重要。本文采用如下目录结构:
lib/
├── main.dart # 应用入口
├── models/
│ └── travel_model.dart # 数据模型
├── services/
│ └── travel_service.dart # 数据服务层
└── pages/
└── travel_main_page.dart # 主页面及子页面
三、数据模型定义
首先,我们定义应用所需的数据模型。Dart 的类设计清晰简洁,便于维护和扩展。
dart
// lib/models/travel_model.dart
import 'package:flutter/material.dart';
/// 景点数据模型
class ScenicSpot {
final String id;
final String name;
final String cityName;
final String provinceName;
final String coverUrl;
final double rating;
final int reviewCount;
final double price;
final List<String> tags;
final String description;
final String address;
final String openTime;
final String bestSeason;
final String recommendedTime;
bool isFavorite;
bool isHot;
final List<String> images;
ScenicSpot({
required this.id,
required this.name,
required this.cityName,
required this.provinceName,
required this.coverUrl,
required this.rating,
required this.reviewCount,
required this.price,
required this.tags,
required this.description,
required this.address,
required this.openTime,
required this.bestSeason,
required this.recommendedTime,
this.isFavorite = false,
this.isHot = false,
required this.images,
});
}
/// 游记/攻略数据模型
class Guide {
final String id;
final String title;
final String authorName;
final String authorAvatar;
final String coverUrl;
final String summary;
final int likeCount;
final int collectCount;
final int commentCount;
final String publishTime;
final String scenicSpotName;
final String scenicSpotId;
bool isFavorite;
bool isLiked;
final List<String> images;
final int days;
final String budget;
Guide({
required this.id,
required this.title,
required this.authorName,
required this.authorAvatar,
required this.coverUrl,
required this.summary,
required this.likeCount,
required this.collectCount,
required this.commentCount,
required this.publishTime,
required this.scenicSpotName,
required this.scenicSpotId,
this.isFavorite = false,
this.isLiked = false,
required this.images,
required this.days,
required this.budget,
});
}
/// 目的地城市数据模型
class Destination {
final String id;
final String name;
final String coverUrl;
final String description;
final int spotCount;
final int guideCount;
final double rating;
final List<String> tags;
final List<String> hotSpots;
Destination({
required this.id,
required this.name,
required this.coverUrl,
required this.description,
required this.spotCount,
required this.guideCount,
required this.rating,
required this.tags,
required this.hotSpots,
});
}
/// 轮播图数据模型
class Banner {
final String id;
final String title;
final String imageUrl;
final String actionType;
final String actionValue;
Banner({
required this.id,
required this.title,
required this.imageUrl,
required this.actionType,
required this.actionValue,
});
}
/// 用户数据模型
class User {
final String id;
final String nickname;
final String avatar;
final String signature;
final int favoriteCount;
final int guideCount;
final int followCount;
final int fansCount;
final int level;
User({
required this.id,
required this.nickname,
required this.avatar,
required this.signature,
required this.favoriteCount,
required this.guideCount,
required this.followCount,
required this.fansCount,
required this.level,
});
}
四、服务层实现
服务层负责数据获取和处理,本文采用模拟数据的方式,便于读者快速运行和调试。在实际项目中,可以替换为真实的 API 调用。
dart
// lib/services/travel_service.dart
import '../models/travel_model.dart';
/// 旅游攻略服务 - 模拟网络请求
class TravelService {
/// 获取首页轮播图
Future<List<Banner>> getBanners() async {
await Future.delayed(const Duration(milliseconds: 500));
return _mockBanners;
}
/// 获取推荐景点列表
Future<List<ScenicSpot>> getRecommendSpots(int page) async {
await Future.delayed(const Duration(milliseconds: 500));
int pageSize = 10;
int start = (page - 1) * pageSize;
int end = start + pageSize;
if (start >= _mockSpots.length) return [];
return _mockSpots.sublist(
start,
end > _mockSpots.length ? _mockSpots.length : end,
);
}
/// 获取目的地城市列表
Future<List<Destination>> getDestinations() async {
await Future.delayed(const Duration(milliseconds: 500));
return _mockDestinations;
}
/// 获取游记列表
Future<List<Guide>> getGuides(int page) async {
await Future.delayed(const Duration(milliseconds: 500));
int pageSize = 10;
int start = (page - 1) * pageSize;
int end = start + pageSize;
if (start >= _mockGuides.length) return [];
return _mockGuides.sublist(
start,
end > _mockGuides.length ? _mockGuides.length : end,
);
}
/// 获取当前用户信息
Future<User> getCurrentUser() async {
await Future.delayed(const Duration(milliseconds: 300));
return _mockUser;
}
/// 格式化评论数量
String formatCount(int count) {
if (count >= 10000) {
return '${(count / 10000).toStringAsFixed(1)}w';
} else if (count >= 1000) {
return '${(count / 1000).toStringAsFixed(1)}k';
}
return count.toString();
}
// 模拟轮播图数据
static final List<Banner> _mockBanners = [
Banner(
id: 'banner_1',
title: '云南丽江古城',
imageUrl: 'https://picsum.photos/seed/lijiang/750/400',
actionType: 'spot',
actionValue: 'spot_1',
),
Banner(
id: 'banner_2',
title: '张家界天门山',
imageUrl: 'https://picsum.photos/seed/zhangjiajie/750/400',
actionType: 'spot',
actionValue: 'spot_2',
),
// ... 更多数据
];
// 模拟景点数据
static final List<ScenicSpot> _mockSpots = [
ScenicSpot(
id: 'spot_1',
name: '丽江古城',
cityName: '丽江',
provinceName: '云南',
coverUrl: 'https://picsum.photos/seed/lijiang古城/600/400',
rating: 4.8,
reviewCount: 256000,
price: 0,
tags: ['5A景区', '世界文化遗产', '古镇'],
description: '丽江古城位于云南省丽江市古城区...',
address: '云南省丽江市古城区',
openTime: '全天开放',
bestSeason: '春季秋季',
recommendedTime: '2-3天',
isHot: true,
images: [
'https://picsum.photos/seed/lijiang1/800/600',
'https://picsum.photos/seed/lijiang2/800/600',
],
),
// ... 更多景点数据
];
// 模拟用户数据
static final User _mockUser = User(
id: 'user_1',
nickname: '旅行爱好者',
avatar: 'https://picsum.photos/seed/myavatar/200/200',
signature: '走遍中国,发现美好',
favoriteCount: 56,
guideCount: 12,
followCount: 128,
fansCount: 568,
level: 5,
);
}
五、主页面实现
主页面采用底部导航栏设计,包含四个主要选项卡:推荐、目的地、游记和我的。下面是核心代码实现。
dart
// lib/pages/travel_main_page.dart
import 'package:flutter/material.dart';
import '../models/travel_model.dart';
import '../services/travel_service.dart';
/// 主页面 - 底部选项卡
class TravelMainPage extends StatefulWidget {
const TravelMainPage({super.key});
@override
State<TravelMainPage> createState() => _TravelMainPageState();
}
class _TravelMainPageState extends State<TravelMainPage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
RecommendPage(),
DestinationPage(),
GuidePage(),
MyPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFFFF6B4A),
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '推荐',
),
BottomNavigationBarItem(
icon: Icon(Icons.location_on_outlined),
activeIcon: Icon(Icons.location_on),
label: '目的地',
),
BottomNavigationBarItem(
icon: Icon(Icons.book_outlined),
activeIcon: Icon(Icons.book),
label: '游记',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
六、推荐页面实现
推荐页面是应用的核心页面,包含轮播图和景点列表。支持下拉刷新和上拉加载更多功能。
dart
/// 推荐页面
class RecommendPage extends StatefulWidget {
const RecommendPage({super.key});
@override
State<RecommendPage> createState() => _RecommendPageState();
}
class _RecommendPageState extends State<RecommendPage> {
final TravelService _service = TravelService();
List<Banner> _banners = [];
List<ScenicSpot> _spots = [];
bool _isLoading = true;
bool _isLoadingMore = false;
bool _hasMore = true;
int _currentPage = 1;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_loadData();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
final banners = await _service.getBanners();
final spots = await _service.getRecommendSpots(1);
setState(() {
_banners = banners;
_spots = spots;
_currentPage = 1;
_hasMore = spots.length >= 10;
_isLoading = false;
});
}
Future<void> _loadMore() async {
if (_isLoadingMore || !_hasMore) return;
setState(() => _isLoadingMore = true);
_currentPage++;
final moreSpots = await _service.getRecommendSpots(_currentPage);
if (moreSpots.isNotEmpty) {
setState(() {
_spots.addAll(moreSpots);
_hasMore = moreSpots.length >= 10;
_isLoadingMore = false;
});
} else {
setState(() {
_hasMore = false;
_isLoadingMore = false;
});
}
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFFFF6B4A)),
);
}
return RefreshIndicator(
onRefresh: _loadData,
color: const Color(0xFFFF6B4A),
child: CustomScrollView(
controller: _scrollController,
slivers: [
// 轮播图
SliverToBoxAdapter(
child: _buildBannerSwiper(),
),
// 热门景点标题
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Text(
'热门景点',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(width: 8),
Text(
'热门推荐,精彩不停',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
),
// 景点列表
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index < _spots.length) {
return _buildSpotItem(_spots[index]);
} else if (_isLoadingMore) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: CircularProgressIndicator(color: Color(0xFFFF6B4A)),
),
);
} else if (!_hasMore) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'- 已加载全部 -',
style: TextStyle(color: Colors.grey),
),
),
);
}
return null;
},
childCount: _spots.length + 1,
),
),
],
),
);
}
Widget _buildBannerSwiper() {
return Container(
height: 200,
padding: const EdgeInsets.all(16),
child: PageView.builder(
itemCount: _banners.length,
itemBuilder: (context, index) {
final banner = _banners[index];
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: NetworkImage(banner.imageUrl),
fit: BoxFit.cover,
),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.5),
],
),
),
alignment: Alignment.bottomLeft,
padding: const EdgeInsets.all(12),
child: Text(
banner.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
);
}
Widget _buildSpotItem(ScenicSpot spot) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
spot.coverUrl,
width: 120,
height: 100,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
spot.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
const Text(
'★',
style: TextStyle(color: Color(0xFFFFB800), fontSize: 12),
),
Text(
spot.rating.toString(),
style: const TextStyle(
color: Color(0xFFFFB800),
fontSize: 12,
),
),
],
),
],
),
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.location_on, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text(
spot.cityName,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
children: spot.tags.take(2).map((tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFFF6B4A).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
tag,
style: const TextStyle(
fontSize: 10,
color: Color(0xFFFF6B4A),
),
),
);
}).toList(),
),
const SizedBox(height: 6),
Row(
children: [
if (spot.price > 0) ...[
const Text('¥', style: TextStyle(color: Color(0xFFFF6B4A), fontSize: 12)),
Text(
spot.price.toString(),
style: const TextStyle(
color: Color(0xFFFF6B4A),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Text('起', style: TextStyle(color: Colors.grey, fontSize: 10)),
] else
const Text(
'免费',
style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
],
),
);
}
}
七、目的地上下文页面
目的地上下文页面展示了热门目的地城市列表,采用网格和列表两种布局方式,便于用户快速找到目标景点。
dart
/// 目的地页面
class DestinationPage extends StatefulWidget {
const DestinationPage({super.key});
@override
State<DestinationPage> createState() => _DestinationPageState();
}
class _DestinationPageState extends State<DestinationPage> {
final TravelService _service = TravelService();
List<Destination> _destinations = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
final destinations = await _service.getDestinations();
setState(() {
_destinations = destinations;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFFFF6B4A)),
);
}
return CustomScrollView(
slivers: [
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'热门目的地',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
),
),
// 网格视图
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final dest = _destinations[index];
return _buildDestinationGridItem(dest);
},
childCount: _destinations.length,
),
),
),
// ... 更多内容
],
);
}
Widget _buildDestinationGridItem(Destination dest) {
return Column(
children: [
Expanded(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
dest.coverUrl,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
dest.name,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(height: 4),
Text(
'${dest.spotCount}个景点',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
);
}
}
八、应用入口
最后,配置应用入口文件,设置主题色和默认页面。
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'pages/travel_main_page.dart';
void main() {
runApp(const TravelGuideApp());
}
class TravelGuideApp extends StatelessWidget {
const TravelGuideApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '旅游攻略',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: const Color(0xFFFF6B4A),
scaffoldBackgroundColor: const Color(0xFFF5F5F5),
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFFF6B4A),
),
useMaterial3: true,
),
home: const TravelMainPage(),
);
}
}
九、截图运行板块
以下是旅游攻略应用在 OpenHarmony 设备上的运行截图:
1. 启动页与推荐页
图1:推荐页面截图 - 展示轮播图和热门景点列表
在推荐页面中,我们可以看到:
- 顶部轮播图自动播放,点击可跳转至对应景点详情
- 热门景点列表展示景区封面、名称、评分、标签和价格

2. 景点页面
图2:详情景点页面截图 - 展示热门景点介绍与图集
- 我们可以看到景点标签,价格,介绍,与精彩图集

十、总结
本文详细介绍了如何使用 Flutter 开发旅游攻略应用,并成功运行在 OpenHarmony 设备上。通过本文,读者可以掌握:
- Flutter 项目结构设计规范
- 数据模型的设计与定义
- 服务层的架构实现
- 底部导航栏的实现
- 下拉刷新与上拉加载的实现
- 轮播图组件的使用
- 列表视图的优化
Flutter for OpenHarmony 为开发者提供了强大的跨平台能力,一套代码即可运行在多个平台上,大大提高了开发效率。希望本文能帮助读者快速入门 Flutter 鸿蒙开发。
附录
完整代码仓库
本文涉及的完整代码已托管至 AtomGit:
依赖配置
yaml
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
cached_network_image: ^3.3.1