skip to Main Content

I’m working on a Flutter application where I need to fetch and display a list of movies based on user input in a search field. The TextEditingController should trigger an API call to fetch movie data and update the ListView with the results. However, the list remains empty even after the API call is made.

Here’s my code:

import 'package:flutter/material.dart';
// Assume these classes are defined elsewhere
import 'api_call.dart';
import 'constants.dart';
import 'movie_model.dart';
import 'search_tile.dart';

class SearchMovies extends StatefulWidget {
  const SearchMovies({super.key});

  @override
  State<SearchMovies> createState() => _SearchMoviesState();
}

class _SearchMoviesState extends State<SearchMovies> {
  String text = '';
  List<MovieModel> searchMoviesList = [];
  final TextEditingController _searchController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _searchController.addListener(_onTextChanged);
  }

  @override
  void dispose() {
    _searchController.removeListener(_onTextChanged);
    _searchController.dispose();
    super.dispose();
  }

  void _onTextChanged() {
    fetchMovies(_searchController.text);
  }

  Future<void> fetchMovies(String query) async {
    if (query.isEmpty) {
      setState(() {
        searchMoviesList.clear();
      });
      return;
    }

    try {
      print('Fetching movies for query: $query');
      var movieResponse = await ApiCall(Constants.search + query).getData();
      print('API response: $movieResponse');
      setState(() {
        searchMoviesList.clear();
        List<dynamic> movieResults = movieResponse['results'];
        for (var element in movieResults) {
          searchMoviesList.add(MovieModel.fromJson(element));
        }
        print('Updated searchMoviesList: $searchMoviesList');
      });
    } catch (error) {
      print('Error fetching movies: $error');
      setState(() {
        searchMoviesList.clear();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Column(
          children: [
            TextFormField(
              controller: _searchController,
              onChanged: (value) {
                setState(() {
                  text = value;
                });
              },
              decoration: const InputDecoration(
                hintText: 'Search for movies',
                focusedBorder: UnderlineInputBorder(
                  borderSide: BorderSide(color: Colors.red),
                ),
              ),
            ),
          ],
        ),
      ),
      body: ListView.builder(
        itemCount: searchMoviesList.length,
        itemBuilder: (BuildContext context, int index) {
          return SearchTile(searchMoviesList: searchMoviesList, index: index);
        },
      ),
    );
  }
}



I have tried debugging the fetchmovies() methods. But 'movieResponse'-used to store API result, is giving empty list. I am expecting that after every character result list to be updated. API used:  static String search = 'https://api.themoviedb.org/3/search/movie?api_key=XXXXXXX&query=';

2

Answers


  1. Double-check the structure of the API response. The movieResponse[‘results’] part assumes that the API response has a results key. Print the whole movieResponse to confirm its structure.

    Login or Signup to reply.
  2. I do not know exact problem which causes unwanted behaviour. But I’m gonna provide you some considerations and maybe it will help resolve your issue.

    But I strongly believe that these suggestions will make your code more performant and maintainable in any case.

    1. Do not perform any heavy operations, beside actual updating state, inside setState method. Since on every state update widget rebuilds, this computations can cause laggy UI and junk frames. In your particular case, it may be worth it to move everything out of setState inside your fetchMovies method, create a new list of items (still outside) and then swap this list to searchMoviesList:
        setState(() {
          searchMoviesList = newList // this list obtained from API outside this setState
        })
    
    1. You have setState inside TextFormField’s onChanged callback, which updates text value of state. If you omitted usage of this value – it is OK, but if not – consider to remove this from your code. That how you avoid unnecessary rebuilds since text seems not to be used anywhere.

    2. Consider using some debounce technique to delay request until user, at least partially, is done with input. This technique will delay request and even abort it (actually, not even performing it) in case user still typing his query. 300ms is common value to start, but you can play with it.

    class _SearchMoviesState extends State<SearchMovies> {
    ...
      Timer? _debounce;
    ...
    
      @override
        void dispose() {
    ...
          _debounce?.cancel();
    ...
        }
      }
    
      void _onTextChanged() {
        if (_debounce?.isActive ?? false) _debounce?.cancel();
        _debounce = Timer(const Duration(milliseconds: 300), () {
          fetchMovies(_searchController.text);
        });
      }
    
    1. Consider a little optimisation for checking if results acquired from API are still relevant by comparing actual and last query from user input

    2. Optionally, you can use CancellableOperation instead plain Future. This operation can be cancelled and you can find it useful, since it may even reduce cost of your API usage, if it’s pay-per-request basis.

    So, after all that optimisations I will end up with this code:

    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:async/async.dart';
    
    // Assume these classes are defined elsewhere
    import 'api_call.dart';
    import 'constants.dart';
    import 'movie_model.dart';
    import 'search_tile.dart';
    
    class SearchMovies extends StatefulWidget {
      const SearchMovies({super.key});
    
      @override
      State<SearchMovies> createState() => _SearchMoviesState();
    }
    
    class _SearchMoviesState extends State<SearchMovies> {
      List<MovieModel> searchMoviesList = [];
      final TextEditingController _searchController = TextEditingController();
      Timer? _debounce;
      CancelableOperation<List<dynamic>>? _apiRequest;
      String _lastQuery = '';
    
      @override
      void initState() {
        super.initState();
        _searchController.addListener(_onTextChanged);
      }
    
      @override
      void dispose() {
        _searchController.removeListener(_onTextChanged);
        _searchController.dispose();
        _debounce?.cancel();
        _apiRequest?.cancel();
        super.dispose();
      }
    
      void _onTextChanged() {
        if (_debounce?.isActive ?? false) _debounce?.cancel();
        _debounce = Timer(const Duration(milliseconds: 300), () {
          fetchMovies(_searchController.text);
        });
      }
    
      Future<void> fetchMovies(String query) async {
        _lastQuery = query;
    
        if (query.isEmpty) {
          setState(() {
            searchMoviesList = [];
          });
          return;
        }
    
        try {
          print('Fetching movies for query: $query');
          _apiRequest?.cancel();
          _apiRequest = CancelableOperation.fromFuture(
              ApiCall(Constants.search + query).getData(),
              onCancel: () => print('Request cancelled'));
          final movieResponse = await _apiRequest?.value;
          final List<MovieModel> newList = movieResponse
              .map(
                (e) => MovieModel.fromJson(element),
              )
              .toList();
          print('API response: $movieResponse');
          if (_lastQuery == query) {
            setState(() {
              searchMoviesList = newList;
            });
          }
        } catch (error) {
          print('Error fetching movies: $error');
          setState(() {
            searchMoviesList = [];
          });
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Column(
              children: [
                TextFormField(
                  controller: _searchController,
                  decoration: const InputDecoration(
                    hintText: 'Search for movies',
                    focusedBorder: UnderlineInputBorder(
                      borderSide: BorderSide(color: Colors.red),
                    ),
                  ),
                ),
              ],
            ),
          ),
          body: ListView.builder(
            itemCount: searchMoviesList.length,
            itemBuilder: (BuildContext context, int index) {
              return SearchTile(searchMoviesList: searchMoviesList, index: index);
            },
          ),
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search