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

Flutter电影APP对接TMDB API实现影片类型筛选功能咨询

Flutter电影APP按类型筛选功能实现方案

1. 调整Provider数据结构(核心解决原始数据源被修改的问题)

将原始数据源和筛选后展示用的数据源完全拆分,同时新增变量存储当前选中的类型ID列表,原始数据源仅在接口请求时更新,永远不会被筛选逻辑修改:

// 原始数据源,仅接口请求时更新,全程不参与筛选修改
List<nowPlaying.Result> _originalNowPlaying = [];
List<moviesModel.Result> _originalPopular = [];
List<moviesModel.Result> _originalTopRated = [];
List<moviesModel.Result> _originalUpcoming = [];

// 筛选后对外暴露的展示用数据源
List<nowPlaying.Result> _filteredNowPlaying = [];
List<moviesModel.Result> _filteredPopular = [];
List<moviesModel.Result> _filteredTopRated = [];
List<moviesModel.Result> _filteredUpcoming = [];

// 存储当前选中的类型ID集合
Set<int> _selectedGenreIds = {};
Set<int> get selectedGenreIds => _selectedGenreIds;

// 页面直接读取筛选后的数据源即可
List<nowPlaying.Result> get getNowPlaying => [..._filteredNowPlaying];
List<moviesModel.Result> get getPopular => [..._filteredPopular];
List<moviesModel.Result> get getTopRated => [..._filteredTopRated];
List<moviesModel.Result> get getUpcoming => [..._filteredUpcoming];

修改原有接口请求逻辑,请求成功后仅更新原始数据源,再触发筛选更新展示数据:

Future<void> getNowPlayingMovies({required String type}) async {
  try {
    nowPlaying.NowPlayingModel? nowPlayingModel =
        await GetMoviesTvShows.nowPlayingMovies(type: type, page: 1);
    if (nowPlayingModel != null) {
      List<nowPlaying.Result> temp = [];
      nowPlayingModel.results.forEach((result) {
        if (result.title.isNotEmpty && result.overview.isNotEmpty) {
          temp.add(result);
        }
      });
      // 仅更新原始数据源
      _originalNowPlaying = temp;
      // 触发筛选更新展示数据
      _applyFilter();
      notifyListeners();
    }
  } catch (e, s) {
    print(e);
    print(s);
  }
}
// 热门、TOP榜、即将上映的接口请求方法同理,仅修改对应原始数据源后调用_applyFilter即可

2. 重写筛选逻辑,支持多类型筛选

新增通用筛选私有方法,每次选中/取消类型时自动调用,重新从原始数据源过滤得到展示数据:

// 通用筛选方法
void _applyFilter() {
  // 无选中类型时,展示数据直接等于原始全量数据
  if (_selectedGenreIds.isEmpty) {
    _filteredNowPlaying = List.from(_originalNowPlaying);
    _filteredPopular = List.from(_originalPopular);
    _filteredTopRated = List.from(_originalTopRated);
    _filteredUpcoming = List.from(_originalUpcoming);
    notifyListeners();
    return;
  }

  // 有选中类型的场景:
  // 当前逻辑为「满足任意一个选中类型即可展示」,如果需要「同时满足所有选中类型才展示」,把下方的any改为every即可
  _filteredNowPlaying = _originalNowPlaying.where((movie) => 
    movie.genreIds.any((genreId) => _selectedGenreIds.contains(genreId))
  ).toList();

  _filteredPopular = _originalPopular.where((movie) => 
    movie.genreIds.any((genreId) => _selectedGenreIds.contains(genreId))
  ).toList();

  _filteredTopRated = _originalTopRated.where((movie) => 
    movie.genreIds.any((genreId) => _selectedGenreIds.contains(genreId))
  ).toList();

  _filteredUpcoming = _originalUpcoming.where((movie) => 
    movie.genreIds.any((genreId) => _selectedGenreIds.contains(genreId))
  ).toList();

  notifyListeners();
}

// 对外暴露的类型选中/取消切换方法
void toggleGenre(int genreId) {
  if (_selectedGenreIds.contains(genreId)) {
    _selectedGenreIds.remove(genreId);
  } else {
    _selectedGenreIds.add(genreId);
  }
  // 切换后触发筛选
  _applyFilter();
}

3. 修改Genres类型选择组件

移除组件内部的选中状态存储逻辑,改为和Provider的选中集合联动:

class Genres extends StatelessWidget {
  // 类型选中回调,向外传递选中的类型ID
  final Function(int genreId) onGenreTap;
  // 外部传入当前选中的类型ID集合,控制选中样式
  final Set<int> selectedGenreIds;

  const Genres({
    Key? key,
    required this.onGenreTap,
    required this.selectedGenreIds,
  }) : super(key: key);

  final genres = const [
    [28, "Action"],
    [12, "Adventure"],
    [16, "Animation"],
    [35, "Comedy"],
    [80, "Crime"],
    [99, "Documentary"],
    [18, "Drama"],
    [10751, "Family"],
    [14, "Fantasy"],
    [36, "History"],
    [27, "Horror"],
    [10402, "Music"],
    [9648, "Mystery"],
    [10749, "Romance"],
    [878, "Science Fiction"],
    [10770, "TV Movie"],
    [53, "Thriller"],
    [10752, "War"],
    [37, "Western"]
  ];

  @override
  Widget build(BuildContext context) {
    SizeConfig().init(context);

    return Container(
      margin: const EdgeInsets.symmetric(vertical: kDefaultPadding / 2),
      height: SizeConfig.screenHeight * 0.05,
      child: ListView.builder(
          scrollDirection: Axis.horizontal,
          itemCount: genres.length,
          itemBuilder: (ctx, index) {
            final genreId = genres[index][0] as int;
            final isSelected = selectedGenreIds.contains(genreId);
            return InkWell(
              onTap: () => onGenreTap(genreId),
              child: Container(
                alignment: Alignment.center,
                margin: const EdgeInsets.only(left: kDefaultPadding / 2),
                padding: const EdgeInsets.symmetric(
                    horizontal: kDefaultPadding,
                    vertical: kDefaultPadding / 4),
                decoration: BoxDecoration(
                    color: isSelected ? Colors.amber : Colors.transparent,
                    border: Border.all(
                        color: isSelected
                            ? Colors.transparent
                            : Colors.black26),
                    borderRadius: BorderRadius.circular(20.0)),
                child: Text(
                  genres[index].last.toString(),
                  style: TextStyle(
                      color: isSelected
                          ? Colors.white
                          : kTextColor.withOpacity(0.8),
                      fontSize: SizeConfig.safeBlockHorizontal * 4),
                ),
              ),
            );
          }),
    );
  }
}

在使用Genres组件的页面绑定Provider逻辑即可:

Consumer<你的Provider类名>(
  builder: (context, provider, child) {
    return Genres(
      selectedGenreIds: provider.selectedGenreIds,
      onGenreTap: (genreId) {
        provider.toggleGenre(genreId);
      },
    );
  },
)

多类型筛选逻辑说明

默认实现的是「单类型/多类型任意匹配」逻辑,即只要影片属于任意一个选中的类型就会展示;如果需要实现「多类型交集匹配」,即影片必须同时属于所有选中的类型才展示,仅需要将_applyFilter方法中的any方法替换为every方法即可。

内容的提问来源于stack exchange,提问作者Shubham Vimal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 06:15:03