웹툰 상세정보+에피소드 목록

차준우·2024년 7월 6일

flutter

목록 보기
23/25

이전 포스트에서 만든 메서드를 사용해보자

DetailScreen에서는 이전에 사용한 Future로 메서드 결과를 받고, FutureBuilder로 UI화 하는 방식을 그대로는 사용할 수 없다.
왜냐면 이전포스트에서 만든 메서드들은 ID가 추가로 필요하기 때문에 수정이 필요함

우선 detailScreen에서 아래 코드를 넣어보자

Future<WebtoonDetailModel> webtoon = ApiService.getToonById(id);

에러발생

The instance member 'id' can't be accessed in an initializer.
Try replacing the reference to the instance member with a different expression

인스턴스 멤버 'id'는 이니셜라이저에서 액세스할 수 없습니다.
인스턴스 멤버에 대한 참조를 다른 표현식으로 바꿔보세요.

문제를 해결하려면 우선 DetailScreen을 statueful로 바꿔야한다.
바꾸면 이전에는 멤버명만 사용해서 접근했었는데 widget.멤버명으로 접근하게 바뀐걸 볼 수 있다.

//이전
Text(title)
//수정 후
Text(widget.title)

Stateful로 변경하게 되면 기존 코드가 State클래스라는 것으로 분리된다.

class _DetailScreenState extends State<DetailScreen> {

그래서 위처럼 바뀌는 것.

멤버명 앞에 붙은 widget = DetailScreen위젯 을 말하는 것이다.

그러면 이제 아까 Api관련 에러는 해결됨?

Future<WebtoonDetailModel> webtoon = ApiService.getToonById(widget.id);

stateful 위젯으로 변경하면서 위처럼 widget.id가 됐지만 동일한 에러가 유지된다.
이유는 이 코드는 buil메서드 밖에 정의되어 constuctor에서 widget을 참조할 수 없기 때문

다음과 같이 변경하자

late Future<WebtoonDetailModel> webtoon;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    webtoon = ApiService.getToonById(widget.id);
  }

뭐가 다름?

initStates는 widget(DetailScreen)이 완전히 초기화 된 후에 호출되므로, 에러가 발생하지 않는다. 반면에 기존 코드는 변수를 초기화하는 시점에 widget에 접근하므로 오류가 발생했던 것.

그래서 다음과 같이 해결
1. late
2. initState 에서 초기화

초기화하고 싶은 변수가 있지만 생성자에서 못하는 경우, late 후 initState에서 사용

initState는 한 번만 호출된다는 점과 build메서드보다 먼저 실행된다는 점을 기억하자

webtoonEpisode도 똑같이 해주자.

late Future<WebtoonDetailModel> webtoon;
  late Future<List<WebtoonEpisodeModel>> episodes;

  @override
  void initState() {
    super.initState();
    webtoon = ApiService.getToonById(widget.id);
    episodes = ApiService.getLatestEpisodesById(widget.id);
  }

initState로 초기화하기 위해서 statefulWidget으로 변경한 것

에러

  • 에러1
Exception has occurred.
_TypeError (type '_Map<String, dynamic>' is not a subtype of type 'WebtoonEpisodeModel')

property명 에 오타가 있어서 ... date를 data로 잘못 입력했던...
오타가 있으면 json에서 값을 가져올 수가 없다(없는 key를 찾게 되는거니까)

  • 에러2
    ui구현 중에 만난 에러지만 여기에 정리한다
return Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 50),
                    Text(snapshot.data!.about));
              }
              
Too many positional arguments: 0 expected, but 1 found.
Try removing the extra positional arguments, or specifying the name for named arguments.

함수가 예상하지 않은 추가 인수를 받았다는 것을 의미한다.
즉, 함수가 인수를 받지 않는데, 하나의 인수를 전달하려고 했다는 뜻.
Text를 child 안에 넣어서 사용하자

child: Text(snapshot.data!.about));

UI를 만들자(상세정보)

List의 길이를 모르거나, List가 최적화가 필요하다면 ListView.builer,ListView를 해야하지만
길이를 알고있고, 그 길이가 짧다면 Column으로 해도 무방하다.

  • for문을 사용할 때 중괄호로 감싸면 안된다.

우선 강의 영상대로 코드를 작생해보자.

if (snapshot.hasData) {
                return Column(
                  children:[
                    for(var episode in snapshot.data!)
                    Text(episode.title);
                  ]
                );

그럼 이전에도 봤던 에러발생. UI가 화면을 초과해서 발생.
이럴 때는 ListView를 사용하거나 Column을 SingleChildScrollView로 감싸서 해결할 수 있다.

SingleChildScrollView
SingleChildScrollView 공식문서에서 확인해보니 자식 목록이 있고, 화면 너비가 항상 같은 스크롤처럼 교차 축 축소 래핑 동작이 필요없으면 ListView를 권장한다.

여기서는 위 이유로 listview.builder사용함

return Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 50),
                    child: ListView.builder(
                      itemBuilder: (context, index) {
                        var episode = snapshot.data![index];
                        return Text(episode.title);
                      },
                      itemCount: snapshot.data!.length,
                    ),
                  ),
                );

이전에도 했던 것이지만 다시 정리

현재 ListView의 부모는 Column
Column은 무한한 높이를 가짐. 공간 제한을 해줘야함.
그래서 expanded나 Flexible로 Listview를 감싸야한다.(크기가 정해진 위젯)

현재 스크롤을 시도하면 스크롤이 안된다.

정확히 말하면 화면 전체가 스크롤되는게 아니라 ListView로 이루어진 Episode블록만 스크롤이 가능하다.
하지만 이는 좋은 구현방법이 아니니 SingleChildScrollView도 같이 사용해보겠다.

근데 함께 사용하면 충돌이 일어날 수 있으므로 ListView에서 몇 가지 속성을 사용해야한다.

  • primary : false 스크롤 불가(기본 true)

  • shrinkWrap : true ListView크기 고정

  • Expaneded위젯 제거

    ListView를 Expaneded위젯으로 감쌌던 이유는 Column이 무한한 height를 가지기 때문에 발생하는 에러 때문인데,
    SingleScrollView를 사용함으로써 스크롤, 즉 무한한 높이가 아니게 되었다.
    따라서 필요없어진 Expaneded를 제거해주어야 원하는대로 동작한다.

번외. 에러 잡기

에피소드를 출력해야하는데 아무것도 안나옴

????????

확인해보니 episodes가 null임. api_service부터 확인해보자
WebtoonEpisodeModel형태의 List에 add를 하는 상황에서 WebtoonEpisodeModel.fromJson (이전에 만들었던 생성자)를 사용해서 값을 넣지 않아서 발생한 문제였다.

//전체코드
static Future<List<WebtoonEpisodeModel>> getLatestEpisodesById(
      String id) async {
    List<WebtoonEpisodeModel> episodesInstances = [];
    final url = Uri.parse("$baseUrl/$id/episodes");
    final response = await http.get(url);
    if (response.statusCode == 200) {
      final episodes = jsonDecode(response.body);
      for (var episode in episodes) {
        episodesInstances.add(WebtoonEpisodeModel.fromJson(episode));
      }
      return episodesInstances;
    }
    throw Error();
  }
//문제된 코드
for (var episode in episodes) {
   episodesInstances.add(episode));
}
//수정 코드
for (var episode in episodes) {
   episodesInstances.add(WebtoonEpisodeModel.fromJson(episode));
}

실행 순서로 에러난 이유를 살펴보자
1. api 요청
2. api반환값 response에 저장
3. response statusCode 200이면(정상) jsonDecode해서 episodes에 저장
이 때 episodes는 dynamic타입이다.
4. for문 시작. episodes에서 하나씩 값을 add시도
4-1. add될 List 타입 : WebtoonEpisodeModel,
add할 데이터 타입 : dynamic
에러 발생

WebtoonEpisodeModel타입으로 맞춰줘야한다.
그래서 WebtoonEpisodeModel에서 선언했던 생성자로 fromJson하여 삽입한다.

profile
개애발

0개의 댓글