基于Riverpod调用TMDB API实现热门电影多语言内容切换展示
Flutter电影应用实现:TMDB数据获取与双语切换
问题背景
正在开发Flutter项目,通过TheMovieDB(TMDB)API获取热门电影数据,同时调用其翻译API将电影名称和概述转为巴西葡萄牙语。当前核心需求:同时存储原始内容与翻译内容,并支持一键切换展示。
当前实现概况
- 已定义
PopularMovie类存储电影基础数据 - 使用Riverpod管理状态并获取热门电影列表
- 已定义
MovieTranslation类存储巴西葡萄牙语翻译数据
现有代码实现
PopularMovie 类
import 'dart:convert'; import '../utils/genre_helper.dart'; class PopularMovie { int id; String originalTitle; String overview; double score; String posterPath; String releaseDate; List<int> genreIds; //MovieTranslation translations; PopularMovie({required this.id, required this.originalTitle, required this.overview, required this.score, required this.posterPath, required this.releaseDate, required this.genreIds, //required this.translations }); List<String> getGenreNames(List<int> genreIds) { return GenreHelper.getGenreNames(genreIds); } factory PopularMovie.fromJson(String str) => PopularMovie.fromMap(json.decode(str)); factory PopularMovie.fromMap(Map<String, dynamic> json) { final List<int> genreIds = List<int>.from(json['genre_ids'] ?? []); return PopularMovie( id: json["id"], originalTitle: json["original_title"], overview: json["overview"], score: json["vote_average"]!= null ? json["vote_average"].toDouble() : 0.0, posterPath: json["poster_path"] ?? "", releaseDate: json["release_date"] != null ? json["release_date"].toString() : "", genreIds: genreIds, //translations: brTranslation ); } }
MovieTranslation 类
import 'dart:convert'; class MovieTranslation { final String iso_3166_1; final String translatedName; final String translatedOverview; MovieTranslation({ required this.iso_3166_1, required this.translatedName, required this.translatedOverview }); factory MovieTranslation.fromJson(String str) => MovieTranslation.fromMap(json.decode(str)); factory MovieTranslation.fromMap(Map<String, dynamic> json) { return MovieTranslation( iso_3166_1: json["iso_3166_1"], translatedName: json["data"]["title"], translatedOverview: json["data"]["overview"] ); } }
MovieApiRepository 类
import 'dart:convert'; import 'package:traux_tmdb/data/movie_repository.dart'; import 'package:traux_tmdb/models/movie_translation.dart'; import 'package:traux_tmdb/models/popular_movie.dart'; import 'package:http/http.dart' as http; import '../utils/constants.dart'; class MovieApiRepository implements MovieRepository { final String _popularMovieUrl = 'https://api.themoviedb.org/3/movie/popular?api_key=$apiKey'; @override Future<List<PopularMovie>> fetchPopularMovies() async { final response = await http.get(Uri.parse(_popularMovieUrl)); if(response.statusCode == 200) { final jsonData = json.decode(response.body); final List<PopularMovie> popularMovies = (jsonData['results'] as List) .map((data) => PopularMovie.fromMap(data)) .toList(); return popularMovies; } else{ throw Exception("Erro ao buscar filmes populares"); } } @override Future<MovieTranslation> fetchTranslations(int movieId) async { final String translationsUrl = 'https://api.themoviedb.org/3/movie/$movieId/translations?api_key=$apiKey'; final response = await http.get(Uri.parse(translationsUrl)); if(response.statusCode == 200) { final jsonData = json.decode(response.body); final translationData = (jsonData['translations'].firstWhere( (translation) => translation['iso_3166_1'] == 'BR', orElse: () => null )); return MovieTranslation.fromMap(translationData); } else{ throw Exception("Erro ao buscar tradução do filme"); } } }
MovieProvider
import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/popular_movie.dart'; import 'movie_repository_provider.dart'; final getPopularMoviesUseCaseProvider = FutureProvider<List<PopularMovie>>((ref) async { final repository = ref.read(movieRepositoryProvider); return repository.fetchPopularMovies(); });
HomeScreen 组件
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/popular_movie_provider.dart'; class Home extends ConsumerWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final popularMovies = ref.watch(getPopularMoviesUseCaseProvider); return Scaffold( body:popularMovies.when(data: (movies) => Column( children: [ Expanded( child: ListView( children: [ for(final movie in movies) ListTile( title: Text(movie.originalTitle), ) ], ), ) ], ) , loading: () => const Center( child: CircularProgressIndicator(), ), error: (err, stack) => Text("Error: $err") ), ); } }
分步实现指导
1. 完善数据模型,关联电影与翻译
修改PopularMovie类,添加可选的翻译属性,支持存储对应电影的巴西语翻译:
class PopularMovie { // 原有属性保留不变 int id; String originalTitle; String overview; double score; String posterPath; String releaseDate; List<int> genreIds; MovieTranslation? translation; // 新增:存储翻译数据 PopularMovie({ required this.id, required this.originalTitle, required this.overview, required this.score, required this.posterPath, required this.releaseDate, required this.genreIds, this.translation, // 新增可选参数 }); // 完善fromMap方法,translation初始为null factory PopularMovie.fromMap(Map<String, dynamic> json) { final List<int> genreIds = List<int>.from(json['genre_ids'] ?? []); return PopularMovie( id: json["id"], originalTitle: json["original_title"], overview: json["overview"], score: json["vote_average"]!= null ? json["vote_average"].toDouble() : 0.0, posterPath: json["poster_path"] ?? "", releaseDate: json["release_date"] != null ? json["release_date"].toString() : "", genreIds: genreIds, translation: null, // 初始无翻译,后续按需加载 ); } }
2. 新增全局语言切换状态
添加Riverpod的StateProvider,控制当前展示的语言模式:
// 全局语言切换状态:true=显示葡萄牙语,false=显示原始内容 final isShowingPortugueseProvider = StateProvider<bool>((ref) => false);
3. 用StateNotifier优化电影列表状态管理
原FutureProvider无法修改已加载的电影数据(比如添加翻译),改用StateNotifierProvider实现可变状态管理:
class MovieListNotifier extends StateNotifier<List<PopularMovie>> { final MovieRepository repository; MovieListNotifier(this.repository) : super([]); // 加载热门电影列表 Future<void> fetchPopularMovies() async { try { final movies = await repository.fetchPopularMovies(); state = movies; } catch (e) { throw Exception('获取热门电影失败:$e'); } } // 加载单部电影的翻译数据并更新列表 Future<void> loadTranslation(int movieId) async { try { final translation = await repository.fetchTranslations(movieId); if (translation == null) { throw Exception('该电影暂无巴西葡萄牙语翻译'); } // 创建新的电影列表,更新目标电影的翻译数据 state = state.map((movie) { if (movie.id == movieId) { return PopularMovie( id: movie.id, originalTitle: movie.originalTitle, overview: movie.overview, score: movie.score, posterPath: movie.posterPath, releaseDate: movie.releaseDate, genreIds: movie.genreIds, translation: translation, ); } return movie; }).toList(); } catch (e) { throw Exception(e.toString()); } } } // 注册StateNotifierProvider final movieListNotifierProvider = StateNotifierProvider<MovieListNotifier, List<PopularMovie>>((ref) { final repo = ref.read(movieRepositoryProvider); final notifier = MovieListNotifier(repo); // 初始化时自动加载热门电影 notifier.fetchPopularMovies(); return notifier; });
4. 优化翻译API的异常处理
修改fetchTranslations方法,处理无翻译数据的情况:
@override Future<MovieTranslation?> fetchTranslations(int movieId) async { final String translationsUrl = 'https://api.themoviedb.org/3/movie/$movieId/translations?api_key=$apiKey'; final response = await http.get(Uri.parse(translationsUrl)); if(response.statusCode == 200) { final jsonData = json.decode(response.body); final translationData = jsonData['translations'].firstWhere( (translation) => translation['iso_3166_1'] == 'BR', orElse: () => null ); if (translationData == null) return null; // 无翻译时返回null return MovieTranslation.fromMap(translationData); } else{ throw Exception("获取电影翻译失败"); } }
5. 修改HomeScreen实现双语切换与按需加载
更新页面布局,添加语言切换按钮,实现内容的动态切换与翻译按需加载:
class Home extends ConsumerWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final movies = ref.watch(movieListNotifierProvider); final isShowingPortuguese = ref.watch(isShowingPortugueseProvider); return Scaffold( appBar: AppBar( title: const Text('热门电影'), actions: [ // 语言切换按钮 IconButton( icon: Icon(isShowingPortuguese ? Icons.g_translate : Icons.language), onPressed: () { ref.read(isShowingPortugueseProvider.notifier).state = !isShowingPortuguese; }, ) ], ), body: movies.isEmpty ? const Center(child: CircularProgressIndicator()) : ListView.builder( itemCount: movies.length, itemBuilder: (context, index) { final movie = movies[index]; return ListTile( title: Text( isShowingPortuguese && movie.translation != null ? movie.translation!.translatedName : movie.originalTitle ), subtitle: Text( isShowingPortuguese && movie.translation != null ? movie.translation!.translatedOverview : movie.overview ), // 点击加载翻译(未加载时) onTap: () async { if (movie.translation == null) { try { await ref.read(movieListNotifierProvider.notifier).loadTranslation(movie.id); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())) ); } } }, ); }, ), ); } }
内容的提问来源于stack exchange,提问作者Leonardo Maito
相关产品推荐
相关产品推荐

