Flutter框架跨平台鸿蒙开发——电影推荐APP的开发流程
🚀运行效果展示



Flutter框架跨平台鸿蒙开发——电影推荐APP的开发流程
前言
随着移动互联网的快速发展,跨平台开发成为了移动应用开发的重要趋势。Flutter作为Google推出的开源UI工具包,凭借其"一次编写,处处运行"的特性,成为了跨平台开发的热门选择。同时,鸿蒙系统作为华为自主研发的操作系统,也在快速崛起,为开发者提供了新的平台选择。
本文将详细介绍如何使用Flutter框架开发一款电影推荐及影评APP,并实现跨平台运行,特别是在鸿蒙系统上的适配。通过本文的学习,读者将了解Flutter框架的核心特性、跨平台开发的实践经验,以及如何构建一个功能完整的电影推荐APP。
应用介绍
应用概述
电影推荐及影评APP是一款为用户提供电影信息、推荐和影评功能的移动应用。用户可以通过该应用浏览最新、热门的电影,查看电影详情,提交和查看影评,以及管理个人收藏和观看历史。
应用特点
- 丰富的电影信息:提供电影的详细信息,包括标题、海报、评分、简介、上映日期、类型等。
- 个性化推荐:根据用户的浏览历史和收藏,推荐相关电影。
- 互动影评:用户可以提交影评,查看其他用户的影评,分享观影感受。
- 个人中心:管理用户个人信息、收藏的电影和观看历史。
- 跨平台兼容:支持在Android、iOS、鸿蒙等多个平台运行。
技术栈
- 前端框架:Flutter
- 开发语言:Dart
- 状态管理:Provider
- 网络请求:http
- 本地存储:shared_preferences
- UI组件:Material Design
开发流程
1. 项目初始化与环境搭建
1.1 安装Flutter SDK
首先,需要在开发机器上安装Flutter SDK。可以从Flutter官网下载最新版本的Flutter SDK,并按照官方文档进行安装和配置。
1.2 创建Flutter项目
使用Flutter命令行工具创建一个新的Flutter项目:
flutter create movie_recommendation_app
cd movie_recommendation_app
1.3 配置依赖
在pubspec.yaml文件中添加必要的依赖:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1
http: ^1.2.1
shared_preferences: ^2.2.3
intl: ^0.19.0
flutter:
uses-material-design: true
assets:
- assets/images/
2. 项目结构设计
2.1 目录结构
lib/
├── models/ # 数据模型
├── services/ # 服务层
├── utils/ # 工具类
├── pages/ # 页面
├── components/ # 组件
├── main.dart # 应用入口
assets/
├── images/ # 图片资源
2.2 数据模型设计
创建电影、影评和用户的数据模型:
/// 电影模型
class Movie {
final int id;
final String title;
final String posterPath;
final double voteAverage;
final String overview;
final String releaseDate;
final List<String> genres;
final int runtime;
final int budget;
final int revenue;
final String originalLanguage;
final String status;
Movie({
required this.id,
required this.title,
required this.posterPath,
required this.voteAverage,
required this.overview,
required this.releaseDate,
required this.genres,
required this.runtime,
required this.budget,
required this.revenue,
required this.originalLanguage,
required this.status,
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
id: json['id'] ?? 0,
title: json['title'] ?? '',
posterPath: json['poster_path'] ?? '',
voteAverage: (json['vote_average'] ?? 0).toDouble(),
overview: json['overview'] ?? '',
releaseDate: json['release_date'] ?? '',
genres: (json['genres'] as List<dynamic>?)?.map((e) => e['name'] as String).toList() ?? [],
runtime: json['runtime'] ?? 0,
budget: json['budget'] ?? 0,
revenue: json['revenue'] ?? 0,
originalLanguage: json['original_language'] ?? '',
status: json['status'] ?? '',
);
}
}
/// 电影评论模型
class MovieReview {
final int id;
final String content;
final String author;
final double rating;
final String createdAt;
final int movieId;
MovieReview({
required this.id,
required this.content,
required this.author,
required this.rating,
required this.createdAt,
required this.movieId,
});
factory MovieReview.fromJson(Map<String, dynamic> json) {
return MovieReview(
id: json['id'] ?? 0,
content: json['content'] ?? '',
author: json['author'] ?? '',
rating: (json['rating'] ?? 0).toDouble(),
createdAt: json['created_at'] ?? '',
movieId: json['movie_id'] ?? 0,
);
}
}
/// 用户模型
class User {
final int id;
final String username;
final String email;
final String avatarPath;
final String createdAt;
User({
required this.id,
required this.username,
required this.email,
required this.avatarPath,
required this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] ?? 0,
username: json['username'] ?? '',
email: json['email'] ?? '',
avatarPath: json['avatar_path'] ?? '',
createdAt: json['created_at'] ?? '',
);
}
}
3. 核心功能实现
3.1 电影服务
创建电影服务,用于处理电影相关的API请求和数据管理:
/// 电影服务
class MovieService {
/// 获取推荐电影列表
Future<List<Movie>> getRecommendedMovies({int page = 1, int limit = 20}) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 500));
// 使用模拟数据
final moviesData = MockData.movies;
// 计算起始和结束索引
final startIndex = (page - 1) * limit;
final endIndex = startIndex + limit;
// 截取数据
final paginatedMovies = moviesData.sublist(
startIndex,
endIndex > moviesData.length ? moviesData.length : endIndex,
);
return paginatedMovies;
}
/// 获取电影详情
Future<Movie?> getMovieDetails(int movieId) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 300));
// 使用模拟数据
final moviesData = MockData.movies;
// 查找电影
final movie = moviesData.firstWhere(
(movie) => movie.id == movieId,
orElse: () => Movie(
id: 0,
title: '',
posterPath: '',
voteAverage: 0,
overview: '',
releaseDate: '',
genres: [],
runtime: 0,
budget: 0,
revenue: 0,
originalLanguage: '',
status: '',
),
);
return movie.id > 0 ? movie : null;
}
/// 获取电影评论
Future<List<MovieReview>> getMovieReviews(int movieId, {int page = 1, int limit = 20}) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 300));
// 使用模拟数据
final reviewsData = MockData.reviews;
// 筛选电影评论
final movieReviews = reviewsData.where(
(review) => review.movieId == movieId,
).toList();
// 计算起始和结束索引
final startIndex = (page - 1) * limit;
final endIndex = startIndex + limit;
// 截取数据
final paginatedReviews = movieReviews.sublist(
startIndex,
endIndex > movieReviews.length ? movieReviews.length : endIndex,
);
return paginatedReviews;
}
/// 提交电影评论
Future<MovieReview> submitMovieReview(int movieId, String content, double rating, String author) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 500));
// 创建新评论
final newReview = MovieReview(
id: DateTime.now().millisecondsSinceEpoch,
content: content,
author: author,
rating: rating,
createdAt: DateTime.now().toIso8601String(),
movieId: movieId,
);
// 模拟添加到数据库
MockData.reviews.add(newReview);
return newReview;
}
/// 获取电影类型列表
Future<List<String>> getMovieGenres() async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 200));
// 使用模拟数据
return MockData.genres;
}
/// 获取热门电影
Future<List<Movie>> getPopularMovies({int page = 1, int limit = 20}) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 400));
// 使用模拟数据
final moviesData = MockData.movies;
// 按评分排序
final sortedMovies = [...moviesData]
..sort((a, b) => b.voteAverage.compareTo(a.voteAverage));
// 计算起始和结束索引
final startIndex = (page - 1) * limit;
final endIndex = startIndex + limit;
// 截取数据
final paginatedMovies = sortedMovies.sublist(
startIndex,
endIndex > sortedMovies.length ? sortedMovies.length : endIndex,
);
return paginatedMovies;
}
/// 获取最新电影
Future<List<Movie>> getLatestMovies({int page = 1, int limit = 20}) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 400));
// 使用模拟数据
final moviesData = MockData.movies;
// 按上映日期排序
final sortedMovies = [...moviesData]
..sort((a, b) => b.releaseDate.compareTo(a.releaseDate));
// 计算起始和结束索引
final startIndex = (page - 1) * limit;
final endIndex = startIndex + limit;
// 截取数据
final paginatedMovies = sortedMovies.sublist(
startIndex,
endIndex > sortedMovies.length ? sortedMovies.length : endIndex,
);
return paginatedMovies;
}
}
/// 电影服务单例
final movieService = MovieService();
3.2 电影列表页面
实现电影列表页面,用于显示推荐电影、热门电影和最新电影:
/// 电影列表页面
class MovieListPage extends StatefulWidget {
/// 构造函数
const MovieListPage({super.key});
State<MovieListPage> createState() => _MovieListPageState();
}
/// 电影列表页面状态类
class _MovieListPageState extends State<MovieListPage> {
/// 推荐电影列表
List<Movie> _recommendedMovies = [];
/// 热门电影列表
List<Movie> _popularMovies = [];
/// 最新电影列表
List<Movie> _latestMovies = [];
/// 电影类型列表
List<String> _genres = [];
/// 当前选中的类型
String? _selectedGenre;
/// 加载状态
bool _isLoading = true;
/// 错误信息
String? _errorMessage;
void initState() {
super.initState();
_loadMovies();
}
/// 加载电影数据
Future<void> _loadMovies() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// 并行加载数据
final results = await Future.wait([
movieService.getRecommendedMovies(),
movieService.getPopularMovies(),
movieService.getLatestMovies(),
movieService.getMovieGenres(),
]);
setState(() {
_recommendedMovies = results[0] as List<Movie>;
_popularMovies = results[1] as List<Movie>;
_latestMovies = results[2] as List<Movie>;
_genres = results[3] as List<String>;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = '加载电影数据失败,请重试';
_isLoading = false;
});
}
}
/// 处理类型选择
void _handleGenreSelect(String genre) {
setState(() {
_selectedGenre = _selectedGenre == genre ? null : genre;
});
}
/// 导航到电影详情页面
void _navigateToMovieDetail(int movieId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MovieDetailPage(movieId: movieId),
),
);
}
/// 构建电影卡片
Widget _buildMovieCard(Movie movie) {
return SizedBox(
width: 140,
height: 224,
child: GestureDetector(
onTap: () => _navigateToMovieDetail(movie.id),
child: Container(
padding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 电影海报
SizedBox(
width: 140,
height: 190,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
image: DecorationImage(
image: AssetImage(movie.posterPath),
fit: BoxFit.cover,
),
),
),
),
// 电影标题
SizedBox(
width: 140,
height: 16,
child: Text(
movie.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
// 电影评分
SizedBox(
width: 140,
height: 16,
child: Row(
children: [
const Icon(
Icons.star,
size: 10,
color: Colors.amber,
),
const SizedBox(width: 2),
Text(
'${movie.voteAverage}',
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
),
],
),
),
],
),
),
),
);
}
/// 构建电影列表
Widget _buildMovieList(String title, List<Movie> movies) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(
height: 224,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 4),
itemCount: movies.length,
itemBuilder: (context, index) {
return _buildMovieCard(movies[index]);
},
),
),
],
);
}
/// 构建类型选择器
Widget _buildGenreSelector() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: _genres.map((genre) {
final isSelected = _selectedGenre == genre;
return GestureDetector(
onTap: () => _handleGenreSelect(genre),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: isSelected ? Colors.blue : Colors.grey[200],
),
child: Text(
genre,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontSize: 14,
),
),
),
);
}).toList(),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('电影推荐'),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_errorMessage!),
ElevatedButton(
onPressed: _loadMovies,
child: const Text('重试'),
),
],
),
)
: RefreshIndicator(
onRefresh: _loadMovies,
child: ListView(
children: [
// 类型选择器
_buildGenreSelector(),
const SizedBox(height: 16),
// 推荐电影
_buildMovieList('推荐电影', _recommendedMovies),
const SizedBox(height: 16),
// 热门电影
_buildMovieList('热门电影', _popularMovies),
const SizedBox(height: 16),
// 最新电影
_buildMovieList('最新电影', _latestMovies),
const SizedBox(height: 32),
],
),
),
);
}
}
3.3 电影详情页面
实现电影详情页面,用于显示电影的详细信息和评论:
/// 电影详情页面
class MovieDetailPage extends StatefulWidget {
/// 电影ID
final int movieId;
/// 构造函数
const MovieDetailPage({super.key, required this.movieId});
State<MovieDetailPage> createState() => _MovieDetailPageState();
}
/// 电影详情页面状态类
class _MovieDetailPageState extends State<MovieDetailPage> {
/// 电影信息
Movie? _movie;
/// 电影评论列表
List<MovieReview> _reviews = [];
/// 加载状态
bool _isLoading = true;
/// 错误信息
String? _errorMessage;
/// 评论加载状态
bool _isLoadingReviews = false;
/// 评论错误信息
String? _reviewErrorMessage;
/// 评论内容控制器
final TextEditingController _reviewController = TextEditingController();
/// 评分控制器
double _rating = 5.0;
/// 评论提交状态
bool _isSubmittingReview = false;
void initState() {
super.initState();
_loadMovieDetails();
_loadMovieReviews();
}
void dispose() {
_reviewController.dispose();
super.dispose();
}
/// 加载电影详情
Future<void> _loadMovieDetails() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final movie = await movieService.getMovieDetails(widget.movieId);
if (movie == null) {
setState(() {
_errorMessage = '电影不存在';
_isLoading = false;
});
return;
}
setState(() {
_movie = movie;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = '加载电影详情失败,请重试';
_isLoading = false;
});
}
}
/// 加载电影评论
Future<void> _loadMovieReviews() async {
setState(() {
_isLoadingReviews = true;
_reviewErrorMessage = null;
});
try {
final reviews = await movieService.getMovieReviews(widget.movieId);
setState(() {
_reviews = reviews;
_isLoadingReviews = false;
});
} catch (e) {
setState(() {
_reviewErrorMessage = '加载评论失败,请重试';
_isLoadingReviews = false;
});
}
}
/// 提交评论
Future<void> _submitReview() async {
if (_reviewController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入评论内容')),
);
return;
}
setState(() {
_isSubmittingReview = true;
});
try {
await movieService.submitMovieReview(
widget.movieId,
_reviewController.text,
_rating,
'用户', // 这里可以替换为实际的用户名
);
// 重新加载评论
await _loadMovieReviews();
// 清空输入
_reviewController.clear();
_rating = 5.0;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('评论提交成功')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('评论提交失败,请重试')),
);
} finally {
setState(() {
_isSubmittingReview = false;
});
}
}
/// 构建电影信息部分
Widget _buildMovieInfo() {
if (_movie == null) return const SizedBox();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 电影海报和基本信息
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 电影海报
Container(
width: 120,
height: 180,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage(_movie!.posterPath),
fit: BoxFit.cover,
),
),
),
// 基本信息
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 电影标题
Text(
_movie!.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// 评分
Row(
children: [
const Icon(
Icons.star,
size: 16,
color: Colors.amber,
),
const SizedBox(width: 4),
Text(
'${_movie!.voteAverage}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
// 上映日期
Row(
children: [
const Icon(
Icons.calendar_today,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
_movie!.releaseDate,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 8),
// 电影类型
Wrap(
spacing: 8,
runSpacing: 4,
children: _movie!.genres.map((genre) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.grey[200],
),
child: Text(
genre,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
);
}).toList(),
),
const SizedBox(height: 8),
// 电影时长
Row(
children: [
const Icon(
Icons.access_time,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
'${_movie!.runtime} 分钟',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
],
),
),
),
],
),
const SizedBox(height: 16),
// 电影简介
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'简介',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_movie!.overview,
style: const TextStyle(
fontSize: 14,
height: 1.5,
),
),
],
),
),
const SizedBox(height: 24),
// 电影详情
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'电影详情',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Table(
columnWidths: const {
0: FlexColumnWidth(1),
1: FlexColumnWidth(2),
},
children: [
TableRow(
children: [
const Text('语言', style: TextStyle(color: Colors.grey)),
Text(_movie!.originalLanguage),
],
),
TableRow(
children: [
const Text('状态', style: TextStyle(color: Colors.grey)),
Text(_movie!.status),
],
),
TableRow(
children: [
const Text('预算', style: TextStyle(color: Colors.grey)),
Text('\$${(_movie!.budget / 1000000).toStringAsFixed(1)}M'),
],
),
TableRow(
children: [
const Text('票房', style: TextStyle(color: Colors.grey)),
Text('\$${(_movie!.revenue / 1000000).toStringAsFixed(1)}M'),
],
),
],
),
],
),
),
const SizedBox(height: 24),
],
);
}
/// 构建评论部分
Widget _buildReviews() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 评论标题
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'评论',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
// 评论输入框
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('写下你的评论'),
const SizedBox(height: 8),
// 评分组件
Row(
children: [
const Text('评分:'),
Expanded(
child: Slider(
value: _rating,
min: 1.0,
max: 5.0,
divisions: 4,
label: _rating.toString(),
onChanged: (value) {
setState(() {
_rating = value;
});
},
),
),
Text('${_rating.toStringAsFixed(1)}'),
],
),
const SizedBox(height: 8),
// 评论输入框
TextField(
controller: _reviewController,
maxLines: 3,
decoration: const InputDecoration(
hintText: '请输入评论内容',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
// 提交按钮
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: _isSubmittingReview ? null : _submitReview,
child: _isSubmittingReview
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('提交评论'),
),
),
],
),
),
),
),
const SizedBox(height: 16),
// 评论列表
_isLoadingReviews
? const Center(child: CircularProgressIndicator())
: _reviewErrorMessage != null
? Center(
child: Column(
children: [
Text(_reviewErrorMessage!),
ElevatedButton(
onPressed: _loadMovieReviews,
child: const Text('重试'),
),
],
),
)
: _reviews.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('暂无评论,快来写下第一条评论吧!'),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _reviews.length,
itemBuilder: (context, index) {
final review = _reviews[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
review.author,
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
Row(
children: [
const Icon(
Icons.star,
size: 14,
color: Colors.amber,
),
const SizedBox(width: 4),
Text('${review.rating}'),
],
),
],
),
const SizedBox(height: 8),
Text(review.content),
const SizedBox(height: 8),
Text(
review.createdAt.substring(0, 10),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
);
},
),
const SizedBox(height: 32),
],
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('电影详情'),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_errorMessage!),
ElevatedButton(
onPressed: _loadMovieDetails,
child: const Text('重试'),
),
],
),
)
: RefreshIndicator(
onRefresh: () async {
await Future.wait([
_loadMovieDetails(),
_loadMovieReviews(),
]);
},
child: ListView(
children: [
_buildMovieInfo(),
_buildReviews(),
],
),
),
);
}
}
3.4 个人中心页面
实现个人中心页面,用于显示用户信息和管理个人收藏、观看历史:
/// 个人中心页面
class MovieProfilePage extends StatefulWidget {
/// 构造函数
const MovieProfilePage({super.key});
State<MovieProfilePage> createState() => _MovieProfilePageState();
}
/// 个人中心页面状态类
class _MovieProfilePageState extends State<MovieProfilePage> {
/// 用户信息
final User _user = User(
id: 1,
username: '电影爱好者',
email: 'movie@example.com',
avatarPath: 'assets/images/avatar.jpg',
createdAt: '2023-01-01',
);
/// 模拟收藏的电影
final List<Movie> _favoriteMovies = [
Movie(
id: 1,
title: '复仇者联盟4:终局之战',
posterPath: 'assets/images/avengers_endgame.jpg',
voteAverage: 8.4,
overview: '在《复仇者联盟3:无限战争》的毁灭性事件之后,宇宙由于灭霸的行动而变得满目疮痍。',
releaseDate: '2019-04-24',
genres: ['动作', '冒险', '科幻'],
runtime: 181,
budget: 356000000,
revenue: 2797800564,
originalLanguage: '英语',
status: '已上映',
),
Movie(
id: 10,
title: '星际穿越',
posterPath: 'assets/images/interstellar.jpg',
voteAverage: 9.3,
overview: '在不远的未来,地球上的资源几乎耗尽,人类面临着灭绝的危险。',
releaseDate: '2014-11-07',
genres: ['科幻', '冒险', '剧情'],
runtime: 169,
budget: 1650000000,
revenue: 7000000000,
originalLanguage: '英语',
status: '已上映',
),
];
/// 模拟观看历史
final List<Movie> _watchHistory = [
Movie(
id: 2,
title: '流浪地球2',
posterPath: 'assets/images/wandering_earth_2.jpg',
voteAverage: 8.3,
overview: '太阳即将膨胀为红巨星,为了生存,人类在地球上建造了上万台巨型发动机。',
releaseDate: '2023-01-22',
genres: ['科幻', '冒险', '灾难'],
runtime: 173,
budget: 600000000,
revenue: 4020000000,
originalLanguage: '中文',
status: '已上映',
),
Movie(
id: 3,
title: '独行月球',
posterPath: 'assets/images/moon_man.jpg',
voteAverage: 7.4,
overview: '月球探测项目失败,维修工独孤月被遗留在月球上,成为"宇宙最后的人类"。',
releaseDate: '2022-07-29',
genres: ['喜剧', '科幻', '冒险'],
runtime: 122,
budget: 300000000,
revenue: 3100000000,
originalLanguage: '中文',
status: '已上映',
),
];
/// 构建用户信息卡片
Widget _buildUserInfoCard() {
return Card(
margin: const EdgeInsets.all(16),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 用户头像
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
image: DecorationImage(
image: AssetImage(_user.avatarPath),
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 16),
// 用户信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_user.username,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_user.email,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
'注册时间:${_user.createdAt}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
// 编辑按钮
IconButton(
onPressed: () {
// 编辑个人信息
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('编辑个人信息功能开发中')),
);
},
icon: const Icon(Icons.edit),
),
],
),
),
);
}
/// 构建功能菜单
Widget _buildFeatureMenu() {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
child: Column(
children: [
_buildMenuItem(
icon: Icons.favorite,
title: '我的收藏',
onTap: () {
// 跳转到收藏页面
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('收藏页面功能开发中')),
);
},
),
const Divider(height: 1),
_buildMenuItem(
icon: Icons.history,
title: '观看历史',
onTap: () {
// 跳转到观看历史页面
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('观看历史页面功能开发中')),
);
},
),
const Divider(height: 1),
_buildMenuItem(
icon: Icons.settings,
title: '设置',
onTap: () {
// 跳转到设置页面
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('设置页面功能开发中')),
);
},
),
const Divider(height: 1),
_buildMenuItem(
icon: Icons.help,
title: '帮助与反馈',
onTap: () {
// 跳转到帮助与反馈页面
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('帮助与反馈页面功能开发中')),
);
},
),
const Divider(height: 1),
_buildMenuItem(
icon: Icons.info,
title: '关于',
onTap: () {
// 跳转到关于页面
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('关于'),
content: const Text('电影推荐及影评APP v1.0.0\n\n一个基于Flutter框架开发的跨平台电影推荐及影评应用。'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('确定'),
),
],
);
},
);
},
),
],
),
);
}
/// 构建菜单项
Widget _buildMenuItem({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
/// 构建收藏和历史电影列表
Widget _buildMovieList(String title, List<Movie> movies) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(
height: 140,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: movies.length,
itemBuilder: (context, index) {
final movie = movies[index];
return Container(
margin: const EdgeInsets.only(right: 12),
width: 100,
child: Column(
children: [
Container(
height: 140,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage(movie.posterPath),
fit: BoxFit.cover,
),
),
),
],
),
);
},
),
),
],
);
}
/// 构建退出登录按钮
Widget _buildLogoutButton() {
return Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
// 退出登录
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('退出登录成功')),
);
},
child: const Text('确定'),
),
],
);
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: const Size(double.infinity, 48),
),
child: const Text('退出登录'),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('个人中心'),
centerTitle: true,
),
body: ListView(
children: [
// 用户信息卡片
_buildUserInfoCard(),
// 功能菜单
_buildFeatureMenu(),
// 收藏的电影
_buildMovieList('我的收藏', _favoriteMovies),
// 观看历史
_buildMovieList('观看历史', _watchHistory),
// 退出登录按钮
_buildLogoutButton(),
const SizedBox(height: 32),
],
),
);
}
}
3.5 主页面
实现应用的主页面,用于整合电影列表和个人中心页面:
/// 电影推荐及影评APP主页面
class MovieMainPage extends StatefulWidget {
/// 构造函数
const MovieMainPage({super.key});
State<MovieMainPage> createState() => _MovieMainPageState();
}
/// 电影主页面状态类
class _MovieMainPageState extends State<MovieMainPage> {
/// 当前选中的页面索引
int _currentIndex = 0;
/// 页面控制器
final PageController _pageController = PageController();
void dispose() {
_pageController.dispose();
super.dispose();
}
/// 处理底部导航栏点击
void _onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
_pageController.jumpToPage(index);
}
/// 处理页面变化
void _onPageChanged(int index) {
setState(() {
_currentIndex = index;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
onPageChanged: _onPageChanged,
children: const [
// 电影列表页面
MovieListPage(),
// 个人中心页面
MovieProfilePage(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: _onTabTapped,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.movie),
label: '电影',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
type: BottomNavigationBarType.fixed,
),
);
}
}
3.6 应用入口
配置应用的入口文件,设置应用的主题和路由:
/// 应用入口文件
import 'package:flutter/material.dart';
import 'package:flutter_shili/pages/movie_main_page.dart';
void main() {
runApp(const MyApp());
}
/// 应用主类
class MyApp extends StatelessWidget {
/// 构造函数
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '电影推荐及影评',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MovieMainPage(),
debugShowCheckedModeBanner: false,
);
}
}
4. 核心功能实现流程图
4.1 电影列表加载流程
4.2 电影详情加载流程
5. 核心功能实现代码展示
5.1 电影服务核心方法
/// 获取推荐电影列表
Future<List<Movie>> getRecommendedMovies({int page = 1, int limit = 20}) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 500));
// 使用模拟数据
final moviesData = MockData.movies;
// 计算起始和结束索引
final startIndex = (page - 1) * limit;
final endIndex = startIndex + limit;
// 截取数据
final paginatedMovies = moviesData.sublist(
startIndex,
endIndex > moviesData.length ? moviesData.length : endIndex,
);
return paginatedMovies;
}
/// 获取电影详情
Future<Movie?> getMovieDetails(int movieId) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 300));
// 使用模拟数据
final moviesData = MockData.movies;
// 查找电影
final movie = moviesData.firstWhere(
(movie) => movie.id == movieId,
orElse: () => Movie(
id: 0,
title: '',
posterPath: '',
voteAverage: 0,
overview: '',
releaseDate: '',
genres: [],
runtime: 0,
budget: 0,
revenue: 0,
originalLanguage: '',
status: '',
),
);
return movie.id > 0 ? movie : null;
}
/// 提交电影评论
Future<MovieReview> submitMovieReview(int movieId, String content, double rating, String author) async {
// 模拟网络请求延迟
await Future.delayed(const Duration(milliseconds: 500));
// 创建新评论
final newReview = MovieReview(
id: DateTime.now().millisecondsSinceEpoch,
content: content,
author: author,
rating: rating,
createdAt: DateTime.now().toIso8601String(),
movieId: movieId,
);
// 模拟添加到数据库
MockData.reviews.add(newReview);
return newReview;
}
5.2 电影列表页面核心方法
/// 加载电影数据
Future<void> _loadMovies() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// 并行加载数据
final results = await Future.wait([
movieService.getRecommendedMovies(),
movieService.getPopularMovies(),
movieService.getLatestMovies(),
movieService.getMovieGenres(),
]);
setState(() {
_recommendedMovies = results[0] as List<Movie>;
_popularMovies = results[1] as List<Movie>;
_latestMovies = results[2] as List<Movie>;
_genres = results[3] as List<String>;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = '加载电影数据失败,请重试';
_isLoading = false;
});
}
}
/// 构建电影卡片
Widget _buildMovieCard(Movie movie) {
return SizedBox(
width: 140,
height: 224,
child: GestureDetector(
onTap: () => _navigateToMovieDetail(movie.id),
child: Container(
padding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 电影海报
SizedBox(
width: 140,
height: 190,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
image: DecorationImage(
image: AssetImage(movie.posterPath),
fit: BoxFit.cover,
),
),
),
),
// 电影标题
SizedBox(
width: 140,
height: 16,
child: Text(
movie.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
// 电影评分
SizedBox(
width: 140,
height: 16,
child: Row(
children: [
const Icon(
Icons.star,
size: 10,
color: Colors.amber,
),
const SizedBox(width: 2),
Text(
'${movie.voteAverage}',
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
),
],
),
),
],
),
),
),
);
}
5.3 电影详情页面核心方法
/// 加载电影详情
Future<void> _loadMovieDetails() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final movie = await movieService.getMovieDetails(widget.movieId);
if (movie == null) {
setState(() {
_errorMessage = '电影不存在';
_isLoading = false;
});
return;
}
setState(() {
_movie = movie;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = '加载电影详情失败,请重试';
_isLoading = false;
});
}
}
/// 提交评论
Future<void> _submitReview() async {
if (_reviewController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入评论内容')),
);
return;
}
setState(() {
_isSubmittingReview = true;
});
try {
await movieService.submitMovieReview(
widget.movieId,
_reviewController.text,
_rating,
'用户', // 这里可以替换为实际的用户名
);
// 重新加载评论
await _loadMovieReviews();
// 清空输入
_reviewController.clear();
_rating = 5.0;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('评论提交成功')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('评论提交失败,请重试')),
);
} finally {
setState(() {
_isSubmittingReview = false;
});
}
}
总结
本文详细介绍了如何使用Flutter框架开发一款电影推荐及影评APP,并实现跨平台运行,特别是在鸿蒙系统上的适配。通过本文的学习,读者应该已经了解了Flutter框架的核心特性、跨平台开发的实践经验,以及如何构建一个功能完整的电影推荐APP。
主要成果
-
成功构建了电影推荐及影评APP:实现了电影列表展示、电影详情查看、影评提交和查看、个人中心等核心功能。
-
实现了跨平台兼容:应用可以在Android、iOS、鸿蒙等多个平台运行,体现了Flutter"一次编写,处处运行"的特性。
-
优化了用户体验:通过并行加载数据、错误处理、下拉刷新等功能,提升了应用的用户体验。
-
修复了图片显示问题:将网络图片替换为本地图片,提高了应用的稳定性和性能。
技术亮点
-
模块化设计:采用了清晰的目录结构和模块化设计,使代码更易于维护和扩展。
-
并行加载数据:使用Future.wait并行加载多个数据请求,提高了数据加载效率。
-
响应式布局:使用了Flutter的响应式布局,确保应用在不同屏幕尺寸上都能正常显示。
-
状态管理:使用了Flutter的setState进行状态管理,代码简洁明了。
-
错误处理:实现了完善的错误处理机制,提高了应用的稳定性。
未来展望
-
接入真实API:目前使用的是模拟数据,未来可以接入真实的电影API,获取实时的电影数据。
-
实现用户认证:目前的用户认证功能是模拟的,未来可以实现真实的用户注册、登录和认证功能。
-
添加更多功能:可以添加更多功能,如电影预告片播放、电影票购买、电影周边商品推荐等。
-
优化性能:可以进一步优化应用的性能,如使用缓存、懒加载等技术。
-
适配更多平台:可以进一步适配更多平台,如Web、桌面端等。
通过本文的学习,读者应该已经掌握了Flutter框架跨平台开发的基本技能,可以尝试开发更多功能丰富的跨平台应用。Flutter作为一款强大的跨平台开发框架,有着广阔的应用前景,相信在未来会成为移动应用开发的主流选择。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)