Flutter集成Provider后电影列表不显示、搜索时报results为null的问题排查
问题根因定位
所有异常均为代码逻辑疏漏导致,和Provider的配置、网络请求能力无关,核心问题共4处:
- 状态类拉取数据后未赋值内部私有字段:
MovieInfo中的fetchData方法解析完接口返回值后,仅创建了局部变量存储结果、直接返回,全程未给类内的_result字段赋值,导致对外暴露的result属性永远返回null。 - 变量作用域遮蔽导致空引用:
_HomeState中声明的成员变量movieInfo从未被实际赋值,build方法里重新以同名类型声明了局部变量MovieInfo movieInfo = Provider.of...,把外层的成员变量完全遮蔽;后续在_buildShow这类类内方法中读到的movieInfo实际是未初始化的null,访问属性自然触发空指针报错。 - 空值校验缺失:搜索框的
onChanged回调、列表构建逻辑中,直接对可能为null的result读取.results属性,一旦接口还没返回结果就触发输入搜索,直接抛空异常。 - 资源未正确释放:
MovieInfo中持有的TextEditingController没有随组件生命周期执行dispose,长期运行会引发内存泄漏警告。
分步修复方案
1. 修正状态类的赋值逻辑
删除fetchData中的局部变量,将解析结果直接赋值给私有字段,同时补上资源释放逻辑:
class MovieInfo extends ChangeNotifier { Movies _result; Movies get result => _result; final myController = TextEditingController(); final List<Results> filteredItems = []; Future<void> fetchData() async { var url = Uri.parse( 'https://api.themoviedb.org/3/movie/upcoming?api_key=你的合法密钥&language=ko-KR&page=1'); var response = await http.get(url); // 直接将解析结果赋值给内部私有字段 _result = Movies.fromJson(json.decode(response.body)); notifyListeners(); } // 重写dispose释放控制器资源 @override void dispose() { myController.dispose(); super.dispose(); } }
2. 解决变量遮蔽问题
删掉build方法里的同名局部变量,将Provider获取到的状态实例赋值给类的成员变量,保证类内所有方法都能拿到正确的状态引用:
class _HomeState extends State<Home> { late MovieInfo movieInfo; @override void initState() { super.initState(); // listen: false的写法符合Provider规范,无需迁移到didChangeDependencies Provider.of<MovieInfo>(context, listen: false).fetchData(); } @override Widget build(BuildContext context) { // 给成员变量赋值,不要重新声明同类型的局部变量 movieInfo = Provider.of<MovieInfo>(context); final result = movieInfo.result; return Scaffold( // 原有drawer、appBar逻辑保持不变 body: Scaffold( backgroundColor: Colors.blueGrey, body: Column( children: <Widget>[ TextField( controller: movieInfo.myController, onChanged: (text) { // 未加载完成时直接跳过搜索逻辑,避免空指针 if (result == null) return; setState(() { movieInfo.filteredItems.clear(); movieInfo.filteredItems.addAll(result.results .where((item) => item.title.contains(text))); }); }, // 原有输入框装饰逻辑保持不变 ), Expanded( child: GridView.count( crossAxisCount: 3, childAspectRatio: 2 / 3.5, // 加载中展示提示,替代原来的空列表 children: result == null ? [Center(child: CircularProgressIndicator())] : _buildShow(), ), ), ], ), ), ); } // _buildItem方法保持原有逻辑不变 Widget _buildItem(Results movies) { return InkWell( child: Card( color: Colors.blueGrey, margin: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Column( children: [ Image.network('https://image.tmdb.org/t/p/w500/${movies.posterPath}'), Text('${movies.title}', style: TextStyle( height: 1.5, fontSize: 15, fontWeight: FontWeight.bold)), ], ), ), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => DetailScreen(movie: movies), ), ); }, ); } // 给列表构建入口补空值校验 List<Widget> _buildShow() { if (movieInfo.result == null) return []; if (movieInfo.myController.text.isEmpty) { return movieInfo.result.results.map((movies) => _buildItem(movies)).toList(); } return movieInfo.filteredItems.map((e) => _buildItem(e)).toList(); } }
补充注意事项
- 原代码中TMDB接口的api_key为空,需要替换为合法的访问密钥,否则即使代码逻辑正确,也会拿到接口错误响应,无法解析出有效数据
- 原State类中声明的
Future<Movies> data;字段从未被使用,属于冗余代码,可以直接删除减少无效内存占用 - 列表加载初期展示加载指示器,比直接渲染空列表的用户体验更好,也能避免用户误以为页面加载失败
内容的提问来源于stack exchange,提问作者jichang brad Jung
相关产品推荐
相关产品推荐

