Flutter 框架跨平台鸿蒙开发 - 电影票房查询 - 完整开发教程

Flutter电影票房查询 - 完整开发教程

项目简介

这是一个使用Flutter开发的电影票房查询应用,提供实时票房排行、趋势分析、数据可视化和电影详情等功能。应用采用模拟数据的方式,展示了数据可视化和图表绘制的实现方法。
运行效果图





核心特性

  • 📊 票房排行:实时票房排行榜
  • 📈 趋势分析:每日票房趋势图表
  • 🎬 电影详情:完整的电影信息展示
  • 🔍 搜索功能:快速搜索电影
  • ⭐ 收藏管理:收藏关注的电影
  • 📉 数据可视化:柱状图展示票房趋势
  • 🎨 精美UI:渐变效果、卡片设计
  • 💾 数据持久化:使用SharedPreferences

技术栈

  • Flutter 3.6+
  • Dart 3.0+
  • shared_preferences: 数据持久化
  • Material Design 3
  • 自定义图表绘制

项目架构

电影票房查询
排行榜
趋势分析
搜索
收藏
电影详情
票房数据
趋势图表
基本信息

数据模型设计

Movie - 电影模型

dart 复制代码
class Movie {
  final String id;              // 唯一标识
  final String title;           // 中文片名
  final String englishTitle;    // 英文片名
  final String director;        // 导演
  final List<String> actors;    // 主演列表
  final String genre;           // 类型
  final int duration;           // 时长(分钟)
  final String releaseDate;     // 上映日期
  final String poster;          // 海报(emoji)
  final double rating;          // 评分
  final String description;     // 剧情简介
  bool isFavorite;              // 是否收藏
  
  Movie({
    required this.id,
    required this.title,
    required this.englishTitle,
    required this.director,
    required this.actors,
    required this.genre,
    required this.duration,
    required this.releaseDate,
    required this.poster,
    required this.rating,
    required this.description,
    this.isFavorite = false,
  });
}

BoxOfficeData - 票房数据模型

dart 复制代码
class BoxOfficeData {
  final String movieId;              // 电影ID
  final double todayBox;             // 今日票房(万元)
  final double totalBox;             // 总票房(万元)
  final int rank;                    // 排名
  final double boxShare;             // 票房占比
  final int screenings;              // 排片场次
  final double attendance;           // 上座率
  final List<DailyBoxOffice> dailyData;  // 每日票房
  
  BoxOfficeData({
    required this.movieId,
    required this.todayBox,
    required this.totalBox,
    required this.rank,
    required this.boxShare,
    required this.screenings,
    required this.attendance,
    required this.dailyData,
  });
}

DailyBoxOffice - 每日票房模型

dart 复制代码
class DailyBoxOffice {
  final String date;            // 日期
  final double boxOffice;       // 票房
  final int screenings;         // 场次
  final double attendance;      // 上座率
  
  DailyBoxOffice({
    required this.date,
    required this.boxOffice,
    required this.screenings,
    required this.attendance,
  });
}

核心功能实现

1. 票房数据生成

使用Random生成模拟票房数据:

dart 复制代码
void _generateBoxOfficeData() {
  final random = Random();
  for (int i = 0; i < allMovies.length; i++) {
    final movie = allMovies[i];
    final totalBox = 100000 - (i * 15000) + random.nextInt(5000);
    final todayBox = totalBox * 0.05 + random.nextInt(1000);
    
    // 生成每日票房数据
    final dailyData = <DailyBoxOffice>[];
    for (int day = 7; day >= 0; day--) {
      final date = DateTime.now().subtract(Duration(days: day));
      final dateStr = '${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
      dailyData.add(DailyBoxOffice(
        date: dateStr,
        boxOffice: todayBox * (0.8 + random.nextDouble() * 0.4),
        screenings: 5000 + random.nextInt(3000),
        attendance: 0.3 + random.nextDouble() * 0.4,
      ));
    }
    
    boxOfficeMap[movie.id] = BoxOfficeData(
      movieId: movie.id,
      todayBox: todayBox.toDouble(),
      totalBox: totalBox.toDouble(),
      rank: i + 1,
      boxShare: (30 - i * 3).toDouble(),
      screenings: 8000 - (i * 800),
      attendance: 0.7 - (i * 0.05),
      dailyData: dailyData,
    );
  }
}

数据生成特点:

  • 排名越高票房越高
  • 随机波动增加真实感
  • 生成8天历史数据
  • 计算票房占比和上座率

2. 总票房统计卡片

展示今日和累计总票房:

dart 复制代码
Widget _buildTotalBoxOfficeCard() {
  double totalBox = 0;
  double todayBox = 0;
  for (var data in boxOfficeMap.values) {
    totalBox += data.totalBox;
    todayBox += data.todayBox;
  }
  
  return Container(
    margin: const EdgeInsets.all(16),
    padding: const EdgeInsets.all(20),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.red.shade400, Colors.orange.shade300],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(16),
      boxShadow: [
        BoxShadow(
          color: Colors.red.withValues(alpha: 0.3),
          blurRadius: 8,
          offset: const Offset(0, 4),
        ),
      ],
    ),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        Column(
          children: [
            Text('今日总票房', style: TextStyle(color: Colors.white70)),
            SizedBox(height: 8),
            Text(
              '${(todayBox / 10000).toStringAsFixed(2)}亿',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
          ],
        ),
        Container(width: 1, height: 50, color: Colors.white30),
        Column(
          children: [
            Text('累计总票房', style: TextStyle(color: Colors.white70)),
            SizedBox(height: 8),
            Text(
              '${(totalBox / 10000).toStringAsFixed(2)}亿',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
          ],
        ),
      ],
    ),
  );
}

卡片特点:

  • 渐变背景
  • 阴影效果
  • 分隔线设计
  • 数据汇总展示

3. 排行榜卡片设计

创建信息丰富的排行榜卡片:

dart 复制代码
Widget _buildRankingCard(Movie movie, BoxOfficeData? boxOffice) {
  if (boxOffice == null) return const SizedBox.shrink();
  
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: InkWell(
      onTap: () => _openDetail(movie, boxOffice),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          children: [
            // 排名徽章
            Container(
              width: 40,
              height: 40,
              decoration: BoxDecoration(
                color: _getRankColor(boxOffice.rank),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Center(
                child: Text(
                  '${boxOffice.rank}',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                  ),
                ),
              ),
            ),
            SizedBox(width: 12),
            // 海报
            Container(
              width: 60,
              height: 80,
              decoration: BoxDecoration(
                color: Colors.grey.shade200,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Center(
                child: Text(movie.poster, style: TextStyle(fontSize: 32)),
              ),
            ),
            SizedBox(width: 12),
            // 电影信息
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    movie.title,
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                  Text(movie.genre, style: TextStyle(fontSize: 12)),
                  Row(
                    children: [
                      Icon(Icons.star, size: 14, color: Colors.amber),
                      Text('${movie.rating}'),
                      Icon(Icons.people, size: 14),
                      Text('${(boxOffice.attendance * 100).toStringAsFixed(0)}%'),
                    ],
                  ),
                ],
              ),
            ),
            // 票房数据
            Column(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Text(
                  '${(boxOffice.totalBox / 10000).toStringAsFixed(2)}亿',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.red,
                  ),
                ),
                Text('今日${(boxOffice.todayBox / 10000).toStringAsFixed(2)}亿'),
                Container(
                  padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                  decoration: BoxDecoration(
                    color: Colors.orange.withValues(alpha: 0.2),
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Text('${boxOffice.boxShare.toStringAsFixed(1)}%'),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

// 排名颜色
Color _getRankColor(int rank) {
  switch (rank) {
    case 1: return Colors.amber;        // 金色
    case 2: return Colors.grey.shade400; // 银色
    case 3: return Colors.brown.shade300; // 铜色
    default: return Colors.blue.shade300;
  }
}

4. 简易趋势图表

使用Container绘制柱状图:

dart 复制代码
Widget _buildSimpleTrendChart(List<DailyBoxOffice> dailyData) {
  if (dailyData.isEmpty) return const SizedBox.shrink();
  
  final maxBox = dailyData.map((d) => d.boxOffice).reduce(max);
  
  return SizedBox(
    height: 100,
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.end,
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: dailyData.map((data) {
        final height = (data.boxOffice / maxBox * 80).clamp(10.0, 80.0);
        return Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Container(
              width: 30,
              height: height,
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.blue.shade400, Colors.blue.shade200],
                  begin: Alignment.bottomCenter,
                  end: Alignment.topCenter,
                ),
                borderRadius: BorderRadius.vertical(
                  top: Radius.circular(4),
                ),
              ),
            ),
            SizedBox(height: 4),
            Text(
              data.date,
              style: TextStyle(fontSize: 9, color: Colors.grey.shade600),
            ),
          ],
        );
      }).toList(),
    ),
  );
}

图表特点:

  • 自动计算比例
  • 渐变柱状图
  • 日期标签
  • 响应式高度

5. 详细趋势图表

在详情页展示更详细的趋势图:

dart 复制代码
Widget _buildDetailedTrendChart() {
  if (boxOffice.dailyData.isEmpty) return const SizedBox.shrink();
  
  final maxBox = boxOffice.dailyData.map((d) => d.boxOffice).reduce(max);
  
  return Container(
    height: 200,
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.grey.shade50,
      borderRadius: BorderRadius.circular(12),
    ),
    child: Column(
      children: [
        Expanded(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.end,
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: boxOffice.dailyData.map((data) {
              final height = (data.boxOffice / maxBox * 140).clamp(20.0, 140.0);
              return Expanded(
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 2),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      // 数值标签
                      Text(
                        '${(data.boxOffice / 10000).toStringAsFixed(1)}',
                        style: TextStyle(fontSize: 9),
                      ),
                      SizedBox(height: 4),
                      // 柱状图
                      Container(
                        width: double.infinity,
                        height: height,
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                            colors: [Colors.blue.shade600, Colors.blue.shade300],
                            begin: Alignment.bottomCenter,
                            end: Alignment.topCenter,
                          ),
                          borderRadius: BorderRadius.vertical(
                            top: Radius.circular(4),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }).toList(),
          ),
        ),
        SizedBox(height: 8),
        // 日期标签
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: boxOffice.dailyData.map((data) {
            return Expanded(
              child: Text(
                data.date,
                style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
                textAlign: TextAlign.center,
              ),
            );
          }).toList(),
        ),
      ],
    ),
  );
}

详细图表特点:

  • 显示具体数值
  • 更大的展示空间
  • 背景容器
  • 完整的日期标签

6. 票房数据卡片

展示多维度票房数据:

dart 复制代码
Widget _buildBoxOfficeCard(String label, String value, Color color, IconData icon) {
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: color.withValues(alpha: 0.1),
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: color.withValues(alpha: 0.3)),
    ),
    child: Column(
      children: [
        Icon(icon, color: color, size: 28),
        SizedBox(height: 8),
        Text(
          value,
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
        SizedBox(height: 4),
        Text(
          label,
          style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
        ),
      ],
    ),
  );
}

// 使用示例
Row(
  children: [
    Expanded(
      child: _buildBoxOfficeCard(
        '总票房',
        '${(boxOffice.totalBox / 10000).toStringAsFixed(2)}亿',
        Colors.red,
        Icons.attach_money,
      ),
    ),
    SizedBox(width: 12),
    Expanded(
      child: _buildBoxOfficeCard(
        '今日票房',
        '${(boxOffice.todayBox / 10000).toStringAsFixed(2)}亿',
        Colors.orange,
        Icons.today,
      ),
    ),
  ],
)

数据卡片特点:

  • 图标标识
  • 颜色区分
  • 边框设计
  • 响应式布局

7. SliverAppBar详情页

使用SliverAppBar创建可折叠的详情页:

dart 复制代码
Widget build(BuildContext context) {
  return Scaffold(
    body: CustomScrollView(
      slivers: [
        // 可折叠AppBar
        SliverAppBar(
          expandedHeight: 200,
          pinned: true,
          flexibleSpace: FlexibleSpaceBar(
            title: Text(movie.title),
            background: Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.red.shade400, Colors.orange.shade300],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: Center(
                child: Text(movie.poster, style: TextStyle(fontSize: 80)),
              ),
            ),
          ),
          actions: [
            IconButton(
              icon: Icon(movie.isFavorite ? Icons.star : Icons.star_border),
              onPressed: () => onToggleFavorite(movie),
            ),
          ],
        ),
        // 内容区域
        SliverToBoxAdapter(
          child: Column(
            children: [
              _buildBasicInfo(),
              Divider(height: 32),
              _buildBoxOfficeInfo(),
              Divider(height: 32),
              _buildTrendChart(),
              Divider(height: 32),
              _buildDescription(),
            ],
          ),
        ),
      ],
    ),
  );
}

SliverAppBar特点:

  • 可折叠展开
  • 渐变背景
  • 固定标题栏
  • 流畅滚动

UI组件设计

1. 渐变卡片

创建带渐变和阴影的卡片:

dart 复制代码
Container(
  margin: const EdgeInsets.all(16),
  padding: const EdgeInsets.all(20),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.red.shade400, Colors.orange.shade300],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
      BoxShadow(
        color: Colors.red.withValues(alpha: 0.3),
        blurRadius: 8,
        offset: const Offset(0, 4),
      ),
    ],
  ),
  child: // 内容
)

2. 排名徽章

根据排名显示不同颜色:

dart 复制代码
Container(
  width: 40,
  height: 40,
  decoration: BoxDecoration(
    color: _getRankColor(rank),
    borderRadius: BorderRadius.circular(8),
  ),
  child: Center(
    child: Text(
      '$rank',
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
        color: Colors.white,
      ),
    ),
  ),
)

3. 数据标签

小型数据展示标签:

dart 复制代码
Container(
  padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  decoration: BoxDecoration(
    color: Colors.orange.withValues(alpha: 0.2),
    borderRadius: BorderRadius.circular(10),
  ),
  child: Text(
    '${boxShare.toStringAsFixed(1)}%',
    style: TextStyle(fontSize: 10, color: Colors.orange),
  ),
)

4. 底部导航栏

Material 3风格导航栏:

dart 复制代码
NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() {
      _selectedIndex = index;
    });
  },
  destinations: const [
    NavigationDestination(
      icon: Icon(Icons.leaderboard_outlined),
      selectedIcon: Icon(Icons.leaderboard),
      label: '排行',
    ),
    NavigationDestination(
      icon: Icon(Icons.trending_up_outlined),
      selectedIcon: Icon(Icons.trending_up),
      label: '趋势',
    ),
    NavigationDestination(
      icon: Icon(Icons.search_outlined),
      selectedIcon: Icon(Icons.search),
      label: '搜索',
    ),
    NavigationDestination(
      icon: Icon(Icons.star_outline),
      selectedIcon: Icon(Icons.star),
      label: '收藏',
    ),
  ],
)

功能扩展建议

1. 真实API集成

接入真实票房数据API:

dart 复制代码
import 'package:http/http.dart' as http;

class BoxOfficeApiService {
  static const String baseUrl = 'https://api.boxoffice.com';
  
  // 获取实时票房排行
  Future<List<BoxOfficeData>> fetchRealTimeBoxOffice() async {
    final response = await http.get(
      Uri.parse('$baseUrl/realtime'),
    );
    
    if (response.statusCode == 200) {
      final List data = jsonDecode(response.body);
      return data.map((json) => BoxOfficeData.fromJson(json)).toList();
    }
    throw Exception('Failed to load box office data');
  }
  
  // 获取电影详情
  Future<Movie> fetchMovieDetail(String movieId) async {
    final response = await http.get(
      Uri.parse('$baseUrl/movie/$movieId'),
    );
    
    if (response.statusCode == 200) {
      return Movie.fromJson(jsonDecode(response.body));
    }
    throw Exception('Failed to load movie detail');
  }
  
  // 获取历史票房数据
  Future<List<DailyBoxOffice>> fetchHistoricalData(
    String movieId,
    DateTime startDate,
    DateTime endDate,
  ) async {
    final response = await http.get(
      Uri.parse('$baseUrl/history/$movieId?start=$startDate&end=$endDate'),
    );
    
    if (response.statusCode == 200) {
      final List data = jsonDecode(response.body);
      return data.map((json) => DailyBoxOffice.fromJson(json)).toList();
    }
    throw Exception('Failed to load historical data');
  }
}

2. 高级图表库

使用fl_chart绘制专业图表:

dart 复制代码
import 'package:fl_chart/fl_chart.dart';

Widget _buildLineChart(List<DailyBoxOffice> data) {
  return LineChart(
    LineChartData(
      gridData: FlGridData(show: true),
      titlesData: FlTitlesData(
        leftTitles: AxisTitles(
          sideTitles: SideTitles(showTitles: true),
        ),
        bottomTitles: AxisTitles(
          sideTitles: SideTitles(
            showTitles: true,
            getTitlesWidget: (value, meta) {
              return Text(data[value.toInt()].date);
            },
          ),
        ),
      ),
      borderData: FlBorderData(show: true),
      lineBarsData: [
        LineChartBarData(
          spots: data.asMap().entries.map((entry) {
            return FlSpot(
              entry.key.toDouble(),
              entry.value.boxOffice,
            );
          }).toList(),
          isCurved: true,
          color: Colors.blue,
          barWidth: 3,
          dotData: FlDotData(show: true),
        ),
      ],
    ),
  );
}

3. 票房预测功能

基于历史数据预测未来票房:

dart 复制代码
class BoxOfficePrediction {
  // 简单线性回归预测
  static double predictBoxOffice(List<DailyBoxOffice> historicalData, int daysAhead) {
    if (historicalData.length < 2) return 0;
    
    // 计算平均增长率
    double totalGrowth = 0;
    for (int i = 1; i < historicalData.length; i++) {
      final growth = historicalData[i].boxOffice - historicalData[i - 1].boxOffice;
      totalGrowth += growth;
    }
    final avgGrowth = totalGrowth / (historicalData.length - 1);
    
    // 预测未来票房
    final lastBox = historicalData.last.boxOffice;
    return lastBox + (avgGrowth * daysAhead);
  }
  
  Widget _buildPredictionCard(double predictedBox) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Row(
              children: [
                Icon(Icons.analytics, color: Colors.purple),
                SizedBox(width: 8),
                Text(
                  '票房预测',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
              ],
            ),
            SizedBox(height: 12),
            Text(
              '预计最终票房',
              style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
            ),
            SizedBox(height: 8),
            Text(
              '${(predictedBox / 10000).toStringAsFixed(2)}亿',
              style: TextStyle(
                fontSize: 32,
                fontWeight: FontWeight.bold,
                color: Colors.purple,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

4. 票房对比功能

对比多部电影的票房表现:

dart 复制代码
class BoxOfficeComparison extends StatelessWidget {
  final List<Movie> movies;
  final Map<String, BoxOfficeData> boxOfficeMap;
  
  const BoxOfficeComparison({
    super.key,
    required this.movies,
    required this.boxOfficeMap,
  });
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('票房对比')),
      body: Column(
        children: [
          // 对比图表
          _buildComparisonChart(),
          // 详细数据表格
          _buildComparisonTable(),
        ],
      ),
    );
  }
  
  Widget _buildComparisonChart() {
    return Container(
      height: 300,
      padding: EdgeInsets.all(16),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: movies.map((movie) {
          final boxOffice = boxOfficeMap[movie.id];
          if (boxOffice == null) return SizedBox.shrink();
          
          final maxBox = boxOfficeMap.values
              .map((b) => b.totalBox)
              .reduce(max);
          final height = (boxOffice.totalBox / maxBox * 250).clamp(50.0, 250.0);
          
          return Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Text(
                '${(boxOffice.totalBox / 10000).toStringAsFixed(1)}亿',
                style: TextStyle(fontSize: 12),
              ),
              SizedBox(height: 4),
              Container(
                width: 60,
                height: height,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Colors.blue.shade600, Colors.blue.shade300],
                    begin: Alignment.bottomCenter,
                    end: Alignment.topCenter,
                  ),
                  borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
                ),
              ),
              SizedBox(height: 8),
              Text(
                movie.title,
                style: TextStyle(fontSize: 10),
                maxLines: 2,
                textAlign: TextAlign.center,
              ),
            ],
          );
        }).toList(),
      ),
    );
  }
}

5. 影院排片查询

查询影院排片信息:

dart 复制代码
class Cinema {
  final String id;
  final String name;
  final String address;
  final double distance;
  final List<Screening> screenings;
  
  Cinema({
    required this.id,
    required this.name,
    required this.address,
    required this.distance,
    required this.screenings,
  });
}

class Screening {
  final String movieId;
  final String time;
  final String hall;
  final double price;
  final int availableSeats;
  
  Screening({
    required this.movieId,
    required this.time,
    required this.hall,
    required this.price,
    required this.availableSeats,
  });
}

Widget _buildCinemaList(List<Cinema> cinemas) {
  return ListView.builder(
    itemCount: cinemas.length,
    itemBuilder: (context, index) {
      final cinema = cinemas[index];
      return Card(
        child: ExpansionTile(
          title: Text(cinema.name),
          subtitle: Text('${cinema.distance}km | ${cinema.screenings.length}场'),
          children: cinema.screenings.map((screening) {
            return ListTile(
              title: Text(screening.time),
              subtitle: Text('${screening.hall} | ¥${screening.price}'),
              trailing: Text('${screening.availableSeats}座'),
              onTap: () => _bookTicket(screening),
            );
          }).toList(),
        ),
      );
    },
  );
}

6. 用户评论功能

添加用户评论和评分:

dart 复制代码
class MovieReview {
  final String id;
  final String userId;
  final String userName;
  final double rating;
  final String content;
  final DateTime timestamp;
  final int likes;
  
  MovieReview({
    required this.id,
    required this.userId,
    required this.userName,
    required this.rating,
    required this.content,
    required this.timestamp,
    required this.likes,
  });
}

Widget _buildReviewList(List<MovieReview> reviews) {
  return ListView.builder(
    itemCount: reviews.length,
    itemBuilder: (context, index) {
      final review = reviews[index];
      return Card(
        margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  CircleAvatar(child: Text(review.userName[0])),
                  SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          review.userName,
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                        Row(
                          children: [
                            ...List.generate(5, (i) {
                              return Icon(
                                i < review.rating ? Icons.star : Icons.star_border,
                                size: 16,
                                color: Colors.amber,
                              );
                            }),
                            SizedBox(width: 8),
                            Text(
                              _formatTime(review.timestamp),
                              style: TextStyle(fontSize: 12, color: Colors.grey),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              SizedBox(height: 12),
              Text(review.content),
              SizedBox(height: 8),
              Row(
                children: [
                  TextButton.icon(
                    icon: Icon(Icons.thumb_up_outlined, size: 16),
                    label: Text('${review.likes}'),
                    onPressed: () {},
                  ),
                ],
              ),
            ],
          ),
        ),
      );
    },
  );
}

7. 票房提醒功能

设置票房里程碑提醒:

dart 复制代码
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class BoxOfficeNotificationService {
  final FlutterLocalNotificationsPlugin _notifications = 
      FlutterLocalNotificationsPlugin();
  
  Future<void> setMilestoneAlert(Movie movie, double targetBox) async {
    await _notifications.show(
      movie.id.hashCode,
      '票房里程碑',
      '《${movie.title}》票房即将突破${(targetBox / 10000).toStringAsFixed(0)}亿!',
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'box_office_milestone',
          '票房提醒',
          importance: Importance.high,
        ),
      ),
    );
  }
  
  Future<void> scheduleDailyUpdate() async {
    await _notifications.zonedSchedule(
      0,
      '每日票房更新',
      '查看今日最新票房排行',
      _nextInstanceOfTime(20, 0),  // 每天晚上8点
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'daily_update',
          '每日更新',
          importance: Importance.high,
        ),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time,
    );
  }
}

8. 数据导出功能

导出票房数据为CSV或Excel:

dart 复制代码
import 'package:csv/csv.dart';
import 'package:path_provider/path_provider.dart';

class DataExportService {
  Future<String> exportToCSV(List<Movie> movies, Map<String, BoxOfficeData> boxOfficeMap) async {
    List<List<dynamic>> rows = [];
    
    // 表头
    rows.add([
      '排名',
      '电影名称',
      '总票房(万元)',
      '今日票房(万元)',
      '票房占比',
      '排片场次',
      '上座率',
      '评分',
    ]);
    
    // 数据行
    for (var movie in movies) {
      final boxOffice = boxOfficeMap[movie.id];
      if (boxOffice != null) {
        rows.add([
          boxOffice.rank,
          movie.title,
          boxOffice.totalBox,
          boxOffice.todayBox,
          '${boxOffice.boxShare}%',
          boxOffice.screenings,
          '${(boxOffice.attendance * 100).toStringAsFixed(0)}%',
          movie.rating,
        ]);
      }
    }
    
    // 转换为CSV
    String csv = const ListToCsvConverter().convert(rows);
    
    // 保存文件
    final directory = await getApplicationDocumentsDirectory();
    final path = '${directory.path}/box_office_${DateTime.now().millisecondsSinceEpoch}.csv';
    final file = File(path);
    await file.writeAsString(csv);
    
    return path;
  }
  
  Future<void> shareData(String filePath) async {
    await Share.shareXFiles([XFile(filePath)]);
  }
}

9. 票房分析报告

生成详细的票房分析报告:

dart 复制代码
class BoxOfficeAnalysis {
  final Movie movie;
  final BoxOfficeData boxOffice;
  
  BoxOfficeAnalysis({
    required this.movie,
    required this.boxOffice,
  });
  
  // 计算日均票房
  double get dailyAverage {
    if (boxOffice.dailyData.isEmpty) return 0;
    final total = boxOffice.dailyData
        .map((d) => d.boxOffice)
        .reduce((a, b) => a + b);
    return total / boxOffice.dailyData.length;
  }
  
  // 计算增长率
  double get growthRate {
    if (boxOffice.dailyData.length < 2) return 0;
    final first = boxOffice.dailyData.first.boxOffice;
    final last = boxOffice.dailyData.last.boxOffice;
    return ((last - first) / first) * 100;
  }
  
  // 预测最终票房
  double get predictedFinalBox {
    // 简化预测:基于当前趋势
    return boxOffice.totalBox * 1.2;
  }
  
  Widget buildAnalysisReport() {
    return Card(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '票房分析报告',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 16),
            _buildAnalysisItem('日均票房', '${(dailyAverage / 10000).toStringAsFixed(2)}亿'),
            _buildAnalysisItem('增长率', '${growthRate.toStringAsFixed(1)}%'),
            _buildAnalysisItem('预测最终票房', '${(predictedFinalBox / 10000).toStringAsFixed(2)}亿'),
            _buildAnalysisItem('市场表现', _getPerformanceLevel()),
          ],
        ),
      ),
    );
  }
  
  String _getPerformanceLevel() {
    if (boxOffice.rank <= 3) return '优秀';
    if (boxOffice.rank <= 5) return '良好';
    return '一般';
  }
}

10. 地区票房分布

展示不同地区的票房分布:

dart 复制代码
class RegionalBoxOffice {
  final String region;
  final double boxOffice;
  final double percentage;
  
  RegionalBoxOffice({
    required this.region,
    required this.boxOffice,
    required this.percentage,
  });
}

Widget _buildRegionalDistribution(List<RegionalBoxOffice> data) {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '地区票房分布',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 16),
          ...data.map((region) {
            return Padding(
              padding: EdgeInsets.only(bottom: 12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(region.region),
                      Text(
                        '${(region.boxOffice / 10000).toStringAsFixed(2)}亿',
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                  SizedBox(height: 4),
                  LinearProgressIndicator(
                    value: region.percentage / 100,
                    backgroundColor: Colors.grey.shade200,
                    valueColor: AlwaysStoppedAnimation(Colors.blue),
                  ),
                  SizedBox(height: 4),
                  Text(
                    '${region.percentage.toStringAsFixed(1)}%',
                    style: TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                ],
              ),
            );
          }),
        ],
      ),
    ),
  );
}

性能优化建议

1. 图表渲染优化

使用CustomPainter优化图表绘制:

dart 复制代码
class BoxOfficeChartPainter extends CustomPainter {
  final List<DailyBoxOffice> data;
  final Color color;
  
  BoxOfficeChartPainter({required this.data, required this.color});
  
  @override
  void paint(Canvas canvas, Size size) {
    if (data.isEmpty) return;
    
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;
    
    final maxBox = data.map((d) => d.boxOffice).reduce(max);
    final barWidth = size.width / data.length;
    
    for (int i = 0; i < data.length; i++) {
      final height = (data[i].boxOffice / maxBox) * size.height;
      final rect = Rect.fromLTWH(
        i * barWidth,
        size.height - height,
        barWidth * 0.8,
        height,
      );
      canvas.drawRRect(
        RRect.fromRectAndRadius(rect, Radius.circular(4)),
        paint,
      );
    }
  }
  
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

2. 数据缓存

缓存票房数据减少重复请求:

dart 复制代码
class BoxOfficeCache {
  static final Map<String, CachedData> _cache = {};
  
  static Future<BoxOfficeData?> get(String movieId) async {
    final cached = _cache[movieId];
    if (cached != null && !cached.isExpired) {
      return cached.data;
    }
    return null;
  }
  
  static void set(String movieId, BoxOfficeData data) {
    _cache[movieId] = CachedData(
      data: data,
      timestamp: DateTime.now(),
    );
  }
}

class CachedData {
  final BoxOfficeData data;
  final DateTime timestamp;
  
  CachedData({required this.data, required this.timestamp});
  
  bool get isExpired {
    return DateTime.now().difference(timestamp).inMinutes > 30;
  }
}

3. 列表优化

使用ListView.builder和分页加载:

dart 复制代码
class PaginatedBoxOfficeList extends StatefulWidget {
  @override
  State<PaginatedBoxOfficeList> createState() => _PaginatedBoxOfficeListState();
}

class _PaginatedBoxOfficeListState extends State<PaginatedBoxOfficeList> {
  final ScrollController _scrollController = ScrollController();
  List<Movie> _displayedMovies = [];
  int _currentPage = 0;
  final int _pageSize = 20;
  bool _isLoading = false;
  
  @override
  void initState() {
    super.initState();
    _loadMore();
    _scrollController.addListener(_onScroll);
  }
  
  void _onScroll() {
    if (_scrollController.position.pixels >= 
        _scrollController.position.maxScrollExtent * 0.8) {
      _loadMore();
    }
  }
  
  Future<void> _loadMore() async {
    if (_isLoading) return;
    
    setState(() {
      _isLoading = true;
    });
    
    // 模拟加载
    await Future.delayed(Duration(milliseconds: 500));
    
    final start = _currentPage * _pageSize;
    final end = min(start + _pageSize, allMovies.length);
    
    setState(() {
      _displayedMovies.addAll(allMovies.sublist(start, end));
      _currentPage++;
      _isLoading = false;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _displayedMovies.length + (_isLoading ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == _displayedMovies.length) {
          return Center(child: CircularProgressIndicator());
        }
        return _buildMovieCard(_displayedMovies[index]);
      },
    );
  }
}

4. 图片优化

优化海报图片加载:

dart 复制代码
import 'package:cached_network_image/cached_network_image.dart';

Widget _buildPoster(String imageUrl) {
  return CachedNetworkImage(
    imageUrl: imageUrl,
    placeholder: (context, url) => Container(
      color: Colors.grey.shade200,
      child: Center(child: CircularProgressIndicator()),
    ),
    errorWidget: (context, url, error) => Container(
      color: Colors.grey.shade200,
      child: Icon(Icons.error),
    ),
    memCacheWidth: 200,
    memCacheHeight: 300,
    fit: BoxFit.cover,
  );
}

测试建议

1. 单元测试

测试票房数据计算:

dart 复制代码
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('BoxOffice Tests', () {
    test('Calculate total box office', () {
      final data = [
        DailyBoxOffice(date: '01-01', boxOffice: 1000, screenings: 100, attendance: 0.5),
        DailyBoxOffice(date: '01-02', boxOffice: 1500, screenings: 120, attendance: 0.6),
      ];
      
      final total = data.map((d) => d.boxOffice).reduce((a, b) => a + b);
      expect(total, 2500);
    });
    
    test('Calculate growth rate', () {
      final first = 1000.0;
      final last = 1500.0;
      final growthRate = ((last - first) / first) * 100;
      expect(growthRate, 50.0);
    });
  });
}

2. Widget测试

测试排行榜卡片:

dart 复制代码
void main() {
  testWidgets('RankingCard displays correctly', (WidgetTester tester) async {
    final movie = Movie(
      id: '1',
      title: '测试电影',
      englishTitle: 'Test Movie',
      director: '测试导演',
      actors: ['演员1', '演员2'],
      genre: '动作',
      duration: 120,
      releaseDate: '2024-01-01',
      poster: '🎬',
      rating: 8.5,
      description: '测试描述',
    );
    
    final boxOffice = BoxOfficeData(
      movieId: '1',
      todayBox: 5000,
      totalBox: 50000,
      rank: 1,
      boxShare: 25.0,
      screenings: 8000,
      attendance: 0.7,
      dailyData: [],
    );
    
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: _buildRankingCard(movie, boxOffice),
        ),
      ),
    );
    
    expect(find.text('测试电影'), findsOneWidget);
    expect(find.text('1'), findsOneWidget);
    expect(find.byIcon(Icons.star), findsOneWidget);
  });
}

部署发布

1. Android打包

bash 复制代码
# 生成签名密钥
keytool -genkey -v -keystore ~/boxoffice-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias boxoffice

# 构建APK
flutter build apk --release

# 构建App Bundle
flutter build appbundle --release

2. iOS打包

bash 复制代码
# 构建IPA
flutter build ipa --release

3. 应用图标

yaml 复制代码
dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/icon/app_icon.png"

项目总结

技术亮点

  1. 数据可视化:自定义柱状图展示票房趋势
  2. 渐变设计:多处使用渐变效果提升视觉
  3. SliverAppBar:可折叠的详情页设计
  4. 排名徽章:根据排名动态显示颜色
  5. 数据统计:多维度票房数据展示
  6. 模拟数据:完整的票房数据生成逻辑
  7. 响应式布局:适配不同屏幕尺寸

学习收获

通过本项目,你将掌握:

  • Flutter数据可视化基础
  • 自定义图表绘制
  • SliverAppBar使用
  • 渐变和阴影效果
  • 数据模型设计
  • 列表排序和过滤
  • SharedPreferences数据持久化
  • 复杂UI布局

应用场景

本应用适用于:

  • 电影票房查询
  • 影视数据分析
  • 票房趋势预测
  • 电影推荐系统

后续优化方向

  1. 接入真实票房API
  2. 使用专业图表库(fl_chart)
  3. 添加票房预测功能
  4. 实现电影对比功能
  5. 添加影院排片查询
  6. 支持用户评论
  7. 添加票房提醒
  8. 数据导出功能
  9. 地区票房分布
  10. 生成分析报告

这个电影票房查询应用展示了Flutter在数据可视化和信息展示类应用开发中的强大能力。通过丰富的图表和精美的UI设计,为用户提供了直观的票房数据查询体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
MardaWang1 小时前
当滚动逃离了框架 ——HarmonyOS Web 与原生混排滚动的冲突本质与解法
harmonyos·arkts·鸿蒙·deveco studio
tsqtsqtsq03092 小时前
DevEco Studio 介绍
harmonyos
传奇开心果编程5 小时前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
HwJack205 小时前
【共创稿事节】HarmonyOS 7文旅展陈展厅大空间 3DGS 重建的分块策略与拼接踩坑
3d·华为·harmonyos
熊猫钓鱼>_>7 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
开发语言·华为·kotlin·ai编程·inject·鸿蒙·openharmony
m0_738185828 小时前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_738185829 小时前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
翼辉cto9 小时前
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
开发语言·kotlin·harmonyos
SuperHeroWu79 小时前
TraeCode 国内版接入 DevEco CLI:用官方知识开发鸿蒙应用
ai编程·harmonyos·知识库·trae·aicoding·skills·deveco cli