Flutter全国文创印章店查询应用开发教程

项目概述

本教程将带你开发一个功能完整的Flutter全国文创印章店查询应用。这款应用专为文创爱好者和印章收藏者设计,提供全国范围内文创印章店的查询、导航、预约和评价功能,让用户轻松找到心仪的文创印章店。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 全国店铺覆盖:涵盖全国各地的文创印章店信息
  • 智能定位系统:基于GPS定位推荐附近店铺
  • 多维度筛选:按距离、评分、价格、服务类型等筛选
  • 实时状态查询:显示店铺营业状态、繁忙程度等
  • 在线预约服务:支持提前预约制作时间
  • 用户评价系统:查看其他用户的真实评价和作品展示
  • 收藏管理:收藏常用店铺,方便下次查找
  • 导航集成:一键导航到目标店铺
  • 价格对比:对比不同店铺的服务价格
  • 作品展示:浏览店铺的精品作品案例

技术栈

  • 框架:Flutter 3.x
  • 语言:Dart
  • UI组件:Material Design 3
  • 状态管理:StatefulWidget
  • 动画:AnimationController + Tween
  • 数据存储:内存存储(可扩展为本地数据库)

项目结构设计

核心数据模型

1. 印章店模型(SealShop)
class SealShop {
  final String id;              // 唯一标识
  final String name;            // 店铺名称
  final String address;         // 详细地址
  final double latitude;        // 纬度
  final double longitude;       // 经度
  final double distance;        // 距离用户的距离
  final String phone;           // 联系电话
  final List<String> services;  // 服务项目
  final Map<String, double> prices; // 价格表
  final double rating;          // 评分
  final int reviewCount;        // 评价数量
  final String operatingHours;  // 营业时间
  final ShopStatus status;      // 营业状态
  final List<String> photos;    // 店铺照片
  final List<String> features;  // 特色服务
  final String description;     // 店铺描述
  final DateTime lastUpdated;   // 最后更新时间
  bool isFavorite;             // 是否收藏
  final String ownerName;       // 店主姓名
  final int experienceYears;    // 从业年限
  final List<String> specialties; // 专长领域
}
2. 状态枚举
enum ShopStatus { open, closed, busy, appointment, maintenance }

enum ServiceType {
  traditional,    // 传统印章
  creative,       // 创意印章
  corporate,      // 企业印章
  personal,       // 个人印章
  artistic,       // 艺术印章
  custom,         // 定制印章
}

enum ReservationStatus {
  pending,        // 待确认
  confirmed,      // 已确认
  inProgress,     // 制作中
  completed,      // 已完成
  cancelled,      // 已取消
  expired,        // 已过期
}
3. 评价模型(Review)
class Review {
  final String id;
  final String shopId;
  final String userId;
  final String userName;
  final String userAvatar;
  final double rating;
  final String content;
  final List<String> photos;
  final DateTime createTime;
  final List<String> tags;
  final String serviceType;
  final bool isRecommended;
  final double serviceRating;
  final double qualityRating;
  final double priceRating;
}
4. 预约模型(Reservation)
class Reservation {
  final String id;
  final String shopId;
  final String userId;
  final DateTime reservationTime;
  final String serviceType;
  final String sealContent;
  final String material;
  final String size;
  final double estimatedPrice;
  final ReservationStatus status;
  final String notes;
  final DateTime createTime;
  final String? cancellationReason;
  final DateTime? completionTime;
}

页面架构

应用采用底部导航栏设计,包含五个主要页面:

  1. 地图页面:地图显示附近印章店,支持标记和筛选
  2. 列表页面:列表形式浏览所有店铺,支持搜索和排序
  3. 预约页面:管理预约记录和预约历史
  4. 收藏页面:管理收藏的店铺
  5. 个人页面:用户信息、设置和其他功能

详细实现步骤

第一步:项目初始化

创建新的Flutter项目:

flutter create seal_shop_finder
cd seal_shop_finder

第二步:主应用结构

class SealShopFinderApp extends StatelessWidget {
  const SealShopFinderApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter全国文创印章店查询',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
        useMaterial3: true,
      ),
      home: const SealShopFinderHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

第三步:数据初始化

创建示例印章店数据:

void _initializeData() {
  _sealShops.addAll([
    SealShop(
      id: '1',
      name: '墨韵印章工艺坊',
      address: '北京市朝阳区文化街88号',
      latitude: 39.9042,
      longitude: 116.4074,
      distance: 0.5,
      phone: '010-12345678',
      services: ['传统印章', '创意印章', '企业印章', '艺术印章'],
      prices: {
        '传统印章': 50.0,
        '创意印章': 80.0,
        '企业印章': 120.0,
        '艺术印章': 200.0,
      },
      rating: 4.8,
      reviewCount: 156,
      operatingHours: '09:00-18:00',
      status: ShopStatus.open,
      photos: [],
      features: ['手工雕刻', '当日取件', '免费设计', '包装精美'],
      description: '专业从事印章制作20年,手工雕刻,品质保证。',
      lastUpdated: DateTime.now().subtract(const Duration(minutes: 5)),
      isFavorite: true,
      ownerName: '张师傅',
      experienceYears: 20,
      specialties: ['篆刻艺术', '古文字研究', '印章设计'],
    ),
    // 更多店铺数据...
  ]);
}

第四步:地图页面实现

地图视图组件
Widget _buildMapPage() {
  return Stack(
    children: [
      // 地图容器
      Container(
        width: double.infinity,
        height: double.infinity,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.red.shade100, Colors.red.shade50],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
          ),
        ),
        child: _buildMapView(),
      ),
      
      // 顶部搜索栏
      Positioned(
        top: 0,
        left: 0,
        right: 0,
        child: _buildSearchBar(),
      ),
      
      // 底部店铺信息卡片
      if (_selectedShop != null)
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: _buildShopInfoCard(_selectedShop!),
        ),
      
      // 右侧功能按钮
      Positioned(
        right: 16,
        top: 100,
        child: _buildMapControls(),
      ),
    ],
  );
}
店铺标记组件
Widget _buildShopMarker(SealShop shop) {
  final isSelected = _selectedShop?.id == shop.id;
  
  return Positioned(
    left: _getMarkerX(shop.longitude),
    top: _getMarkerY(shop.latitude),
    child: GestureDetector(
      onTap: () => _selectShop(shop),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        width: isSelected ? 60 : 40,
        height: isSelected ? 60 : 40,
        decoration: BoxDecoration(
          color: _getStatusColor(shop.status),
          shape: BoxShape.circle,
          border: Border.all(color: Colors.white, width: 3),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.2),
              blurRadius: 8,
              spreadRadius: 2,
            ),
          ],
        ),
        child: Icon(
          Icons.store,
          color: Colors.white,
          size: isSelected ? 30 : 20,
        ),
      ),
    ),
  );
}

第五步:列表页面实现

店铺卡片组件
Widget _buildShopCard(SealShop shop) {
  return Card(
    elevation: 4,
    margin: const EdgeInsets.only(bottom: 16),
    child: InkWell(
      onTap: () => _showShopDetail(shop),
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 标题行
            Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        shop.name,
                        style: const TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Row(
                        children: [
                          Icon(Icons.location_on, 
                               size: 16, 
                               color: Colors.grey.shade600),
                          const SizedBox(width: 4),
                          Expanded(
                            child: Text(
                              shop.address,
                              style: TextStyle(
                                color: Colors.grey.shade600,
                                fontSize: 14,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                        vertical: 4,
                      ),
                      decoration: BoxDecoration(
                        color: _getStatusColor(shop.status).withOpacity(0.1),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: Text(
                        _getStatusText(shop.status),
                        style: TextStyle(
                          color: _getStatusColor(shop.status),
                          fontSize: 12,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      '${shop.distance.toStringAsFixed(1)}km',
                      style: TextStyle(
                        color: Colors.grey.shade600,
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 评分和价格信息
            Row(
              children: [
                Row(
                  children: [
                    Icon(Icons.star, color: Colors.amber, size: 16),
                    const SizedBox(width: 4),
                    Text(
                      '${shop.rating.toStringAsFixed(1)}',
                      style: const TextStyle(fontWeight: FontWeight.w500),
                    ),
                    Text(
                      ' (${shop.reviewCount})',
                      style: TextStyle(
                        color: Colors.grey.shade600,
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
                const Spacer(),
                Text(
                  '起价 ¥${_getMinPrice(shop).toStringAsFixed(0)}',
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 师傅信息
            Container(
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: Colors.red.withOpacity(0.1),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Row(
                children: [
                  Icon(Icons.person, color: Colors.red, size: 16),
                  const SizedBox(width: 8),
                  Text('${shop.ownerName}${shop.experienceYears}年经验'),
                  const Spacer(),
                  Text(
                    '专业师傅',
                    style: TextStyle(
                      color: Colors.red,
                      fontWeight: FontWeight.bold,
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
            
            const SizedBox(height: 12),
            
            // 服务标签
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: shop.services.take(3).map((service) => Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: Colors.red.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text(
                  service,
                  style: const TextStyle(fontSize: 12, color: Colors.red),
                ),
              )).toList(),
            ),
            
            const SizedBox(height: 12),
            
            // 操作按钮
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () => _navigateToShop(shop),
                    icon: const Icon(Icons.directions, size: 16),
                    label: const Text('导航'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: () => _makeReservation(shop),
                    icon: const Icon(Icons.schedule, size: 16),
                    label: const Text('预约'),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton(
                  onPressed: () => _toggleFavorite(shop),
                  icon: Icon(
                    shop.isFavorite ? Icons.favorite : Icons.favorite_border,
                    color: shop.isFavorite ? Colors.red : Colors.grey,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

第六步:预约功能实现

预约对话框
void _makeReservation(SealShop shop) {
  if (shop.status == ShopStatus.closed) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('该店铺暂时关闭,无法预约')),
    );
    return;
  }

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('预约 ${shop.name}'),
      content: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('店铺: ${shop.name}'),
            const SizedBox(height: 8),
            Text('地址: ${shop.address}'),
            const SizedBox(height: 16),
            
            const Text('选择服务类型:', 
                 style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            ...shop.services.map((service) => RadioListTile<String>(
              title: Text(service),
              subtitle: Text(${shop.prices[service]?.toStringAsFixed(0) ?? "0"}'),
              value: service,
              groupValue: _selectedService,
              onChanged: (value) {
                setState(() {
                  _selectedService = value;
                });
              },
            )),
            
            const SizedBox(height: 16),
            TextField(
              decoration: const InputDecoration(
                labelText: '印章内容',
                hintText: '请输入要刻制的文字',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) => _sealContent = value,
            ),
            
            const SizedBox(height: 16),
            TextField(
              decoration: const InputDecoration(
                labelText: '备注',
                hintText: '特殊要求或说明',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
              onChanged: (value) => _reservationNotes = value,
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            if (_selectedService != null && _sealContent.isNotEmpty) {
              _confirmReservation(shop);
              Navigator.pop(context);
            } else {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('请填写完整信息')),
              );
            }
          },
          child: const Text('确认预约'),
        ),
      ],
    ),
  );
}

第七步:搜索和筛选功能

搜索栏组件
Widget _buildSearchBar() {
  return Container(
    margin: const EdgeInsets.all(16),
    padding: const EdgeInsets.symmetric(horizontal: 16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(25),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.1),
          blurRadius: 10,
          spreadRadius: 2,
        ),
      ],
    ),
    child: Row(
      children: [
        const Icon(Icons.search, color: Colors.grey),
        const SizedBox(width: 12),
        Expanded(
          child: TextField(
            decoration: const InputDecoration(
              hintText: '搜索印章店...',
              border: InputBorder.none,
            ),
            onChanged: (value) {
              setState(() {
                _searchQuery = value;
              });
              _filterShops();
            },
          ),
        ),
        IconButton(
          icon: const Icon(Icons.tune, color: Colors.red),
          onPressed: _showFilterDialog,
        ),
      ],
    ),
  );
}
筛选功能
void _filterShops() {
  setState(() {
    _filteredShops = _sealShops.where((shop) {
      bool matchesSearch = _searchQuery.isEmpty ||
          shop.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
          shop.address.toLowerCase().contains(_searchQuery.toLowerCase()) ||
          shop.services.any((service) => 
              service.toLowerCase().contains(_searchQuery.toLowerCase()));

      bool matchesStatus = _selectedStatus == null ||
          shop.status == _selectedStatus;

      bool matchesDistance = shop.distance <= _maxDistance;

      bool matchesRating = shop.rating >= _minRating;

      return matchesSearch && matchesStatus && matchesDistance && matchesRating;
    }).toList();
  });
  _sortShops();
}

第八步:收藏功能

收藏切换
void _toggleFavorite(SealShop shop) {
  setState(() {
    shop.isFavorite = !shop.isFavorite;

    if (shop.isFavorite) {
      if (!_favoriteShops.contains(shop)) {
        _favoriteShops.add(shop);
      }
    } else {
      _favoriteShops.remove(shop);
    }
  });

  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(shop.isFavorite ? '已添加到收藏' : '已从收藏中移除'),
      duration: const Duration(seconds: 1),
    ),
  );
}
收藏页面
Widget _buildFavoritePage() {
  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
            ? _buildEmptyState('暂无收藏', '收藏常用印章店,方便下次查找')
            : ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _favoriteShops.length,
                itemBuilder: (context, index) => 
                    _buildShopCard(_favoriteShops[index]),
              ),
      ),
    ],
  );
}

第九步:店铺详情页面

void _showShopDetail(SealShop shop) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    builder: (context) => DraggableScrollableSheet(
      initialChildSize: 0.7,
      maxChildSize: 0.9,
      minChildSize: 0.5,
      builder: (context, scrollController) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 拖拽指示器
            Center(
              child: Container(
                width: 40,
                height: 4,
                decoration: BoxDecoration(
                  color: Colors.grey.shade300,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            const SizedBox(height: 16),
            
            // 店铺基本信息
            Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        shop.name,
                        style: const TextStyle(
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        shop.address,
                        style: TextStyle(color: Colors.grey.shade600),
                      ),
                    ],
                  ),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 6,
                  ),
                  decoration: BoxDecoration(
                    color: _getStatusColor(shop.status).withOpacity(0.1),
                    borderRadius: BorderRadius.circular(16),
                  ),
                  child: Text(
                    _getStatusText(shop.status),
                    style: TextStyle(
                      color: _getStatusColor(shop.status),
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
            
            const SizedBox(height: 16),
            
            // 评分和师傅信息
            Row(
              children: [
                Icon(Icons.star, color: Colors.amber, size: 20),
                const SizedBox(width: 4),
                Text(
                  '${shop.rating.toStringAsFixed(1)}',
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                  ),
                ),
                Text(
                  ' (${shop.reviewCount}条评价)',
                  style: TextStyle(color: Colors.grey.shade600),
                ),
                const Spacer(),
                Text(
                  '${shop.ownerName}${shop.experienceYears}年',
                  style: const TextStyle(fontWeight: FontWeight.w500),
                ),
              ],
            ),
            
            const SizedBox(height: 16),
            
            // 详细内容
            Expanded(
              child: ListView(
                controller: scrollController,
                children: [
                  // 店铺描述
                  const Text(
                    '店铺介绍',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(shop.description),
                  
                  const SizedBox(height: 16),
                  
                  // 服务项目和价格
                  const Text(
                    '服务项目',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  ...shop.services.map((service) => Card(
                    child: ListTile(
                      title: Text(service),
                      trailing: Text(
                        ${shop.prices[service]?.toStringAsFixed(0) ?? "0"}',
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                          color: Colors.green,
                        ),
                      ),
                    ),
                  )),
                  
                  const SizedBox(height: 16),
                  
                  // 专长领域
                  const Text(
                    '专长领域',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: shop.specialties.map((specialty) => Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 12,
                        vertical: 6,
                      ),
                      decoration: BoxDecoration(
                        color: Colors.red.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: Text(
                        specialty,
                        style: const TextStyle(color: Colors.red),
                      ),
                    )).toList(),
                  ),
                  
                  const SizedBox(height: 16),
                  
                  // 特色服务
                  const Text(
                    '特色服务',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: shop.features.map((feature) => Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 12,
                        vertical: 6,
                      ),
                      decoration: BoxDecoration(
                        color: Colors.blue.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: Text(
                        feature,
                        style: const TextStyle(color: Colors.blue),
                      ),
                    )).toList(),
                  ),
                  
                  const SizedBox(height: 16),
                  
                  // 营业时间
                  const Text(
                    '营业时间',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    shop.operatingHours,
                    style: const TextStyle(fontSize: 16),
                  ),
                  
                  const SizedBox(height: 16),
                  
                  // 联系电话
                  const Text(
                    '联系电话',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    shop.phone,
                    style: const TextStyle(fontSize: 16),
                  ),
                ],
              ),
            ),
            
            // 底部按钮
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {
                      Navigator.pop(context);
                      _navigateToShop(shop);
                    },
                    icon: const Icon(Icons.directions),
                    label: const Text('导航'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: () {
                      Navigator.pop(context);
                      _makeReservation(shop);
                    },
                    icon: const Icon(Icons.schedule),
                    label: const Text('预约'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

核心功能详解

1. 地理位置计算

double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
  const double earthRadius = 6371; // 地球半径(公里)
  
  double dLat = _degreesToRadians(lat2 - lat1);
  double dLon = _degreesToRadians(lon2 - lon1);
  
  double a = sin(dLat / 2) * sin(dLat / 2) +
      cos(_degreesToRadians(lat1)) * cos(_degreesToRadians(lat2)) *
      sin(dLon / 2) * sin(dLon / 2);
  
  double c = 2 * atan2(sqrt(a), sqrt(1 - a));
  
  return earthRadius * c;
}

double _degreesToRadians(double degrees) {
  return degrees * (pi / 180);
}

2. 状态颜色系统

Color _getStatusColor(ShopStatus status) {
  switch (status) {
    case ShopStatus.open: return Colors.green;
    case ShopStatus.closed: return Colors.grey;
    case ShopStatus.busy: return Colors.orange;
    case ShopStatus.appointment: return Colors.blue;
    case ShopStatus.maintenance: return Colors.red;
  }
}

String _getStatusText(ShopStatus status) {
  switch (status) {
    case ShopStatus.open: return '营业中';
    case ShopStatus.closed: return '已关闭';
    case ShopStatus.busy: return '繁忙';
    case ShopStatus.appointment: return '仅预约';
    case ShopStatus.maintenance: return '维护中';
  }
}

3. 排序算法

void _sortShops() {
  setState(() {
    switch (_currentFilter) {
      case FilterType.distance:
        _filteredShops.sort((a, b) => a.distance.compareTo(b.distance));
        break;
      case FilterType.rating:
        _filteredShops.sort((a, b) => b.rating.compareTo(a.rating));
        break;
      case FilterType.price:
        _filteredShops.sort((a, b) => _getMinPrice(a).compareTo(_getMinPrice(b)));
        break;
      case FilterType.experience:
        _filteredShops.sort((a, b) => b.experienceYears.compareTo(a.experienceYears));
        break;
    }
  });
}

性能优化

1. 列表优化

使用ListView.builder实现虚拟滚动:

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: _filteredShops.length,
  itemBuilder: (context, index) {
    final shop = _filteredShops[index];
    return _buildShopCard(shop);
  },
)

2. 图片加载优化

Widget _buildShopImage(String imageUrl) {
  return FadeInImage.assetNetwork(
    placeholder: 'assets/images/placeholder.png',
    image: imageUrl,
    fit: BoxFit.cover,
    fadeInDuration: const Duration(milliseconds: 300),
    imageErrorBuilder: (context, error, stackTrace) {
      return Container(
        color: Colors.grey.shade200,
        child: const Icon(Icons.image_not_supported),
      );
    },
  );
}

3. 状态管理优化

合理使用setState,避免不必要的重建:

void _updateShopStatus(String shopId, ShopStatus newStatus) {
  setState(() {
    final shopIndex = _sealShops.indexWhere((shop) => shop.id == shopId);
    if (shopIndex != -1) {
      // 只更新特定店铺的状态
      _sealShops[shopIndex] = SealShop(
        // 复制所有属性,只更新status
        id: _sealShops[shopIndex].id,
        name: _sealShops[shopIndex].name,
        // ... 其他属性
        status: newStatus,
        // ... 其他属性
      );
    }
  });
}

扩展功能

1. 地图集成

可以集成Google Maps或高德地图:

dependencies:
  google_maps_flutter: ^2.5.0
  # 或者
  amap_flutter_map: ^3.0.0

2. 定位服务

使用geolocator获取用户位置:

dependencies:
  geolocator: ^10.1.0

3. 本地存储

使用shared_preferences保存用户设置:

dependencies:
  shared_preferences: ^2.2.0

4. 网络请求

集成http插件实现在线数据获取:

dependencies:
  http: ^1.1.0

测试策略

1. 单元测试

测试核心业务逻辑:

test('should calculate distance correctly', () {
  final distance = _calculateDistance(39.9042, 116.4074, 39.9142, 116.4174);
  expect(distance, greaterThan(0));
  expect(distance, lessThan(2)); // 应该小于2公里
});

2. Widget测试

测试UI组件:

testWidgets('should display shop name', (WidgetTester tester) async {
  await tester.pumpWidget(MyApp());
  expect(find.text('墨韵印章工艺坊'), findsOneWidget);
});

3. 集成测试

测试完整用户流程:

testWidgets('should navigate to shop detail when card is tapped', 
    (WidgetTester tester) async {
  await tester.pumpWidget(MyApp());
  await tester.tap(find.text('墨韵印章工艺坊'));
  await tester.pumpAndSettle();
  
  expect(find.text('店铺介绍'), findsOneWidget);
});

部署发布

1. Android打包

flutter build apk --release

2. iOS打包

flutter build ios --release

3. 应用商店发布

准备应用图标、截图和描述,提交到各大应用商店。

总结

本教程详细介绍了Flutter全国文创印章店查询应用的完整开发过程,涵盖了:

  • 数据模型设计:合理的数据结构设计
  • UI界面开发:Material Design 3风格界面
  • 功能实现:地图显示、搜索筛选、预约管理、收藏功能
  • 地理位置服务:距离计算、位置标记
  • 用户体验优化:动画效果、加载状态、错误处理
  • 性能优化:列表优化、状态管理、内存管理
  • 扩展功能:地图集成、定位服务、本地存储
  • 测试策略:单元测试、Widget测试、集成测试

这款应用不仅功能完整,而且代码结构清晰,易于维护和扩展。通过本教程的学习,你可以掌握Flutter应用开发的核心技能,为后续开发更复杂的应用打下坚实基础。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐