Flutter附近文创市集查询应用开发教程

项目概述

本教程将带你开发一个功能完整的Flutter附近文创市集查询应用。这款应用专为文创爱好者和艺术家设计,提供附近文创市集的实时信息查询、活动详情浏览、摊位预订和社区交流等功能,让用户能够轻松发现和参与各种文创活动。
运行效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 实时市集信息:展示附近正在进行和即将举办的文创市集
  • 精准定位查询:基于GPS定位查找附近的文创活动
  • 丰富活动详情:展示市集时间、地点、主题、参展商家等信息
  • 摊位预订功能:支持在线预订市集摊位
  • 社区互动:用户评价、照片分享、活动打卡
  • 收藏管理:收藏感兴趣的市集和活动
  • 分类筛选:按主题、时间、距离等条件筛选
  • 活动提醒:设置感兴趣活动的提醒通知

技术栈

  • 框架:Flutter 3.x
  • 语言:Dart
  • UI组件:Material Design 3
  • 状态管理:StatefulWidget + Provider
  • 地图服务:Google Maps(模拟实现)
  • 定位服务:Geolocator(模拟实现)
  • 数据存储:SharedPreferences + SQLite
  • 网络请求:HTTP(模拟数据)

项目结构设计

核心数据模型

1. 文创市集模型(CreativeMarket)
class CreativeMarket {
  final String id;              // 唯一标识
  final String name;            // 市集名称
  final String description;     // 市集描述
  final String theme;           // 主题分类
  final String address;         // 详细地址
  final double latitude;        // 纬度
  final double longitude;       // 经度
  final DateTime startDate;     // 开始时间
  final DateTime endDate;       // 结束时间
  final String organizer;       // 主办方
  final String contact;         // 联系方式
  final List<String> images;    // 市集图片
  final List<Vendor> vendors;   // 参展商家
  final List<String> tags;      // 标签
  final MarketStatus status;    // 市集状态
  final double entryFee;        // 入场费
  final int expectedVisitors;   // 预计参观人数
  bool isFavorite;             // 是否收藏
  final double distance;        // 距离(公里)
  final List<Activity> activities; // 相关活动
  final double rating;          // 评分
  final int reviewCount;        // 评价数量
}
2. 参展商家模型(Vendor)
class Vendor {
  final String id;
  final String name;
  final String description;
  final String category;
  final List<String> products;
  final String contact;
  final List<String> images;
  final String boothNumber;
  final bool isBookable;
  final double boothFee;
}
3. 活动模型(Activity)
class Activity {
  final String id;
  final String name;
  final String description;
  final DateTime startTime;
  final DateTime endTime;
  final String location;
  final String type; // workshop, performance, exhibition
  final bool requiresRegistration;
  final int maxParticipants;
  final int currentParticipants;
}
4. 市集状态枚举
enum MarketStatus {
  upcoming,     // 即将开始
  ongoing,      // 进行中
  ended,        // 已结束
  cancelled,    // 已取消
}

页面架构

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

  1. 首页:附近市集和推荐活动
  2. 发现页面:市集搜索和分类浏览
  3. 我的活动:收藏和参与的活动
  4. 个人中心:用户信息和设置

详细实现步骤

第一步:项目初始化

创建新的Flutter项目:

flutter create creative_market_finder
cd creative_market_finder

第二步:主应用结构

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter附近文创市集查询',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const CreativeMarketFinderHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

第三步:数据初始化

创建示例市集数据:

void _initializeMarkets() {
  _markets = [
    CreativeMarket(
      id: '1',
      name: '春日文创集市',
      description: '汇聚本地原创设计师和手工艺人的春季文创市集,展示各种独特的手工艺品、原创设计和文创产品。',
      theme: '手工艺品',
      address: '北京市朝阳区798艺术区',
      latitude: 39.9847,
      longitude: 116.4963,
      startDate: DateTime.now().add(const Duration(days: 2)),
      endDate: DateTime.now().add(const Duration(days: 4)),
      organizer: '798艺术区管委会',
      contact: '010-64381784',
      images: ['assets/images/market1_1.jpg', 'assets/images/market1_2.jpg'],
      vendors: [
        Vendor(
          id: '1',
          name: '手作工坊',
          description: '专注于传统手工艺品制作',
          category: '手工艺品',
          products: ['陶瓷', '编织品', '木雕'],
          contact: '13800138001',
          images: ['assets/images/vendor1.jpg'],
          boothNumber: 'A01',
          isBookable: true,
          boothFee: 200.0,
        ),
      ],
      tags: ['原创', '手工', '艺术'],
      status: MarketStatus.upcoming,
      entryFee: 0.0,
      expectedVisitors: 500,
      isFavorite: true,
      distance: 2.3,
      activities: [
        Activity(
          id: '1',
          name: '陶艺体验工作坊',
          description: '学习基础陶艺制作技巧',
          startTime: DateTime.now().add(const Duration(days: 2, hours: 10)),
          endTime: DateTime.now().add(const Duration(days: 2, hours: 12)),
          location: 'A区工作坊',
          type: 'workshop',
          requiresRegistration: true,
          maxParticipants: 20,
          currentParticipants: 8,
        ),
      ],
      rating: 4.8,
      reviewCount: 156,
    ),
    CreativeMarket(
      id: '2',
      name: '青年设计师市集',
      description: '展示年轻设计师的创新作品,包括服装设计、产品设计、平面设计等多个领域。',
      theme: '设计创新',
      address: '上海市黄浦区新天地',
      latitude: 31.2222,
      longitude: 121.4767,
      startDate: DateTime.now().subtract(const Duration(days: 1)),
      endDate: DateTime.now().add(const Duration(days: 1)),
      organizer: '上海设计师协会',
      contact: '021-63912345',
      images: ['assets/images/market2_1.jpg'],
      vendors: [],
      tags: ['设计', '创新', '青年'],
      status: MarketStatus.ongoing,
      entryFee: 20.0,
      expectedVisitors: 800,
      isFavorite: false,
      distance: 1.8,
      activities: [],
      rating: 4.6,
      reviewCount: 203,
    ),
    CreativeMarket(
      id: '3',
      name: '复古文化市集',
      description: '以复古文化为主题的文创市集,展示vintage风格的服饰、饰品和生活用品。',
      theme: '复古文化',
      address: '广州市天河区太古汇',
      latitude: 23.1291,
      longitude: 113.3240,
      startDate: DateTime.now().add(const Duration(days: 7)),
      endDate: DateTime.now().add(const Duration(days: 9)),
      organizer: '广州文创联盟',
      contact: '020-38751234',
      images: ['assets/images/market3_1.jpg'],
      vendors: [],
      tags: ['复古', '文化', 'vintage'],
      status: MarketStatus.upcoming,
      entryFee: 15.0,
      expectedVisitors: 600,
      isFavorite: true,
      distance: 5.2,
      activities: [],
      rating: 4.4,
      reviewCount: 98,
    ),
    CreativeMarket(
      id: '4',
      name: '科技文创展',
      description: '结合科技与文创的创新展览,展示AR/VR艺术作品、数字艺术和智能设计产品。',
      theme: '科技创新',
      address: '深圳市南山区华侨城创意文化园',
      latitude: 22.5431,
      longitude: 113.9340,
      startDate: DateTime.now().add(const Duration(days: 14)),
      endDate: DateTime.now().add(const Duration(days: 16)),
      organizer: '深圳科技文创协会',
      contact: '0755-26781234',
      images: ['assets/images/market4_1.jpg'],
      vendors: [],
      tags: ['科技', '数字艺术', '创新'],
      status: MarketStatus.upcoming,
      entryFee: 30.0,
      expectedVisitors: 1000,
      isFavorite: false,
      distance: 8.7,
      activities: [],
      rating: 4.9,
      reviewCount: 67,
    ),
    CreativeMarket(
      id: '5',
      name: '民族文化市集',
      description: '展示各民族传统文化和手工艺品的文创市集,传承和弘扬民族文化。',
      theme: '民族文化',
      address: '成都市锦江区宽窄巷子',
      latitude: 30.6598,
      longitude: 104.0633,
      startDate: DateTime.now().subtract(const Duration(days: 3)),
      endDate: DateTime.now().subtract(const Duration(days: 1)),
      organizer: '成都民族文化促进会',
      contact: '028-86541234',
      images: ['assets/images/market5_1.jpg'],
      vendors: [],
      tags: ['民族', '传统', '文化'],
      status: MarketStatus.ended,
      entryFee: 10.0,
      expectedVisitors: 700,
      isFavorite: true,
      distance: 12.5,
      activities: [],
      rating: 4.7,
      reviewCount: 134,
    ),
  ];
}

第四步:首页设计

位置信息和搜索栏
Widget _buildHomePage() {
  final ongoingMarkets = _markets.where((market) => 
      market.status == MarketStatus.ongoing).toList();
  final upcomingMarkets = _markets.where((market) => 
      market.status == MarketStatus.upcoming).toList();
  
  return SingleChildScrollView(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 位置和搜索栏
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.deepPurple.shade300, Colors.deepPurple.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,
                      ),
                    ),
                    onTap: () => setState(() => _selectedIndex = 1),
                    readOnly: true,
                  ),
                ),
              ],
            ),
          ),
        ),
        
        // 快速筛选
        _buildQuickFilters(),
        
        // 正在进行的市集
        if (ongoingMarkets.isNotEmpty) ...[
          _buildSectionTitle('正在进行'),
          SizedBox(
            height: 280,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 16),
              itemCount: ongoingMarkets.length,
              itemBuilder: (context, index) {
                return Container(
                  width: 300,
                  margin: const EdgeInsets.only(right: 12),
                  child: _buildMarketCard(ongoingMarkets[index]),
                );
              },
            ),
          ),
        ],
        
        // 即将开始的市集
        if (upcomingMarkets.isNotEmpty) ...[
          _buildSectionTitle('即将开始'),
          ...upcomingMarkets.take(3).map((market) => 
            _buildMarketCard(market)
          ),
        ],
        
        // 热门主题
        _buildSectionTitle('热门主题'),
        _buildPopularThemes(),
        
        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.money_off, 'color': Colors.orange},
    {'name': '本周末', 'icon': Icons.weekend, 'color': Colors.purple},
  ];

  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: InkWell(
            onTap: () => _applyQuickFilter(filter['name'] as String),
            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 _buildMarketCard(CreativeMarket market) {
  return Card(
    margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    elevation: 4,
    child: InkWell(
      onTap: () => _showMarketDetails(market),
      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: [
                        _getThemeColor(market.theme),
                        _getThemeColor(market.theme).withOpacity(0.7),
                      ],
                    ),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: const Center(
                    child: Icon(
                      Icons.palette,
                      size: 40,
                      color: Colors.white,
                    ),
                  ),
                ),
                
                const SizedBox(width: 16),
                
                // 市集信息
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(
                            child: Text(
                              market.name,
                              style: const TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                          GestureDetector(
                            onTap: () => _toggleFavorite(market),
                            child: Icon(
                              market.isFavorite ? Icons.favorite : Icons.favorite_border,
                              color: market.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(
                            '${market.rating}',
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(width: 4),
                          Text('(${market.reviewCount}条评价)'),
                        ],
                      ),
                      
                      const SizedBox(height: 4),
                      
                      Row(
                        children: [
                          Icon(
                            Icons.location_on,
                            size: 16,
                            color: Colors.grey.shade600,
                          ),
                          const SizedBox(width: 4),
                          Text(
                            '${market.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: _getStatusColor(market.status),
                              borderRadius: BorderRadius.circular(10),
                            ),
                            child: Text(
                              _getStatusText(market.status),
                              style: const TextStyle(
                                color: Colors.white,
                                fontSize: 12,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 市集描述
            Text(
              market.description,
              style: TextStyle(
                color: Colors.grey.shade700,
                fontSize: 14,
              ),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
            
            const SizedBox(height: 12),
            
            // 时间信息
            Row(
              children: [
                Icon(
                  Icons.schedule,
                  size: 16,
                  color: Colors.grey.shade600,
                ),
                const SizedBox(width: 4),
                Text(
                  _formatDateRange(market.startDate, market.endDate),
                  style: TextStyle(
                    color: Colors.grey.shade600,
                    fontSize: 12,
                  ),
                ),
                const Spacer(),
                if (market.entryFee > 0)
                  Text(
                    ${market.entryFee.toStringAsFixed(0)}',
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: Colors.red,
                    ),
                  )
                else
                  const Text(
                    '免费',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: Colors.green,
                    ),
                  ),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 主题标签
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: market.tags.take(3).map((tag) {
                return Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 8,
                    vertical: 4,
                  ),
                  decoration: BoxDecoration(
                    color: _getThemeColor(market.theme).withOpacity(0.1),
                    borderRadius: BorderRadius.circular(12),
                    border: Border.all(
                      color: _getThemeColor(market.theme).withOpacity(0.3),
                    ),
                  ),
                  child: Text(
                    tag,
                    style: TextStyle(
                      color: _getThemeColor(market.theme),
                      fontSize: 12,
                    ),
                  ),
                );
              }).toList(),
            ),
          ],
        ),
      ),
    ),
  );
}

第六步:市集详情页面

class MarketDetailPage extends StatefulWidget {
  final CreativeMarket market;

  const MarketDetailPage({super.key, required this.market});

  
  State<MarketDetailPage> createState() => _MarketDetailPageState();
}

class _MarketDetailPageState extends State<MarketDetailPage>
    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.market.name),
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      _getThemeColor(widget.market.theme),
                      _getThemeColor(widget.market.theme).withOpacity(0.8),
                    ],
                  ),
                ),
                child: Stack(
                  children: [
                    const Center(
                      child: Icon(
                        Icons.palette,
                        size: 100,
                        color: Colors.white54,
                      ),
                    ),
                    Positioned(
                      bottom: 20,
                      left: 20,
                      right: 20,
                      child: _buildMarketBasicInfo(),
                    ),
                  ],
                ),
              ),
            ),
            actions: [
              IconButton(
                icon: Icon(
                  widget.market.isFavorite ? Icons.favorite : Icons.favorite_border,
                  color: widget.market.isFavorite ? Colors.red : Colors.white,
                ),
                onPressed: _toggleFavorite,
              ),
              IconButton(
                icon: const Icon(Icons.share),
                onPressed: _shareMarket,
              ),
            ],
          ),
          
          // 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(),
                _buildVendorsTab(),
                _buildActivitiesTab(),
                _buildReviewsTab(),
              ],
            ),
          ),
        ],
      ),
      bottomNavigationBar: _buildBottomActions(),
    );
  }

  Widget _buildMarketBasicInfo() {
    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.market.rating}',
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 16,
                ),
              ),
              const SizedBox(width: 8),
              Text('${widget.market.reviewCount}条评价'),
              const Spacer(),
              Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 8,
                  vertical: 4,
                ),
                decoration: BoxDecoration(
                  color: _getStatusColor(widget.market.status),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text(
                  _getStatusText(widget.market.status),
                  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.market.distance.toStringAsFixed(1)}km'),
              const SizedBox(width: 16),
              const Icon(Icons.people, size: 16),
              const SizedBox(width: 4),
              Text('预计${widget.market.expectedVisitors}人参观'),
            ],
          ),
        ],
      ),
    );
  }

  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.market.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.market.address),
          _buildInfoRow(Icons.schedule, '时间', 
              _formatDateRange(widget.market.startDate, widget.market.endDate)),
          _buildInfoRow(Icons.business, '主办方', widget.market.organizer),
          _buildInfoRow(Icons.phone, '联系方式', widget.market.contact),
          _buildInfoRow(Icons.attach_money, '入场费', 
              widget.market.entryFee > 0 ? ${widget.market.entryFee.toStringAsFixed(0)}' : '免费'),
          
          const SizedBox(height: 24),
          
          // 主题标签
          const Text(
            '主题标签',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          
          Wrap(
            spacing: 12,
            runSpacing: 8,
            children: widget.market.tags.map((tag) {
              return Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 8,
                ),
                decoration: BoxDecoration(
                  color: _getThemeColor(widget.market.theme).withOpacity(0.1),
                  borderRadius: BorderRadius.circular(20),
                  border: Border.all(
                    color: _getThemeColor(widget.market.theme).withOpacity(0.3),
                  ),
                ),
                child: Text(
                  tag,
                  style: TextStyle(
                    color: _getThemeColor(widget.market.theme),
                    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 _buildVendorsTab() {
    return widget.market.vendors.isEmpty
        ? const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.store, size: 64, color: Colors.grey),
                SizedBox(height: 16),
                Text('暂无商家信息', style: TextStyle(color: Colors.grey)),
              ],
            ),
          )
        : ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: widget.market.vendors.length,
            itemBuilder: (context, index) {
              final vendor = widget.market.vendors[index];
              return _buildVendorCard(vendor);
            },
          );
  }

  Widget _buildVendorCard(Vendor vendor) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Container(
                  width: 60,
                  height: 60,
                  decoration: BoxDecoration(
                    color: _getThemeColor(widget.market.theme).withOpacity(0.1),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Center(
                    child: Icon(
                      Icons.store,
                      size: 24,
                      color: _getThemeColor(widget.market.theme),
                    ),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        vendor.name,
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        '摊位号:${vendor.boothNumber}',
                        style: TextStyle(color: Colors.grey.shade600),
                      ),
                    ],
                  ),
                ),
                if (vendor.isBookable)
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 8,
                      vertical: 4,
                    ),
                    decoration: BoxDecoration(
                      color: Colors.green,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: const Text(
                      '可预订',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                      ),
                    ),
                  ),
              ],
            ),
            const SizedBox(height: 12),
            Text(
              vendor.description,
              style: TextStyle(color: Colors.grey.shade700),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: vendor.products.map((product) {
                return Chip(
                  label: Text(product),
                  backgroundColor: Colors.grey.shade100,
                );
              }).toList(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildActivitiesTab() {
    return widget.market.activities.isEmpty
        ? const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.event, size: 64, color: Colors.grey),
                SizedBox(height: 16),
                Text('暂无相关活动', style: TextStyle(color: Colors.grey)),
              ],
            ),
          )
        : ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: widget.market.activities.length,
            itemBuilder: (context, index) {
              final activity = widget.market.activities[index];
              return _buildActivityCard(activity);
            },
          );
  }

  Widget _buildActivityCard(Activity activity) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  _getActivityIcon(activity.type),
                  color: _getThemeColor(widget.market.theme),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    activity.name,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                if (activity.requiresRegistration)
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 8,
                      vertical: 4,
                    ),
                    decoration: BoxDecoration(
                      color: Colors.orange,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: const Text(
                      '需报名',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                      ),
                    ),
                  ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              activity.description,
              style: TextStyle(color: Colors.grey.shade700),
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Icon(Icons.schedule, size: 16, color: Colors.grey.shade600),
                const SizedBox(width: 4),
                Text(
                  _formatTimeRange(activity.startTime, activity.endTime),
                  style: TextStyle(color: Colors.grey.shade600),
                ),
                const SizedBox(width: 16),
                Icon(Icons.location_on, size: 16, color: Colors.grey.shade600),
                const SizedBox(width: 4),
                Text(
                  activity.location,
                  style: TextStyle(color: Colors.grey.shade600),
                ),
              ],
            ),
            if (activity.requiresRegistration) ...[
              const SizedBox(height: 8),
              Row(
                children: [
                  Icon(Icons.people, size: 16, color: Colors.grey.shade600),
                  const SizedBox(width: 4),
                  Text(
                    '${activity.currentParticipants}/${activity.maxParticipants}人',
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                  const Spacer(),
                  ElevatedButton(
                    onPressed: activity.currentParticipants < activity.maxParticipants
                        ? () => _registerActivity(activity)
                        : null,
                    child: Text(
                      activity.currentParticipants < activity.maxParticipants
                          ? '立即报名'
                          : '已满员',
                    ),
                  ),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }

  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 _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: _contactOrganizer,
              icon: const Icon(Icons.phone),
              label: const Text('联系主办方'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _navigateToMarket,
              icon: const Icon(Icons.navigation),
              label: const Text('导航前往'),
            ),
          ),
        ],
      ),
    );
  }

  // 工具方法和事件处理...
}

核心功能详解

1. 位置服务模拟

void _getCurrentLocation() async {
  // 模拟获取当前位置
  setState(() {
    _currentLocation = '北京市朝阳区';
    _userLatitude = 39.9370;
    _userLongitude = 116.4560;
  });
  
  // 更新市集距离
  _updateMarketDistances();
}

void _updateMarketDistances() {
  for (var market in _markets) {
    // 简单的距离计算模拟
    final latDiff = (market.latitude - _userLatitude).abs();
    final lngDiff = (market.longitude - _userLongitude).abs();
    market.distance = (latDiff + lngDiff) * 100; // 简化计算
  }
  
  // 按距离排序
  _markets.sort((a, b) => a.distance.compareTo(b.distance));
  setState(() {});
}

2. 搜索和筛选算法

void _performSearch(String query) {
  if (query.isEmpty) {
    setState(() {
      _filteredMarkets = List.from(_markets);
    });
    return;
  }

  setState(() {
    _filteredMarkets = _markets.where((market) {
      final nameMatch = market.name.toLowerCase().contains(query.toLowerCase());
      final descMatch = market.description.toLowerCase().contains(query.toLowerCase());
      final themeMatch = market.theme.toLowerCase().contains(query.toLowerCase());
      final tagMatch = market.tags.any((tag) =>
          tag.toLowerCase().contains(query.toLowerCase()));
      final organizerMatch = market.organizer.toLowerCase().contains(query.toLowerCase());
      
      return nameMatch || descMatch || themeMatch || tagMatch || organizerMatch;
    }).toList();
  });
}

3. 主题颜色系统

Color _getThemeColor(String theme) {
  switch (theme) {
    case '手工艺品': return Colors.brown;
    case '设计创新': return Colors.blue;
    case '复古文化': return Colors.amber;
    case '科技创新': return Colors.purple;
    case '民族文化': return Colors.red;
    case '艺术展览': return Colors.pink;
    case '音乐表演': return Colors.orange;
    case '美食文化': return Colors.green;
    default: return Colors.deepPurple;
  }
}

Color _getStatusColor(MarketStatus status) {
  switch (status) {
    case MarketStatus.upcoming: return Colors.blue;
    case MarketStatus.ongoing: return Colors.green;
    case MarketStatus.ended: return Colors.grey;
    case MarketStatus.cancelled: return Colors.red;
  }
}

String _getStatusText(MarketStatus status) {
  switch (status) {
    case MarketStatus.upcoming: return '即将开始';
    case MarketStatus.ongoing: return '进行中';
    case MarketStatus.ended: return '已结束';
    case MarketStatus.cancelled: return '已取消';
  }
}

性能优化

1. 列表优化

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

ListView.builder(
  itemCount: markets.length,
  itemExtent: 250, // 固定高度提高性能
  itemBuilder: (context, index) {
    return _buildMarketCard(markets[index]);
  },
)

2. 图片缓存

使用渐变色作为占位符,减少内存使用:

Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [
        _getThemeColor(market.theme),
        _getThemeColor(market.theme).withOpacity(0.7),
      ],
    ),
  ),
)

3. 状态管理优化

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

void _updateMarketList() {
  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

4. 社交分享

集成社交分享功能:

dependencies:
  share_plus: ^7.2.1

测试策略

1. 单元测试

测试核心业务逻辑:

test('should filter markets by status correctly', () {
  final markets = [
    CreativeMarket(status: MarketStatus.ongoing),
    CreativeMarket(status: MarketStatus.upcoming),
    CreativeMarket(status: MarketStatus.ended),
  ];
  
  final ongoingMarkets = markets.where((market) => 
      market.status == MarketStatus.ongoing).toList();
  expect(ongoingMarkets.length, equals(1));
});

2. Widget测试

测试UI组件:

testWidgets('should display market 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

Logo

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

更多推荐