Flutter 框架跨平台鸿蒙开发 - 全国手账实体店查询应用开发教程
完整的店铺信息展示:名称、地址、评分、商品等智能搜索和筛选:多维度筛选条件详细的店铺页面:商品、评价、优惠信息位置服务集成:距离计算和排序优雅的UI设计:Material Design 3风格这款应用不仅功能丰富,而且充分考虑了手账爱好者的实际需求。通过本教程的学习,你可以掌握Flutter地图应用开发的核心技能。欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplat
·
Flutter全国手账实体店查询应用开发教程
项目概述
本教程将带你开发一个功能完整的Flutter全国手账实体店查询应用。这款应用专为手账爱好者设计,提供全国手账实体店的位置查询、商品浏览、店铺详情和用户评价等功能,让手账爱好者能够轻松找到心仪的手账店铺。
运行效果图



应用特色
- 全国店铺覆盖:收录全国各地的手账实体店信息
- 精准定位查询:基于GPS定位查找附近的手账店
- 丰富商品展示:展示各店铺的手账本、贴纸、胶带等商品
- 详细店铺信息:营业时间、联系方式、地址导航
- 用户评价系统:真实用户评价和评分
- 收藏管理:收藏喜欢的店铺,方便再次访问
- 分类筛选:按商品类型、价格区间、距离等筛选
- 优惠信息:实时更新店铺优惠活动信息
技术栈
- 框架:Flutter 3.x
- 语言:Dart
- UI组件:Material Design 3
- 状态管理:StatefulWidget + Provider
- 地图服务:Google Maps(模拟实现)
- 定位服务:Geolocator(模拟实现)
- 数据存储:SharedPreferences + SQLite
- 网络请求:HTTP(模拟数据)
项目结构设计
核心数据模型
1. 手账店铺模型(JournalShop)
class JournalShop {
final String id; // 唯一标识
final String name; // 店铺名称
final String description; // 店铺描述
final String address; // 详细地址
final double latitude; // 纬度
final double longitude; // 经度
final String phone; // 联系电话
final String businessHours; // 营业时间
final List<String> categories; // 商品分类
final double rating; // 评分
final int reviewCount; // 评价数量
final List<String> images; // 店铺图片
final List<Product> products; // 商品列表
final List<String> tags; // 标签
final bool isOpen; // 是否营业
final String ownerName; // 店主姓名
final DateTime establishedDate; // 开店时间
bool isFavorite; // 是否收藏
final double distance; // 距离(公里)
final List<Promotion> promotions; // 优惠活动
}
2. 商品模型(Product)
class Product {
final String id;
final String name;
final String description;
final double price;
final String category;
final List<String> images;
final String brand;
final bool inStock;
final int stockQuantity;
final List<String> tags;
final double rating;
final int reviewCount;
}
3. 用户评价模型(Review)
class Review {
final String id;
final String userId;
final String userName;
final String shopId;
final double rating;
final String comment;
final DateTime date;
final List<String> images;
final bool isVerified;
}
4. 优惠活动模型(Promotion)
class Promotion {
final String id;
final String title;
final String description;
final DateTime startDate;
final DateTime endDate;
final String discountType; // percentage, fixed, buy_one_get_one
final double discountValue;
final bool isActive;
}
页面架构
应用采用底部导航栏设计,包含四个主要页面:
- 首页:附近店铺和推荐内容
- 搜索页面:店铺搜索和筛选
- 收藏页面:收藏的店铺管理
- 我的页面:用户信息和设置
详细实现步骤
第一步:项目初始化
创建新的Flutter项目:
flutter create journal_shop_finder
cd journal_shop_finder
第二步:主应用结构
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter全国手账实体店查询',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
useMaterial3: true,
),
home: const JournalShopFinderHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
第三步:数据初始化
创建示例店铺数据:
void _initializeShops() {
_shops = [
JournalShop(
id: '1',
name: '梦想手账屋',
description: '专业手账用品店,提供各种精美手账本、贴纸、胶带等用品。',
address: '北京市朝阳区三里屯太古里南区S4-31',
latitude: 39.9370,
longitude: 116.4560,
phone: '010-85951234',
businessHours: '10:00-22:00',
categories: ['手账本', '贴纸', '胶带', '笔类'],
rating: 4.8,
reviewCount: 156,
images: ['assets/images/shop1_1.jpg', 'assets/images/shop1_2.jpg'],
products: [
Product(
id: '1',
name: 'Hobonichi手账本2024',
description: '日本原装进口手账本,优质纸张,适合各种笔类书写。',
price: 168.0,
category: '手账本',
images: ['assets/images/product1.jpg'],
brand: 'Hobonichi',
inStock: true,
stockQuantity: 25,
tags: ['进口', '优质', '热销'],
rating: 4.9,
reviewCount: 89,
),
],
tags: ['精品', '进口商品', '专业'],
isOpen: true,
ownerName: '张小美',
establishedDate: DateTime(2020, 3, 15),
isFavorite: true,
distance: 1.2,
promotions: [
Promotion(
id: '1',
title: '新品上市8折优惠',
description: '所有新品手账本享受8折优惠,限时一周!',
startDate: DateTime.now().subtract(const Duration(days: 2)),
endDate: DateTime.now().add(const Duration(days: 5)),
discountType: 'percentage',
discountValue: 20.0,
isActive: true,
),
],
),
JournalShop(
id: '2',
name: '文艺手账坊',
description: '文艺青年的手账天堂,原创设计手账本和独特贴纸。',
address: '上海市徐汇区田子坊210号',
latitude: 31.2165,
longitude: 121.4700,
phone: '021-64731234',
businessHours: '09:30-21:30',
categories: ['原创手账', '文艺贴纸', '复古胶带'],
rating: 4.6,
reviewCount: 203,
images: ['assets/images/shop2_1.jpg'],
products: [],
tags: ['原创', '文艺', '设计感'],
isOpen: true,
ownerName: '李文艺',
establishedDate: DateTime(2019, 8, 20),
isFavorite: false,
distance: 2.8,
promotions: [],
),
JournalShop(
id: '3',
name: '手账小铺',
description: '温馨小店,提供各种可爱手账用品,价格亲民。',
address: '广州市天河区正佳广场3楼A区',
latitude: 23.1291,
longitude: 113.3240,
phone: '020-38751234',
businessHours: '10:00-22:00',
categories: ['可爱贴纸', '彩色胶带', '装饰用品'],
rating: 4.4,
reviewCount: 98,
images: ['assets/images/shop3_1.jpg'],
products: [],
tags: ['可爱', '亲民', '温馨'],
isOpen: false,
ownerName: '王小萌',
establishedDate: DateTime(2021, 5, 10),
isFavorite: true,
distance: 0.8,
promotions: [],
),
JournalShop(
id: '4',
name: '手账达人工作室',
description: '专业手账教学和用品销售,提供手账制作课程。',
address: '深圳市南山区海岸城购物中心2楼',
latitude: 22.5431,
longitude: 113.9340,
phone: '0755-26781234',
businessHours: '10:00-21:00',
categories: ['教学课程', '专业工具', '高端手账'],
rating: 4.9,
reviewCount: 67,
images: ['assets/images/shop4_1.jpg'],
products: [],
tags: ['专业', '教学', '高端'],
isOpen: true,
ownerName: '陈达人',
establishedDate: DateTime(2018, 11, 3),
isFavorite: false,
distance: 5.2,
promotions: [],
),
JournalShop(
id: '5',
name: '梦幻手账世界',
description: '梦幻主题手账店,各种童话和梦幻风格的手账用品。',
address: '成都市锦江区春熙路步行街',
latitude: 30.6598,
longitude: 104.0633,
phone: '028-86541234',
businessHours: '09:00-23:00',
categories: ['梦幻贴纸', '童话胶带', '公主手账'],
rating: 4.7,
reviewCount: 134,
images: ['assets/images/shop5_1.jpg'],
products: [],
tags: ['梦幻', '童话', '公主风'],
isOpen: true,
ownerName: '林梦幻',
establishedDate: DateTime(2020, 12, 25),
isFavorite: true,
distance: 3.5,
promotions: [],
),
];
}
第四步:首页设计
位置信息和搜索栏
Widget _buildHomePage() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 位置和搜索栏
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.pink.shade300, Colors.pink.shade500],
),
),
child: SafeArea(
child: Column(
children: [
Row(
children: [
const Icon(Icons.location_on, color: Colors.white),
const SizedBox(width: 8),
Expanded(
child: Text(
_currentLocation,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.refresh, color: Colors.white),
onPressed: _refreshLocation,
),
],
),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
),
child: TextField(
decoration: const InputDecoration(
hintText: '搜索手账店铺...',
prefixIcon: Icon(Icons.search),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
),
onSubmitted: _performSearch,
),
),
],
),
),
),
// 快速筛选
_buildQuickFilters(),
// 附近店铺
_buildSectionTitle('附近店铺'),
_buildNearbyShops(),
// 推荐店铺
_buildSectionTitle('推荐店铺'),
_buildRecommendedShops(),
// 热门商品分类
_buildSectionTitle('热门分类'),
_buildPopularCategories(),
const SizedBox(height: 20),
],
),
);
}
快速筛选按钮
Widget _buildQuickFilters() {
final filters = [
{'name': '附近', 'icon': Icons.near_me, 'color': Colors.blue},
{'name': '营业中', 'icon': Icons.access_time, 'color': Colors.green},
{'name': '高评分', 'icon': Icons.star, 'color': Colors.amber},
{'name': '有优惠', 'icon': Icons.local_offer, 'color': Colors.red},
];
return Container(
height: 80,
padding: const EdgeInsets.symmetric(vertical: 16),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filters.length,
itemBuilder: (context, index) {
final filter = filters[index];
return Container(
width: 80,
margin: const EdgeInsets.only(right: 12),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: (filter['color'] as Color).withOpacity(0.1),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
filter['icon'] as IconData,
color: filter['color'] as Color,
size: 24,
),
),
const SizedBox(height: 4),
Text(
filter['name'] as String,
style: const TextStyle(fontSize: 12),
),
],
),
);
},
),
);
}
第五步:店铺卡片设计
Widget _buildShopCard(JournalShop shop) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 4,
child: InkWell(
onTap: () => _showShopDetails(shop),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 店铺头部信息
Row(
children: [
// 店铺图片
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.pink.shade300,
Colors.pink.shade500,
],
),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Icon(
Icons.store,
size: 40,
color: Colors.white,
),
),
),
const SizedBox(width: 16),
// 店铺信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
shop.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
GestureDetector(
onTap: () => _toggleFavorite(shop),
child: Icon(
shop.isFavorite ? Icons.favorite : Icons.favorite_border,
color: shop.isFavorite ? Colors.red : Colors.grey,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.star,
size: 16,
color: Colors.amber.shade600,
),
const SizedBox(width: 4),
Text(
'${shop.rating}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
Text('(${shop.reviewCount}条评价)'),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 4),
Text(
'${shop.distance.toStringAsFixed(1)}km',
style: TextStyle(color: Colors.grey.shade600),
),
const SizedBox(width: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: shop.isOpen ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Text(
shop.isOpen ? '营业中' : '已打烊',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
],
),
),
],
),
const SizedBox(height: 12),
// 店铺描述
Text(
shop.description,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// 商品分类标签
Wrap(
spacing: 8,
runSpacing: 4,
children: shop.categories.take(3).map((category) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.pink.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.pink.shade200),
),
child: Text(
category,
style: TextStyle(
color: Colors.pink.shade700,
fontSize: 12,
),
),
);
}).toList(),
),
// 优惠信息
if (shop.promotions.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(
Icons.local_offer,
size: 16,
color: Colors.red.shade600,
),
const SizedBox(width: 4),
Expanded(
child: Text(
shop.promotions.first.title,
style: TextStyle(
color: Colors.red.shade700,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
],
),
),
),
);
}
第六步:店铺详情页面
class ShopDetailPage extends StatefulWidget {
final JournalShop shop;
const ShopDetailPage({super.key, required this.shop});
State<ShopDetailPage> createState() => _ShopDetailPageState();
}
class _ShopDetailPageState extends State<ShopDetailPage>
with TickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
void dispose() {
_tabController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// 店铺头部信息
SliverAppBar(
expandedHeight: 300,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(widget.shop.name),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.pink.shade300,
Colors.pink.shade500,
],
),
),
child: Stack(
children: [
const Center(
child: Icon(
Icons.store,
size: 100,
color: Colors.white54,
),
),
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildShopBasicInfo(),
),
],
),
),
),
actions: [
IconButton(
icon: Icon(
widget.shop.isFavorite ? Icons.favorite : Icons.favorite_border,
color: widget.shop.isFavorite ? Colors.red : Colors.white,
),
onPressed: _toggleFavorite,
),
IconButton(
icon: const Icon(Icons.share),
onPressed: _shareShop,
),
],
),
// Tab栏
SliverPersistentHeader(
pinned: true,
delegate: _SliverTabBarDelegate(
TabBar(
controller: _tabController,
tabs: const [
Tab(text: '详情'),
Tab(text: '商品'),
Tab(text: '评价'),
Tab(text: '优惠'),
],
),
),
),
// Tab内容
SliverFillRemaining(
child: TabBarView(
controller: _tabController,
children: [
_buildDetailTab(),
_buildProductsTab(),
_buildReviewsTab(),
_buildPromotionsTab(),
],
),
),
],
),
bottomNavigationBar: _buildBottomActions(),
);
}
Widget _buildShopBasicInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(
Icons.star,
color: Colors.amber.shade600,
size: 20,
),
const SizedBox(width: 4),
Text(
'${widget.shop.rating}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(width: 8),
Text('${widget.shop.reviewCount}条评价'),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: widget.shop.isOpen ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(12),
),
child: Text(
widget.shop.isOpen ? '营业中' : '已打烊',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.location_on, size: 16),
const SizedBox(width: 4),
Text('距离 ${widget.shop.distance.toStringAsFixed(1)}km'),
const SizedBox(width: 16),
const Icon(Icons.access_time, size: 16),
const SizedBox(width: 4),
Text(widget.shop.businessHours),
],
),
],
),
);
}
Widget _buildDetailTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 店铺介绍
const Text(
'店铺介绍',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
widget.shop.description,
style: const TextStyle(fontSize: 16, height: 1.5),
),
const SizedBox(height: 24),
// 联系信息
const Text(
'联系信息',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_buildInfoRow(Icons.location_on, '地址', widget.shop.address),
_buildInfoRow(Icons.phone, '电话', widget.shop.phone),
_buildInfoRow(Icons.access_time, '营业时间', widget.shop.businessHours),
_buildInfoRow(Icons.person, '店主', widget.shop.ownerName),
const SizedBox(height: 24),
// 商品分类
const Text(
'主营商品',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 8,
children: widget.shop.categories.map((category) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.pink.shade50,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.pink.shade200),
),
child: Text(
category,
style: TextStyle(
color: Colors.pink.shade700,
fontWeight: FontWeight.bold,
),
),
);
}).toList(),
),
const SizedBox(height: 24),
// 地图位置
const Text(
'店铺位置',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.map, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text('地图加载中...', style: TextStyle(color: Colors.grey)),
],
),
),
),
],
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.pink),
const SizedBox(width: 12),
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Expanded(
child: Text(value),
),
],
),
);
}
Widget _buildProductsTab() {
return widget.shop.products.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无商品信息', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: widget.shop.products.length,
itemBuilder: (context, index) {
final product = widget.shop.products[index];
return _buildProductCard(product);
},
);
}
Widget _buildProductCard(Product product) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.pink.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Icon(Icons.book, size: 32, color: Colors.pink),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
product.description,
style: TextStyle(color: Colors.grey.shade600),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
children: [
Text(
'¥${product.price.toStringAsFixed(0)}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: product.inStock ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(12),
),
child: Text(
product.inStock ? '有货' : '缺货',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildReviewsTab() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.rate_review, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无评价信息', style: TextStyle(color: Colors.grey)),
],
),
);
}
Widget _buildPromotionsTab() {
return widget.shop.promotions.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.local_offer, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无优惠活动', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: widget.shop.promotions.length,
itemBuilder: (context, index) {
final promotion = widget.shop.promotions[index];
return _buildPromotionCard(promotion);
},
);
}
Widget _buildPromotionCard(Promotion promotion) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [Colors.red.shade50, Colors.red.shade100],
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.local_offer, color: Colors.red.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
promotion.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
),
if (promotion.isActive)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'进行中',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Text(
promotion.description,
style: TextStyle(color: Colors.grey.shade700),
),
const SizedBox(height: 8),
Text(
'活动时间:${_formatDate(promotion.startDate)} - ${_formatDate(promotion.endDate)}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
],
),
),
),
);
}
Widget _buildBottomActions() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _callShop,
icon: const Icon(Icons.phone),
label: const Text('拨打电话'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _navigateToShop,
icon: const Icon(Icons.navigation),
label: const Text('导航前往'),
),
),
],
),
);
}
String _formatDate(DateTime date) {
return '${date.month}/${date.day}';
}
void _toggleFavorite() {
setState(() {
widget.shop.isFavorite = !widget.shop.isFavorite;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.shop.isFavorite ? '已添加到收藏' : '已从收藏中移除'),
duration: const Duration(seconds: 1),
),
);
}
void _shareShop() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('分享功能开发中...')),
);
}
void _callShop() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('拨打电话:${widget.shop.phone}')),
);
}
void _navigateToShop() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('正在启动导航...')),
);
}
}
// 自定义SliverTabBarDelegate
class _SliverTabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar tabBar;
_SliverTabBarDelegate(this.tabBar);
double get minExtent => tabBar.preferredSize.height;
double get maxExtent => tabBar.preferredSize.height;
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
child: tabBar,
);
}
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
第七步:搜索和筛选功能
Widget _buildSearchPage() {
return Column(
children: [
// 搜索栏
Container(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索店铺名称或商品...',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.tune),
onPressed: _showFilterDialog,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
),
),
onChanged: _onSearchChanged,
),
),
// 筛选标签
if (_activeFilters.isNotEmpty) _buildActiveFilters(),
// 搜索结果
Expanded(
child: _filteredShops.isEmpty
? _buildEmptySearchResult()
: ListView.builder(
itemCount: _filteredShops.length,
itemBuilder: (context, index) {
return _buildShopCard(_filteredShops[index]);
},
),
),
],
);
}
void _showFilterDialog() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
maxChildSize: 0.9,
minChildSize: 0.5,
builder: (context, scrollController) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'筛选条件',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
controller: scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFilterSection('营业状态', [
{'label': '营业中', 'value': 'open'},
{'label': '已打烊', 'value': 'closed'},
]),
_buildFilterSection('距离', [
{'label': '1km内', 'value': '1km'},
{'label': '3km内', 'value': '3km'},
{'label': '5km内', 'value': '5km'},
]),
_buildFilterSection('评分', [
{'label': '4.5分以上', 'value': '4.5+'},
{'label': '4.0分以上', 'value': '4.0+'},
{'label': '3.5分以上', 'value': '3.5+'},
]),
_buildFilterSection('商品类型', [
{'label': '手账本', 'value': '手账本'},
{'label': '贴纸', 'value': '贴纸'},
{'label': '胶带', 'value': '胶带'},
{'label': '笔类', 'value': '笔类'},
]),
],
),
),
),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _clearFilters,
child: const Text('清除筛选'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_applyFilters();
},
child: const Text('应用筛选'),
),
),
],
),
],
),
);
},
),
);
}
第八步:收藏管理
Widget _buildFavoritePage() {
final favoriteShops = _shops.where((shop) => shop.isFavorite).toList();
return Column(
children: [
if (favoriteShops.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.favorite, color: Colors.red),
const SizedBox(width: 8),
Text(
'我的收藏 (${favoriteShops.length})',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Spacer(),
TextButton(
onPressed: _clearAllFavorites,
child: const Text('清空收藏'),
),
],
),
),
],
Expanded(
child: favoriteShops.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 80, color: Colors.grey),
SizedBox(height: 16),
Text(
'暂无收藏店铺',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'收藏喜欢的店铺,方便下次查看',
style: TextStyle(color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: favoriteShops.length,
itemBuilder: (context, index) {
return _buildShopCard(favoriteShops[index]);
},
),
),
],
);
}
核心功能详解
1. 位置服务模拟
void _getCurrentLocation() async {
// 模拟获取当前位置
setState(() {
_currentLocation = '北京市朝阳区';
_userLatitude = 39.9370;
_userLongitude = 116.4560;
});
// 更新店铺距离
_updateShopDistances();
}
void _updateShopDistances() {
for (var shop in _shops) {
// 简单的距离计算模拟
final latDiff = (shop.latitude - _userLatitude).abs();
final lngDiff = (shop.longitude - _userLongitude).abs();
shop.distance = (latDiff + lngDiff) * 100; // 简化计算
}
// 按距离排序
_shops.sort((a, b) => a.distance.compareTo(b.distance));
setState(() {});
}
2. 搜索算法
void _performSearch(String query) {
if (query.isEmpty) {
setState(() {
_filteredShops = List.from(_shops);
});
return;
}
setState(() {
_filteredShops = _shops.where((shop) {
final nameMatch = shop.name.toLowerCase().contains(query.toLowerCase());
final descMatch = shop.description.toLowerCase().contains(query.toLowerCase());
final categoryMatch = shop.categories.any((category) =>
category.toLowerCase().contains(query.toLowerCase()));
final tagMatch = shop.tags.any((tag) =>
tag.toLowerCase().contains(query.toLowerCase()));
return nameMatch || descMatch || categoryMatch || tagMatch;
}).toList();
});
}
3. 筛选逻辑
void _applyFilters() {
List<JournalShop> filtered = List.from(_shops);
// 营业状态筛选
if (_selectedFilters.contains('open')) {
filtered = filtered.where((shop) => shop.isOpen).toList();
}
if (_selectedFilters.contains('closed')) {
filtered = filtered.where((shop) => !shop.isOpen).toList();
}
// 距离筛选
if (_selectedFilters.contains('1km')) {
filtered = filtered.where((shop) => shop.distance <= 1.0).toList();
}
if (_selectedFilters.contains('3km')) {
filtered = filtered.where((shop) => shop.distance <= 3.0).toList();
}
if (_selectedFilters.contains('5km')) {
filtered = filtered.where((shop) => shop.distance <= 5.0).toList();
}
// 评分筛选
if (_selectedFilters.contains('4.5+')) {
filtered = filtered.where((shop) => shop.rating >= 4.5).toList();
}
if (_selectedFilters.contains('4.0+')) {
filtered = filtered.where((shop) => shop.rating >= 4.0).toList();
}
// 商品类型筛选
final categoryFilters = _selectedFilters.where((filter) =>
['手账本', '贴纸', '胶带', '笔类'].contains(filter)).toList();
if (categoryFilters.isNotEmpty) {
filtered = filtered.where((shop) =>
shop.categories.any((category) => categoryFilters.contains(category))).toList();
}
setState(() {
_filteredShops = filtered;
});
}
性能优化
1. 列表优化
使用ListView.builder实现虚拟滚动:
ListView.builder(
itemCount: shops.length,
itemExtent: 200, // 固定高度提高性能
itemBuilder: (context, index) {
return _buildShopCard(shops[index]);
},
)
2. 图片缓存
使用渐变色作为占位符,减少内存使用:
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.pink.shade300, Colors.pink.shade500],
),
),
)
3. 状态管理优化
合理使用setState,避免不必要的重建:
void _updateShopList() {
if (mounted) {
setState(() {
// 只更新必要的状态
});
}
}
扩展功能
1. 真实地图集成
可以集成Google Maps或高德地图:
dependencies:
google_maps_flutter: ^2.5.0
2. 位置服务
集成真实的位置服务:
dependencies:
geolocator: ^10.1.0
3. 推送通知
使用flutter_local_notifications:
dependencies:
flutter_local_notifications: ^16.1.0
测试策略
1. 单元测试
测试核心业务逻辑:
test('should filter shops by distance correctly', () {
final shops = [
JournalShop(distance: 0.5),
JournalShop(distance: 2.0),
JournalShop(distance: 4.0),
];
final filtered = shops.where((shop) => shop.distance <= 1.0).toList();
expect(filtered.length, equals(1));
});
2. Widget测试
测试UI组件:
testWidgets('should display shop name', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
expect(find.text('梦想手账屋'), findsOneWidget);
});
部署发布
1. Android打包
flutter build apk --release
2. iOS打包
flutter build ios --release
总结
本教程详细介绍了Flutter全国手账实体店查询应用的完整开发过程,涵盖了:
- 完整的店铺信息展示:名称、地址、评分、商品等
- 智能搜索和筛选:多维度筛选条件
- 详细的店铺页面:商品、评价、优惠信息
- 收藏管理功能:个人收藏夹
- 位置服务集成:距离计算和排序
- 优雅的UI设计:Material Design 3风格
这款应用不仅功能丰富,而且充分考虑了手账爱好者的实际需求。通过本教程的学习,你可以掌握Flutter地图应用开发的核心技能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)