You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 15:47:33