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

相关推荐
弓.长.2 小时前
基础入门 React Native 鸿蒙跨平台开发:网络请求实战
网络·react native·harmonyos
小白阿龙3 小时前
鸿蒙+flutter 跨平台开发——从零打造手持弹幕App实战
flutter·华为·harmonyos·鸿蒙
[H*]3 小时前
Flutter框架跨平台鸿蒙开发——文件下载器综合应用
flutter
编程乐学14 小时前
鸿蒙非原创--DevEcoStudio开发的奶茶点餐APP
华为·harmonyos·deveco studio·鸿蒙开发·奶茶点餐·鸿蒙大作业
鸣弦artha15 小时前
Flutter框架跨平台鸿蒙开发 —— Text Widget:文本展示的艺术
flutter·华为·harmonyos
lili-felicity16 小时前
React Native for Harmony:Rating 评分组件- 支持全星 / 半星 / 禁用 / 自定义样式
react native·华为·harmonyos
grd416 小时前
RN for OpenHarmony 小工具 App 实战:屏幕尺子实现
笔记·harmonyos
No Silver Bullet16 小时前
HarmonyOS NEXT开发进阶(十九):如何在 DevEco Studio 中查看已安装应用的运行日志
华为·harmonyos
大雷神18 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地
华为·harmonyos