먹고 기도하고 코딩하라

[Flutter] API 통신 중 RangeError: invalid value only valid value is 0: 4 에러 해결하기 본문

앱/Flutter

[Flutter] API 통신 중 RangeError: invalid value only valid value is 0: 4 에러 해결하기

사과먹는사람 2022. 9. 24. 13:07
728x90
728x90

2줄 요약:

이 글은 API로 데이터를 받아와서 리스트뷰로 띄우는 중에 생긴 에러를 다루고 있음.

데이터를 json 디코딩해서 모델로 변경하지 않고 바로 쓰고 있다면, 실제로는 값이 없는 프로퍼티를 참조했을 때 이런 에러가 날 수 있음.

 

 

본문

원래 swift로 개발하던 앱이 있었는데 언니가 안드로이드로도 내라고 해서 플러터로 급 개발 중이다.

검색 기능을 구현하던 중 문제가 생겼다.

RangeError (end): Invalid value: Only valid value is 0: 4

검색해보니 0: 1 이런 건 있어도 0: 4 이런 에러는 없었다.

왜 이런 에러가 나는지 알 수가 없었음.

이쯤에서 API에 데이터 요청을 하고 받아와서 위젯에 그리는 코드를 한 번 살펴보자.

// 데이터 요청
getJSON(String query) async {
    if (page == 1) {
      setState(() {
        _loading = true;
        movieList.clear();	// 클래스에 List movieList; 로 선언되어 있다
      });
    }
    var url = "https://api.themoviedb.org/3/search/movie?api_key=${FlutterConfig
        .get('TMDB_KEY')}&language=ko-KR&query=${query}&page=${page}";
    var response = await http.get(Uri.parse(url));
    setState(() {
      List result = json.decode(response.body)['results'];
      if (result.isEmpty) {
        if (page == 1) {
          _noData = true;
        } else {
          _endofData = true;
        }
      } else {
        _noData = false;
        _endofData = false;
        movieList.addAll(result);
      }
    });
    if (page == 1) {
      setState(() => _loading = false);
    }
  }
// ListBuilder 부분
ListView.builder(
            shrinkWrap: true,
            itemBuilder: (context, index) {
              return GestureDetector(
                child: Container(
                    color: Colors.white,
                    padding: EdgeInsets.symmetric(vertical: 5, horizontal: 0),
                    child: Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                              movieList[index]['release_date'] == null
                                  ? Text(
                                "연도 미상",
                                style: TextStyle(color: Colors.black38,
                                    fontWeight: FontWeight.w500),
                              ) : Text(
                                movieList[index]['release_date']
                                    .toString()
                                    .substring(0, 4)
                                    .trim(),
                                style: TextStyle(color: Colors.black38,
                                    fontWeight: FontWeight.w500),
                              ),
                              SizedBox(height: 5),
                              Text(
                                "\u{2B50} ${movieList[index]['vote_average']}"
                                    .toString()
                                    .trim(),
                                style: TextStyle(color: Colors.black38,
                                    fontWeight: FontWeight.w500),
                              )
                            ],
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                          )
                        ]
                    )
                ),
              );
            },
            itemCount: movieList.length,
            controller: _scrollController
          )

핵심만 빠르게 보기 위해서 코드를 조금 줄였다.

왜 이런 에러가 나는지 알 수 없었다. 어떤 건 데이터가 조금이어도 잘 표시가 됐지만, 어떤 건 데이터가 많아도 이런 에러가 나기도 했기 때문이다.

 

결론적으로, API에서 받아온 데이터를 따로 가공하지 않고 그대로 쓴 게 문제였다.

로그를 찍어보니, 'release_date'가 비어있으면 아예 index에 잡히지 않았다.

그런데 공교롭게도 그게 4번째 데이터여서 0: 4 문제가 생기는 거였다.

다른 검색어로도 시도했는데 0: 4가 나오길래 테스트해보니, 그 검색어 역시 4번째 데이터의 release_date가 비어 있었다.

그러니 나오지 않을 수밖에...

 

 

해결법

이를 해결하기 위해 데이터 모델 클래스를 만들어준다.

일단은 이 정도만 필요하기 때문에 간단하게만 만들었다.

class MovieModel {
  int? totalResults;
  int? totalPages;
  List<Movie>? results;

  MovieModel({this.totalResults, this.totalPages, this.results});

  fromJson(Map<String, dynamic> json) {
    totalResults = json['total_results'];
    totalPages = json['total_pages'];
    if (json['results'] != null) {
      results = [];
      json['results'].forEach((v) {
        results!.add(new Movie.fromJson(v));
      });
    }
  }
}

class Movie {
  int? id;
  String? posterPath;
  String? title;
  String? releaseDate;
  num? voteAverage;

  Movie({
    this.id,
    this.posterPath,
    this.title,
    this.releaseDate,
    this.voteAverage
  });
}

그런 다음 getJson 함수를 조금 바꿔준다. 똑같은 부분은 생략한다.

먼저 MovieModel 객체를 만들어준 다음에 fromJson 메소드 매개변수로 json을 넣어준다. 그럼 객체의 results에는 Movie들의 정보가 저장된다.

그 리스트가 비었는지 확인해서, 비지 않았다면 movieList에 하나씩 더해준다.

여기서 원래 List movieList로 선언되어 있던 걸 List<Movie> movieList로 타입을 확실히 정해준다.

  getJSON(String query) async {
    // code
    setState(() {
      MovieModel model = MovieModel();
      model.fromJson(json.decode(response.body));
      if (model.results?.isEmpty ?? true) {
        // code
      } else {
        _noData = false;
        _endofData = false;
        model.results?.forEach((movie) => movieList.add(movie));
      }
    });
    // code
  }

이제 위젯에서 string interpolation을 할 때 다음과 같이 바꿔주면 된다.

				movieList[index].releaseDate?.isEmpty ?? true	// 여기
                                    ? Text(
                                  "연도 미상",
                                  style: TextStyle(color: Colors.black38,
                                      fontWeight: FontWeight.w500),
                                ) : Text(
                                  movieList[index].releaseDate	// 여기
                                      .toString()
                                      .substring(0, 4)
                                      .trim(),
                                  style: TextStyle(color: Colors.black38,
                                      fontWeight: FontWeight.w500),
                                ),
                                SizedBox(height: 5),
                                Text(
                                  "\u{2B50} ${movieList[index].voteAverage}"	// 여기
                                      .toString()
                                      .trim(),
                                  style: TextStyle(color: Colors.black38,
                                      fontWeight: FontWeight.w500),
                                )

 

API에서 받은 데이터를 그대로 쓰는 데에는 위험이 도사리고 있다.

웬만하면 모델 클래스를 만들어 모델로 변환하고, 데이터가 없을 때는 어떻게 할지 에러 처리를 하는 것이 좋다.

 

728x90
반응형

' > Flutter' 카테고리의 다른 글

Dart에서 객체 다루기 - Map  (0) 2021.10.06
Dart에서 리스트 다루기  (0) 2021.10.04
생존을 위한 Dart 문법 톺아보기  (0) 2021.10.02
Comments