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"
项目总结
技术亮点
- 数据可视化:自定义柱状图展示票房趋势
- 渐变设计:多处使用渐变效果提升视觉
- SliverAppBar:可折叠的详情页设计
- 排名徽章:根据排名动态显示颜色
- 数据统计:多维度票房数据展示
- 模拟数据:完整的票房数据生成逻辑
- 响应式布局:适配不同屏幕尺寸
学习收获
通过本项目,你将掌握:
- Flutter数据可视化基础
- 自定义图表绘制
- SliverAppBar使用
- 渐变和阴影效果
- 数据模型设计
- 列表排序和过滤
- SharedPreferences数据持久化
- 复杂UI布局
应用场景
本应用适用于:
- 电影票房查询
- 影视数据分析
- 票房趋势预测
- 电影推荐系统
后续优化方向
- 接入真实票房API
- 使用专业图表库(fl_chart)
- 添加票房预测功能
- 实现电影对比功能
- 添加影院排片查询
- 支持用户评论
- 添加票房提醒
- 数据导出功能
- 地区票房分布
- 生成分析报告
这个电影票房查询应用展示了Flutter在数据可视化和信息展示类应用开发中的强大能力。通过丰富的图表和精美的UI设计,为用户提供了直观的票房数据查询体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net