Flutter民宿预订助手:打造完整的短租平台应用

项目概述

在共享经济蓬勃发展的今天,民宿短租已成为旅行住宿的重要选择。本教程将带你使用Flutter开发一个功能完整的民宿预订助手应用,涵盖房源浏览、搜索筛选、预订管理、用户交互等核心功能。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 房源探索:精美的房源卡片展示,支持多维度筛选
  • 智能搜索:基于位置、日期、价格的智能搜索系统
  • 预订管理:完整的预订流程和状态管理
  • 用户体验:直观的导航设计和流畅的交互体验
  • 数据模拟:完整的数据模型和模拟数据生成

技术架构

核心技术栈

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

项目结构

lib/
├── main.dart              # 应用入口和主要逻辑
├── models/               # 数据模型(集成在main.dart中)
│   ├── bnb_property.dart # 房源模型
│   ├── booking.dart      # 预订模型
│   ├── review.dart       # 评价模型
│   └── search_filter.dart # 搜索筛选模型
├── screens/              # 页面组件(集成在main.dart中)
│   ├── explore_page.dart # 探索页面
│   ├── wishlist_page.dart # 心愿单页面
│   ├── trips_page.dart   # 旅程页面
│   ├── inbox_page.dart   # 消息页面
│   └── profile_page.dart # 个人页面
└── widgets/              # 自定义组件(集成在main.dart中)
    ├── property_card.dart # 房源卡片
    ├── search_bar.dart   # 搜索栏
    └── booking_card.dart # 预订卡片

数据模型设计

房源模型(BnbProperty)

房源模型是应用的核心数据结构,包含了房源的所有基本信息:

class BnbProperty {
  final String id;              // 房源唯一标识
  final String title;           // 房源标题
  final String description;     // 房源描述
  final String location;        // 详细地址
  final String city;            // 所在城市
  final String country;         // 所在国家
  final double price;           // 每晚价格
  final String currency;        // 货币单位
  final List<String> images;    // 房源图片列表
  final String hostName;        // 房东姓名
  final String hostAvatar;      // 房东头像
  final double rating;          // 评分
  final int reviewCount;        // 评价数量
  final int maxGuests;          // 最大房客数
  final int bedrooms;           // 卧室数量
  final int bathrooms;          // 浴室数量
  final List<String> amenities; // 设施列表
  final String propertyType;    // 房源类型
  final bool isInstantBook;     // 是否支持即时预订
  final bool isSuperhost;       // 是否为超赞房东
  final List<String> houseRules;// 房屋规则
  final String checkIn;         // 入住时间
  final String checkOut;        // 退房时间
  final double latitude;        // 纬度
  final double longitude;       // 经度
  final bool isFavorite;        // 是否收藏
  final List<String> tags;      // 标签列表
}

预订模型(Booking)

预订模型管理用户的预订信息和状态:

class Booking {
  final String id;              // 预订唯一标识
  final String propertyId;      // 关联房源ID
  final String propertyTitle;   // 房源标题
  final String propertyImage;   // 房源图片
  final DateTime checkInDate;   // 入住日期
  final DateTime checkOutDate;  // 退房日期
  final int guests;             // 房客数量
  final double totalPrice;      // 总价格
  final String status;          // 预订状态
  final DateTime bookingDate;   // 预订时间
  final String hostName;        // 房东姓名
  final String guestName;       // 房客姓名
  final String guestEmail;      // 房客邮箱
  final String guestPhone;      // 房客电话
  final String specialRequests; // 特殊要求
}

预订状态包括:

  • pending:待确认
  • confirmed:已确认
  • cancelled:已取消
  • completed:已完成

评价模型(Review)

评价模型存储用户对房源的评价信息:

class Review {
  final String id;              // 评价唯一标识
  final String propertyId;      // 关联房源ID
  final String guestName;       // 评价者姓名
  final String guestAvatar;     // 评价者头像
  final double rating;          // 总体评分
  final String comment;         // 评价内容
  final DateTime reviewDate;    // 评价时间
  final List<String> photos;    // 评价图片
  final Map<String, double> categoryRatings; // 分类评分
}

分类评分包括:

  • cleanliness:清洁度
  • accuracy:准确性
  • communication:沟通
  • location:位置
  • checkin:入住体验
  • value:性价比

搜索筛选模型(SearchFilter)

搜索筛选模型定义了搜索和筛选的条件:

class SearchFilter {
  final String? location;       // 目的地
  final DateTime? checkIn;      // 入住日期
  final DateTime? checkOut;     // 退房日期
  final int guests;             // 房客数量
  final double? minPrice;       // 最低价格
  final double? maxPrice;       // 最高价格
  final String? propertyType;   // 房源类型
  final List<String> amenities; // 必需设施
  final bool instantBookOnly;   // 仅即时预订
  final bool superhostOnly;     // 仅超赞房东
}

应用主体结构

应用入口

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '民宿预订助手',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const BnbHomePage(),
    );
  }
}

应用采用Material Design 3设计规范,使用青色(teal)作为主题色,营造清新自然的视觉体验。

主页面结构

主页面使用IndexedStack实现底部导航的页面切换:

class BnbHomePage extends StatefulWidget {
  
  State<BnbHomePage> createState() => _BnbHomePageState();
}

class _BnbHomePageState extends State<BnbHomePage> {
  int _selectedIndex = 0;
  final List<BnbProperty> _properties = [];
  final List<BnbProperty> _favoriteProperties = [];
  final List<Booking> _bookings = [];
  final List<Review> _reviews = [];
  SearchFilter _currentFilter = SearchFilter();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('民宿预订助手'),
        backgroundColor: Colors.teal.withValues(alpha: 0.1),
        actions: [
          IconButton(
            onPressed: () => _showSearchDialog(),
            icon: const Icon(Icons.search),
          ),
          IconButton(
            onPressed: () => _showFilterDialog(),
            icon: const Icon(Icons.tune),
          ),
        ],
      ),
      body: IndexedStack(
        index: _selectedIndex,
        children: [
          _buildExplorePage(),
          _buildWishlistPage(),
          _buildTripsPage(),
          _buildInboxPage(),
          _buildProfilePage(),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.explore), label: '探索'),
          NavigationDestination(icon: Icon(Icons.favorite), label: '心愿单'),
          NavigationDestination(icon: Icon(Icons.luggage), label: '旅程'),
          NavigationDestination(icon: Icon(Icons.message), label: '消息'),
          NavigationDestination(icon: Icon(Icons.person), label: '个人'),
        ],
      ),
    );
  }
}

数据生成与管理

房源数据生成

应用使用模拟数据生成器创建丰富的房源信息:

void _generateProperties() {
  final propertyTitles = [
    '海景别墅 - 私人海滩',
    '市中心精品公寓',
    '山间小木屋',
    '古城内传统四合院',
    '湖边度假屋',
    // ... 更多标题
  ];

  final descriptions = [
    '享受私人海滩和无敌海景,完美的度假体验',
    '位于市中心黄金地段,交通便利,设施齐全',
    '远离城市喧嚣,在大自然中放松身心',
    // ... 更多描述
  ];

  final cities = ['三亚', '北京', '上海', '杭州', '成都', '厦门', '青岛', '大理', '丽江', '桂林'];
  final propertyTypes = ['整套房子', '独立房间', '合住房间', '精品酒店'];
  final amenities = ['WiFi', '空调', '洗衣机', '厨房', '停车位', '游泳池', '健身房', '阳台', '花园', '烧烤设备'];
  final tags = ['海景', '市中心', '安静', '家庭友好', '商务', '浪漫', '宠物友好', '无障碍'];

  final random = Random();

  for (int i = 0; i < 15; i++) {
    // 随机选择设施和标签
    final selectedAmenities = <String>[];
    final selectedTags = <String>[];

    for (int j = 0; j < 5 + random.nextInt(5); j++) {
      final amenity = amenities[random.nextInt(amenities.length)];
      if (!selectedAmenities.contains(amenity)) {
        selectedAmenities.add(amenity);
      }
    }

    for (int j = 0; j < 2 + random.nextInt(3); j++) {
      final tag = tags[random.nextInt(tags.length)];
      if (!selectedTags.contains(tag)) {
        selectedTags.add(tag);
      }
    }

    final city = cities[random.nextInt(cities.length)];
    
    _properties.add(BnbProperty(
      id: 'property_$i',
      title: propertyTitles[i],
      description: descriptions[random.nextInt(descriptions.length)],
      location: '$city${['', '', ''][random.nextInt(3)]}${['中心', '海边', '山区', '古城'][random.nextInt(4)]}',
      city: city,
      country: '中国',
      price: 200 + random.nextInt(800).toDouble(),
      currency: '¥',
      images: ['property_${i + 1}_1.jpg', 'property_${i + 1}_2.jpg', 'property_${i + 1}_3.jpg'],
      hostName: ['张先生', '李女士', '王先生', '陈女士', '刘先生'][random.nextInt(5)],
      hostAvatar: 'host_${i % 5 + 1}.jpg',
      rating: 4.0 + random.nextDouble() * 1.0,
      reviewCount: random.nextInt(200),
      maxGuests: 2 + random.nextInt(6),
      bedrooms: 1 + random.nextInt(3),
      bathrooms: 1 + random.nextInt(2),
      amenities: selectedAmenities,
      propertyType: propertyTypes[random.nextInt(propertyTypes.length)],
      isInstantBook: random.nextBool(),
      isSuperhost: random.nextBool(),
      houseRules: ['禁止吸烟', '禁止聚会', '22:00后保持安静', '爱护房屋设施'],
      checkIn: '15:00',
      checkOut: '11:00',
      latitude: 39.9 + random.nextDouble() * 10,
      longitude: 116.4 + random.nextDouble() * 10,
      isFavorite: random.nextBool(),
      tags: selectedTags,
    ));
  }
}

预订数据生成

预订数据生成器创建不同状态的预订记录:

void _generateBookings() {
  final guestNames = ['张三', '李四', '王五', '赵六', '钱七'];
  final statuses = ['pending', 'confirmed', 'cancelled', 'completed'];
  final random = Random();

  for (int i = 0; i < 8; i++) {
    final property = _properties[random.nextInt(_properties.length)];
    final checkIn = DateTime.now().add(Duration(days: random.nextInt(60) - 30));
    final nights = 1 + random.nextInt(7);
    final checkOut = checkIn.add(Duration(days: nights));
    final guests = 1 + random.nextInt(property.maxGuests);

    _bookings.add(Booking(
      id: 'booking_$i',
      propertyId: property.id,
      propertyTitle: property.title,
      propertyImage: property.images.first,
      checkInDate: checkIn,
      checkOutDate: checkOut,
      guests: guests,
      totalPrice: property.price * nights,
      status: statuses[random.nextInt(statuses.length)],
      bookingDate: DateTime.now().subtract(Duration(days: random.nextInt(30))),
      hostName: property.hostName,
      guestName: guestNames[random.nextInt(guestNames.length)],
      guestEmail: 'guest${i + 1}@example.com',
      guestPhone: '138${random.nextInt(99999999).toString().padLeft(8, '0')}',
      specialRequests: i % 3 == 0 ? '希望提供婴儿床' : '',
    ));
  }
}

核心功能实现

探索页面

探索页面是应用的核心功能,展示房源列表和搜索功能:

Widget _buildExplorePage() {
  final filteredProperties = _filterProperties();

  return Column(
    children: [
      _buildSearchBar(),
      Expanded(
        child: ListView.builder(
          padding: const EdgeInsets.all(16),
          itemCount: filteredProperties.length,
          itemBuilder: (context, index) {
            return _buildPropertyCard(filteredProperties[index]);
          },
        ),
      ),
    ],
  );
}
搜索栏设计

搜索栏提供快速访问搜索功能的入口:

Widget _buildSearchBar() {
  return Container(
    margin: const EdgeInsets.all(16),
    child: Card(
      child: InkWell(
        onTap: () => _showSearchDialog(),
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  const Icon(Icons.search, color: Colors.teal),
                  const SizedBox(width: 8),
                  Text(
                    _currentFilter.location ?? '你想去哪里?',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w500,
                      color: _currentFilter.location != null ? Colors.black : Colors.grey[600],
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: Text(
                      _currentFilter.checkIn != null && _currentFilter.checkOut != null
                          ? '${_formatDate(_currentFilter.checkIn!)} - ${_formatDate(_currentFilter.checkOut!)}'
                          : '添加日期',
                      style: TextStyle(
                        fontSize: 14,
                        color: _currentFilter.checkIn != null ? Colors.black87 : Colors.grey[600],
                      ),
                    ),
                  ),
                  const SizedBox(width: 16),
                  Text(
                    '${_currentFilter.guests}位房客',
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey[600],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    ),
  );
}
房源卡片设计

房源卡片是展示房源信息的核心组件:

Widget _buildPropertyCard(BnbProperty property) {
  return Card(
    margin: const EdgeInsets.only(bottom: 16),
    child: InkWell(
      onTap: () => _showPropertyDetail(property),
      borderRadius: BorderRadius.circular(12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 房源图片区域
          Container(
            height: 200,
            width: double.infinity,
            decoration: BoxDecoration(
              color: Colors.teal.withValues(alpha: 0.1),
              borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
            ),
            child: Stack(
              children: [
                Center(
                  child: Icon(
                    Icons.home,
                    size: 80,
                    color: Colors.teal.withValues(alpha: 0.3),
                  ),
                ),
                // 房源类型标签
                Positioned(
                  top: 12,
                  left: 12,
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      color: Colors.teal.withValues(alpha: 0.9),
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      property.propertyType,
                      style: const TextStyle(
                        fontSize: 12,
                        color: Colors.white,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                ),
                // 超赞房东标签
                if (property.isSuperhost)
                  Positioned(
                    top: 12,
                    right: 60,
                    child: Container(
                      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      decoration: BoxDecoration(
                        color: Colors.orange.withValues(alpha: 0.9),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: const Text(
                        '超赞房东',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.white,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ),
                  ),
                // 收藏按钮
                Positioned(
                  top: 12,
                  right: 12,
                  child: IconButton(
                    onPressed: () {
                      setState(() {
                        // 切换收藏状态
                      });
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text(property.isFavorite ? '已取消收藏' : '已添加收藏'),
                        ),
                      );
                    },
                    icon: Icon(
                      property.isFavorite ? Icons.favorite : Icons.favorite_border,
                      color: Colors.red,
                    ),
                    style: IconButton.styleFrom(
                      backgroundColor: Colors.white.withValues(alpha: 0.9),
                    ),
                  ),
                ),
              ],
            ),
          ),
          // 房源信息区域
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Expanded(
                      child: Text(
                        property.title,
                        style: const TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    Row(
                      children: [
                        Icon(Icons.star, color: Colors.orange, size: 16),
                        const SizedBox(width: 4),
                        Text(
                          property.rating.toStringAsFixed(1),
                          style: const TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.bold,
                            color: Colors.orange,
                          ),
                        ),
                        const SizedBox(width: 4),
                        Text(
                          '(${property.reviewCount})',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.grey[500],
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                Text(
                  property.location,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  property.description,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Icon(Icons.people, size: 16, color: Colors.grey[600]),
                    const SizedBox(width: 4),
                    Text(
                      '${property.maxGuests}位房客',
                      style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                    ),
                    const SizedBox(width: 16),
                    Icon(Icons.bed, size: 16, color: Colors.grey[600]),
                    const SizedBox(width: 4),
                    Text(
                      '${property.bedrooms}间卧室',
                      style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                    ),
                    const SizedBox(width: 16),
                    Icon(Icons.bathtub, size: 16, color: Colors.grey[600]),
                    const SizedBox(width: 4),
                    Text(
                      '${property.bathrooms}间浴室',
                      style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Text(
                      '${property.currency}${property.price.toInt()}',
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                        color: Colors.teal,
                      ),
                    ),
                    const Text(
                      ' / 晚',
                      style: TextStyle(fontSize: 14, color: Colors.grey),
                    ),
                    const Spacer(),
                    // 标签展示
                    if (property.tags.isNotEmpty)
                      Wrap(
                        spacing: 4,
                        children: property.tags.take(2).map((tag) {
                          return Container(
                            padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                            decoration: BoxDecoration(
                              color: Colors.teal.withValues(alpha: 0.1),
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: Text(
                              tag,
                              style: const TextStyle(fontSize: 10, color: Colors.teal),
                            ),
                          );
                        }).toList(),
                      ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

搜索与筛选功能

搜索对话框

搜索对话框提供目的地和日期选择功能:

void _showSearchDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('搜索房源'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            decoration: const InputDecoration(
              labelText: '目的地',
              hintText: '输入城市或地区',
              prefixIcon: Icon(Icons.location_on),
            ),
            onChanged: (value) {
              setState(() {
                _currentFilter = SearchFilter(
                  location: value.isEmpty ? null : value,
                  checkIn: _currentFilter.checkIn,
                  checkOut: _currentFilter.checkOut,
                  guests: _currentFilter.guests,
                  // ... 其他筛选条件
                );
              });
            },
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: TextButton(
                  onPressed: () async {
                    final date = await showDatePicker(
                      context: context,
                      initialDate: DateTime.now(),
                      firstDate: DateTime.now(),
                      lastDate: DateTime.now().add(const Duration(days: 365)),
                    );
                    if (date != null) {
                      setState(() {
                        _currentFilter = SearchFilter(
                          location: _currentFilter.location,
                          checkIn: date,
                          checkOut: _currentFilter.checkOut,
                          guests: _currentFilter.guests,
                          // ... 其他筛选条件
                        );
                      });
                    }
                  },
                  child: Text(_currentFilter.checkIn != null 
                      ? _formatDate(_currentFilter.checkIn!) 
                      : '入住日期'),
                ),
              ),
              Expanded(
                child: TextButton(
                  onPressed: () async {
                    final date = await showDatePicker(
                      context: context,
                      initialDate: _currentFilter.checkIn?.add(const Duration(days: 1)) ?? 
                                 DateTime.now().add(const Duration(days: 1)),
                      firstDate: _currentFilter.checkIn?.add(const Duration(days: 1)) ?? 
                                DateTime.now().add(const Duration(days: 1)),
                      lastDate: DateTime.now().add(const Duration(days: 365)),
                    );
                    if (date != null) {
                      setState(() {
                        _currentFilter = SearchFilter(
                          location: _currentFilter.location,
                          checkIn: _currentFilter.checkIn,
                          checkOut: date,
                          guests: _currentFilter.guests,
                          // ... 其他筛选条件
                        );
                      });
                    }
                  },
                  child: Text(_currentFilter.checkOut != null 
                      ? _formatDate(_currentFilter.checkOut!) 
                      : '退房日期'),
                ),
              ),
            ],
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(context);
            setState(() {});
          },
          child: const Text('搜索'),
        ),
      ],
    ),
  );
}
筛选功能实现

筛选功能根据用户设定的条件过滤房源:

List<BnbProperty> _filterProperties() {
  return _properties.where((property) {
    // 位置筛选
    if (_currentFilter.location != null && _currentFilter.location!.isNotEmpty) {
      if (!property.location.contains(_currentFilter.location!) &&
          !property.city.contains(_currentFilter.location!)) {
        return false;
      }
    }

    // 价格筛选
    if (_currentFilter.minPrice != null && property.price < _currentFilter.minPrice!) {
      return false;
    }
    if (_currentFilter.maxPrice != null && property.price > _currentFilter.maxPrice!) {
      return false;
    }

    // 房客数量筛选
    if (property.maxGuests < _currentFilter.guests) {
      return false;
    }

    // 房源类型筛选
    if (_currentFilter.propertyType != null && 
        property.propertyType != _currentFilter.propertyType) {
      return false;
    }

    // 设施筛选
    if (_currentFilter.amenities.isNotEmpty) {
      for (String amenity in _currentFilter.amenities) {
        if (!property.amenities.contains(amenity)) {
          return false;
        }
      }
    }

    // 即时预订筛选
    if (_currentFilter.instantBookOnly && !property.isInstantBook) {
      return false;
    }

    // 超赞房东筛选
    if (_currentFilter.superhostOnly && !property.isSuperhost) {
      return false;
    }

    return true;
  }).toList();
}

心愿单功能

心愿单页面展示用户收藏的房源:

Widget _buildWishlistPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '我的心愿单',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          '共${_favoriteProperties.length}个收藏',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: _favoriteProperties.isEmpty
              ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.favorite_border, size: 64, color: Colors.grey),
                      SizedBox(height: 16),
                      Text('还没有收藏的房源', style: TextStyle(color: Colors.grey)),
                      SizedBox(height: 8),
                      Text('去探索页面发现心仪的房源吧!', style: TextStyle(color: Colors.grey)),
                    ],
                  ),
                )
              : ListView.builder(
                  itemCount: _favoriteProperties.length,
                  itemBuilder: (context, index) {
                    return _buildFavoritePropertyCard(_favoriteProperties[index]);
                  },
                ),
        ),
      ],
    ),
  );
}

旅程管理

旅程页面使用TabBar展示不同状态的预订:

Widget _buildTripsPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '我的旅程',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 16),
        DefaultTabController(
          length: 3,
          child: Column(
            children: [
              const TabBar(
                tabs: [
                  Tab(text: '即将到来'),
                  Tab(text: '进行中'),
                  Tab(text: '已完成'),
                ],
              ),
              const SizedBox(height: 16),
              SizedBox(
                height: 500,
                child: TabBarView(
                  children: [
                    _buildBookingsList('pending'),
                    _buildBookingsList('confirmed'),
                    _buildBookingsList('completed'),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}
预订卡片设计

预订卡片展示预订的详细信息和状态:

Widget _buildBookingCard(Booking booking) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: InkWell(
      onTap: () => _showBookingDetail(booking),
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Container(
                  width: 60,
                  height: 60,
                  decoration: BoxDecoration(
                    color: Colors.teal.withValues(alpha: 0.1),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: const Icon(Icons.home, color: Colors.teal, size: 30),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        booking.propertyTitle,
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(height: 4),
                      Text(
                        '房东:${booking.hostName}',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey[600],
                        ),
                      ),
                    ],
                  ),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: booking.statusColor.withValues(alpha: 0.1),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    booking.statusText,
                    style: TextStyle(
                      fontSize: 12,
                      color: booking.statusColor,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Icon(Icons.calendar_today, size: 16, color: Colors.grey[600]),
                const SizedBox(width: 4),
                Text(
                  '${_formatDate(booking.checkInDate)} - ${_formatDate(booking.checkOutDate)}',
                  style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                ),
                const SizedBox(width: 16),
                Icon(Icons.nights_stay, size: 16, color: Colors.grey[600]),
                const SizedBox(width: 4),
                Text(
                  '${booking.nights}晚',
                  style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                ),
                const SizedBox(width: 16),
                Icon(Icons.people, size: 16, color: Colors.grey[600]),
                const SizedBox(width: 4),
                Text(
                  '${booking.guests}位房客',
                  style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Text(
                  '总价:¥${booking.totalPrice.toInt()}',
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.teal,
                  ),
                ),
                const Spacer(),
                if (booking.status == 'pending')
                  TextButton(
                    onPressed: () => _cancelBooking(booking),
                    child: const Text('取消预订', style: TextStyle(color: Colors.red)),
                  ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

预订流程

房源详情对话框

房源详情对话框展示房源的完整信息:

void _showPropertyDetail(BnbProperty property) {
  showDialog(
    context: context,
    builder: (context) => Dialog(
      child: Container(
        width: MediaQuery.of(context).size.width * 0.9,
        height: MediaQuery.of(context).size.height * 0.8,
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text(
                    property.title,
                    style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                  ),
                ),
                IconButton(
                  onPressed: () => Navigator.pop(context),
                  icon: const Icon(Icons.close),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Container(
              height: 200,
              width: double.infinity,
              decoration: BoxDecoration(
                color: Colors.teal.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Icon(Icons.home, size: 80, color: Colors.teal),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 评分和价格
                    Row(
                      children: [
                        Icon(Icons.star, color: Colors.orange, size: 20),
                        const SizedBox(width: 4),
                        Text(
                          property.rating.toStringAsFixed(1),
                          style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(width: 8),
                        Text('(${property.reviewCount}条评价)'),
                        const Spacer(),
                        Text(
                          '${property.currency}${property.price.toInt()}/晚',
                          style: const TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                            color: Colors.teal,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 16),
                    Text(
                      property.location,
                      style: TextStyle(fontSize: 14, color: Colors.grey[600]),
                    ),
                    const SizedBox(height: 16),
                    Text(
                      property.description,
                      style: const TextStyle(fontSize: 16),
                    ),
                    const SizedBox(height: 16),
                    // 房源信息
                    const Text(
                      '房源信息',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        _buildInfoChip(Icons.people, '${property.maxGuests}位房客'),
                        const SizedBox(width: 8),
                        _buildInfoChip(Icons.bed, '${property.bedrooms}间卧室'),
                        const SizedBox(width: 8),
                        _buildInfoChip(Icons.bathtub, '${property.bathrooms}间浴室'),
                      ],
                    ),
                    const SizedBox(height: 16),
                    // 房源设施
                    const Text(
                      '房源设施',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: property.amenities.map((amenity) {
                        return Chip(
                          label: Text(amenity),
                          backgroundColor: Colors.teal.withValues(alpha: 0.1),
                        );
                      }).toList(),
                    ),
                    const SizedBox(height: 16),
                    // 房东信息
                    const Text(
                      '房东信息',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        CircleAvatar(
                          backgroundColor: Colors.teal.withValues(alpha: 0.1),
                          child: Text(
                            property.hostName.substring(0, 1),
                            style: const TextStyle(color: Colors.teal, fontWeight: FontWeight.bold),
                          ),
                        ),
                        const SizedBox(width: 12),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              property.hostName,
                              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                            ),
                            if (property.isSuperhost)
                              const Text(
                                '超赞房东',
                                style: TextStyle(color: Colors.orange, fontSize: 12),
                              ),
                          ],
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  Navigator.pop(context);
                  _showBookingDialog(property);
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.teal,
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: const Text('立即预订', style: TextStyle(fontSize: 16)),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}
预订对话框

预订对话框允许用户选择日期和房客数量:

void _showBookingDialog(BnbProperty property) {
  DateTime? checkIn;
  DateTime? checkOut;
  int guests = 1;

  showDialog(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setDialogState) => AlertDialog(
        title: const Text('预订房源'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              children: [
                Expanded(
                  child: TextButton(
                    onPressed: () async {
                      final date = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now(),
                        firstDate: DateTime.now(),
                        lastDate: DateTime.now().add(const Duration(days: 365)),
                      );
                      if (date != null) {
                        setDialogState(() => checkIn = date);
                      }
                    },
                    child: Text(checkIn != null ? _formatDate(checkIn!) : '入住日期'),
                  ),
                ),
                Expanded(
                  child: TextButton(
                    onPressed: () async {
                      final date = await showDatePicker(
                        context: context,
                        initialDate: checkIn?.add(const Duration(days: 1)) ?? 
                                   DateTime.now().add(const Duration(days: 1)),
                        firstDate: checkIn?.add(const Duration(days: 1)) ?? 
                                  DateTime.now().add(const Duration(days: 1)),
                        lastDate: DateTime.now().add(const Duration(days: 365)),
                      );
                      if (date != null) {
                        setDialogState(() => checkOut = date);
                      }
                    },
                    child: Text(checkOut != null ? _formatDate(checkOut!) : '退房日期'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                const Text('房客数量:'),
                IconButton(
                  onPressed: guests > 1 ? () => setDialogState(() => guests--) : null,
                  icon: const Icon(Icons.remove),
                ),
                Text('$guests'),
                IconButton(
                  onPressed: guests < property.maxGuests ? () => setDialogState(() => guests++) : null,
                  icon: const Icon(Icons.add),
                ),
              ],
            ),
            if (checkIn != null && checkOut != null)
              Padding(
                padding: const EdgeInsets.only(top: 16),
                child: Column(
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text('${property.currency}${property.price.toInt()} × ${checkOut!.difference(checkIn!).inDays}晚'),
                        Text('${property.currency}${(property.price * checkOut!.difference(checkIn!).inDays).toInt()}'),
                      ],
                    ),
                    const Divider(),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text('总价', style: TextStyle(fontWeight: FontWeight.bold)),
                        Text(
                          '${property.currency}${(property.price * checkOut!.difference(checkIn!).inDays).toInt()}',
                          style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.teal),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: checkIn != null && checkOut != null ? () {
              _createBooking(property, checkIn!, checkOut!, guests);
              Navigator.pop(context);
            } : null,
            child: const Text('确认预订'),
          ),
        ],
      ),
    ),
  );
}
创建预订

创建预订方法将新预订添加到预订列表:

void _createBooking(BnbProperty property, DateTime checkIn, DateTime checkOut, int guests) {
  final booking = Booking(
    id: 'booking_${_bookings.length}',
    propertyId: property.id,
    propertyTitle: property.title,
    propertyImage: property.images.first,
    checkInDate: checkIn,
    checkOutDate: checkOut,
    guests: guests,
    totalPrice: property.price * checkOut.difference(checkIn).inDays,
    status: 'pending',
    bookingDate: DateTime.now(),
    hostName: property.hostName,
    guestName: '张三',
    guestEmail: 'zhangsan@example.com',
    guestPhone: '13800138000',
    specialRequests: '',
  );

  setState(() {
    _bookings.add(booking);
  });

  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('预订成功!请等待房东确认')),
  );
}

消息功能

消息页面展示与房东的沟通记录:

Widget _buildInboxPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '消息',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: ListView.builder(
            itemCount: 5,
            itemBuilder: (context, index) {
              return _buildMessageCard(index);
            },
          ),
        ),
      ],
    ),
  );
}

Widget _buildMessageCard(int index) {
  final messages = [
    {'name': '张先生', 'message': '您好,请问房间有WiFi吗?', 'time': '2小时前', 'unread': true},
    {'name': '李女士', 'message': '感谢您的入住,欢迎下次再来!', 'time': '1天前', 'unread': false},
    {'name': '王先生', 'message': '您的预订已确认', 'time': '2天前', 'unread': false},
    {'name': '陈女士', 'message': '房间钥匙在门口的密码盒里', 'time': '3天前', 'unread': false},
    {'name': '刘先生', 'message': '欢迎入住,有问题随时联系', 'time': '5天前', 'unread': false},
  ];

  final message = messages[index];

  return Card(
    margin: const EdgeInsets.only(bottom: 8),
    child: ListTile(
      leading: CircleAvatar(
        backgroundColor: Colors.teal.withValues(alpha: 0.1),
        child: Text(
          (message['name']! as String).substring(0, 1),
          style: const TextStyle(color: Colors.teal, fontWeight: FontWeight.bold),
        ),
      ),
      title: Text(
        message['name']! as String,
        style: TextStyle(
          fontWeight: message['unread'] as bool ? FontWeight.bold : FontWeight.normal,
        ),
      ),
      subtitle: Text(
        message['message']! as String,
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
      trailing: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            message['time']! as String,
            style: TextStyle(
              fontSize: 12,
              color: Colors.grey[600],
            ),
          ),
          if (message['unread'] as bool)
            Container(
              margin: const EdgeInsets.only(top: 4),
              width: 8,
              height: 8,
              decoration: const BoxDecoration(
                color: Colors.teal,
                shape: BoxShape.circle,
              ),
            ),
        ],
      ),
      onTap: () {
        // 打开聊天页面
      },
    ),
  );
}

个人中心

个人中心页面提供用户信息管理和应用设置:

Widget _buildProfilePage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '个人中心',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 24),
        // 用户信息卡片
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                CircleAvatar(
                  radius: 30,
                  backgroundColor: Colors.teal.withValues(alpha: 0.1),
                  child: const Icon(Icons.person, size: 30, color: Colors.teal),
                ),
                const SizedBox(width: 16),
                const Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        '张三',
                        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                      ),
                      SizedBox(height: 4),
                      Text(
                        'zhangsan@example.com',
                        style: TextStyle(color: Colors.grey),
                      ),
                      SizedBox(height: 4),
                      Text(
                        '已验证用户',
                        style: TextStyle(color: Colors.green, fontSize: 12),
                      ),
                    ],
                  ),
                ),
                IconButton(
                  onPressed: () {
                    // 编辑个人信息
                  },
                  icon: const Icon(Icons.edit),
                ),
              ],
            ),
          ),
        ),
        const SizedBox(height: 24),
        // 功能菜单
        Expanded(
          child: ListView(
            children: [
              _buildMenuTile(Icons.person, '个人信息', '管理您的个人资料'),
              _buildMenuTile(Icons.security, '账户安全', '密码和安全设置'),
              _buildMenuTile(Icons.payment, '支付方式', '管理支付和账单'),
              _buildMenuTile(Icons.notifications, '通知设置', '推送和邮件通知'),
              _buildMenuTile(Icons.language, '语言设置', '选择您的语言'),
              _buildMenuTile(Icons.help, '帮助中心', '常见问题和客服'),
              _buildMenuTile(Icons.info, '关于我们', '版本信息和条款'),
              const Divider(),
              _buildMenuTile(Icons.logout, '退出登录', '', isLogout: true),
            ],
          ),
        ),
      ],
    ),
  );
}

Widget _buildMenuTile(IconData icon, String title, String subtitle, {bool isLogout = false}) {
  return ListTile(
    leading: Icon(
      icon,
      color: isLogout ? Colors.red : Colors.teal,
    ),
    title: Text(
      title,
      style: TextStyle(
        color: isLogout ? Colors.red : null,
        fontWeight: FontWeight.w500,
      ),
    ),
    subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
    trailing: const Icon(Icons.chevron_right),
    onTap: () {
      if (isLogout) {
        _showLogoutDialog();
      } else {
        // 处理其他菜单项
      }
    },
  );
}

用户体验优化

响应式设计

应用采用响应式设计,适配不同屏幕尺寸:

// 房源详情对话框的响应式设计
Container(
  width: MediaQuery.of(context).size.width * 0.9,
  height: MediaQuery.of(context).size.height * 0.8,
  // ...
)

// 房源卡片的自适应布局
Container(
  height: 200,
  width: double.infinity,
  // ...
)

交互反馈

应用提供丰富的交互反馈:

// 点击反馈
InkWell(
  onTap: () => _showPropertyDetail(property),
  borderRadius: BorderRadius.circular(12),
  child: // ...
)

// 状态提示
ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(content: Text('预订成功!请等待房东确认')),
);

// 加载状态
CircularProgressIndicator()

视觉设计

应用采用现代化的视觉设计:

// 卡片阴影效果
Card(
  elevation: 2,
  shadowColor: Colors.black.withValues(alpha: 0.1),
  // ...
)

// 渐变背景
Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.teal.withValues(alpha: 0.1), Colors.white],
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
    ),
  ),
  // ...
)

// 圆角设计
BorderRadius.circular(12)

性能优化

列表优化

使用ListView.builder实现高效的列表渲染:

ListView.builder(
  itemCount: filteredProperties.length,
  itemBuilder: (context, index) {
    return _buildPropertyCard(filteredProperties[index]);
  },
)

状态管理

合理使用setState进行状态更新:

setState(() {
  _favoriteProperties.remove(property);
});

内存管理

及时释放不需要的资源:


void dispose() {
  // 清理资源
  super.dispose();
}

扩展功能建议

地图集成

集成地图功能显示房源位置:

// 使用google_maps_flutter包
GoogleMap(
  initialCameraPosition: CameraPosition(
    target: LatLng(property.latitude, property.longitude),
    zoom: 15,
  ),
  markers: {
    Marker(
      markerId: MarkerId(property.id),
      position: LatLng(property.latitude, property.longitude),
    ),
  },
)

支付集成

集成支付功能:

// 使用stripe_payment或其他支付SDK
void _processPayment() async {
  // 支付处理逻辑
}

推送通知

集成推送通知功能:

// 使用firebase_messaging
void _setupPushNotifications() {
  // 推送通知设置
}

离线支持

添加离线数据缓存:

// 使用shared_preferences或sqflite
void _cacheData() {
  // 数据缓存逻辑
}

总结

本教程完整展示了如何使用Flutter开发一个功能丰富的民宿预订助手应用。应用包含了房源浏览、搜索筛选、预订管理、用户交互等核心功能,采用了现代化的UI设计和良好的用户体验。

主要特点

  1. 完整的功能体系:涵盖了民宿预订的完整流程
  2. 优秀的用户体验:直观的界面设计和流畅的交互
  3. 灵活的数据模型:支持复杂的业务逻辑
  4. 可扩展的架构:便于后续功能扩展

学习收获

通过本教程,你将掌握:

  • Flutter应用的整体架构设计
  • 复杂数据模型的设计和管理
  • 用户界面的设计和实现
  • 状态管理和数据流控制
  • 用户交互和体验优化

这个民宿预订助手应用为你提供了一个完整的Flutter开发实践案例,可以作为学习Flutter开发的重要参考,也可以作为实际项目开发的基础框架。

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐