선생님, 개발을 잘하고 싶어요.

[Flutter 기본] ListView 생성하기 본문

개발/flutter

[Flutter 기본] ListView 생성하기

알고싶은 승민 2020. 1. 25. 23:38

도입

  필자는 요즘 Flutter를 공부하고 있는데 이 녀석, 선언적 UI, 멀티 플랫폼 빌드 지원, 구글의 전폭적인 지원을 통한 성장이라는 요소들이 매우 매력적이다. 또한 Flutter 프레임워크와 이 프레임워크에서 사용하는 Dart라는 언어는 공식문서가 어마어마하게 잘되어있더라. 그래서 혼자 공부할 맛이 나는 요즘이다.

 

  그건 그렇고, 결국 프론트엔드 개발을 위한 프레임워크이다 보니 어쩌면 제일 중요한, 리스트를 그리는 방법에 대해서 명확하게 이해하는 게 필요하다. 그래서 정리해보았다. (내가 애니메이션 공부하다가 리스트 그리는 법을 몰라서 작성하는 포스트 이다.)

읽을 대상

  • Dart 클래스 생성에 대해서 알아야 함.
  • StatelessWidget에 대해서 알아야 함.
  • Flutter에 흥미가 있어야 함.

생성 방법 모음

  Flutter는 공식문서가 정말 매우 매우 매우 잘 되어 있기 때문에, 사실상 이번 글은 공식문서의 번역이라고 봐도 무방하다. (물론 필자가 원하는 대로 추가한 내용도 있으니 읽으면 좋을 듯)

  

 공식문서에서 제안하는 ListView 생성하는 방법은 총 4가지인데

1. 명시적으로 children으로 List <Widget>을 넘긴다.
2. ListView.builder를 사용해서 Lazy 하게 생성한다.
3. ListView.separted를 사용해서 Lazy 하게 생성하며, 아이템 사이에 별도의 아이템 (separator) 또한 Lazy 하게 생성한다.
4. ListView.custom을 사용해서 생성한다.

필자가 오늘 다룰 내용은 위의 3가지이다. 간단히 ListView의 사용법을 동작하는 코드 뭉치와 작동하는 예시를 통해 확인해보자. 

 

동작하는 코드 뭉치

  동작하는 코드 뭉치를 살펴보기 전에 우리가 오늘 만들 애플리케이션 화면부터 짬깐보자.

  • 해더가 있을 것
  • 사람 정보 카드를 보여줄 것
    • 이름, 나이를 보여줄 것 
    • 왼손잡이는 왼쪽 화살표를, 오른손잡이는 오른쪽 화살표를 그려줄 것

오늘은 ListView의 생성을 보는 게 목적이니까 기타 정의들은 후다닥 넘어가자.

 

Person Class 정의

class Person {
  int age;
  String name;
  bool isLeftHand;

  Person(this.age, this.name, this.isLeftHand);
}

PersonTile 정의

class PersonTile extends StatelessWidget {
  PersonTile(this._person);

  final Person _person;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(Icons.person),
      title: Text(_person.name),
      subtitle: Text("${_person.age}세"),
      trailing: PersonHandIcon(_person.isLeftHand),
    );
  }
}

class PersonHandIcon extends StatelessWidget {
  PersonHandIcon(this._isLeftHand);

  final bool _isLeftHand;

  @override
  Widget build(BuildContext context) {
    if (_isLeftHand) return Icon(Icons.arrow_left);
    else return Icon(Icons.arrow_right);
  }
}

 

HeaderTile 정의

class HeaderTile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Image.network("https://t1.daumcdn.net/thumb/R720x0/?fname=http://t1.daumcdn.net/brunch/service/user/1YN0/image/ak-gRe29XA2HXzvSBowU7Tl7LFE.png"),
    );
  }
}

 

호다닥

 

1. 명시적 children 넘기기

공식 문서 링크 

 

  첫 번째 방법은 ListView의 기본 생성자를 이용하는 것이다. 모든 Widget 객체를 만들고 넣어준다.

class ExplicitListConstructing extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(8),
      children: <Widget>[
        HeaderTile(),
        PersonTile(people[0]),
        PersonTile(people[1]),
        PersonTile(people[2]),
        PersonTile(people[3]),
        PersonTile(people[4]),
        PersonTile(people[5]),
      ],
    );
  }
}
  • 적은 수의 아이템을 가질 때에 적합하다.
  • ListView 로드 시점에 모든 child가 생성된다.

2. ListView.builder를 사용해서 생성하기

공식 문서 링크

 

  두 번째 방법은 RecyclerView를 사용해서 리스트를 구성해 본 경험이 있다면 더 익숙한 방법이다. 바로 builder를 사용해서 구성하는 것이다.

class UsingBuilderListConstructing extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.all(8),
      itemCount: people.length + 1,
      itemBuilder: (BuildContext context, int index) {
        if (index == 0) return HeaderTile();
        return PersonTile(people[index-1]);
      },
    );
  }
}

여기에서 재밌게 볼만한 내용은 두 가지인데.

  1. itemCount : 리스트에 그려질 총 child의 개수이다. Header 타일을 추가적으로 그려주기 위해, +1을 해준 것을 볼 수 있다. (이는 우리가 Adapter의 getItemCount와 유사하다!)
  2. itemBuilder : 리스트에 그려질 항목을 Lazy 하게, 해당 child가 화면에 보여야 할 때 생성한다. 이로 인해 많은 아이템을 그려주어야 할 때, 우리는 당연히 이걸 써야 한다.

itemBuilder에 대해서 조금만 더 살펴보자. 공식 문서상 ListView.builder 생성자는 IndexedWidgetBuilder라는 녀석을 인자로 받는다. 

 

그리고 이 녀석은 함수 타입의 별칭인데, 이렇게 생겼다. 

typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);

앞에서부터 하나씩 뜯어보면

  • Widget을 반환하는 함수
  • BuildContext와 int를 인자로 받는 함수

라는 것을 알 수 있고 여기서 중요한 int는 index로, 해당 child가 ListView에서 몇 번째 자식인지를 나타낸다. (얘를 들면 Header는 리스트의 맨 처음 나와야 하므로, index가 0이다.)

 

그래서 우리는 코드 상에서 람다를 사용해서 IndexedWidgetBuilder를 건네주었다. 

요런 녀석 탄생

3. ListView.separted를 사용해서 간단히 Divider까지 포함한 생성하기

공식 문서 링크

자 이제 다 왔다. 그런데 아이템과 아이템 사이에 구분자를 넣어주면 좀 더 깔끔한 리스트를 만들 수 있을 것 같다. 어떻게 할까? itemBuilder에서 index에 따라 한 번은 Divider를 그려주고, 다른 한번은 PersonTile을 그려주는 형태로 구현이 가능할 것이다.

 

하지만 플러터에서는 이런 작업이 매우 일반적으로 일어나는 작업이라는 사실에서 해당 기능을 아예 프레임워크에서 제공하였는데, 그게 바로 ListView.separted이다. 

 

결과물부터 보자

짜잔, 구분자가 들어갔다

 이를 위해서 우리가 할 일은 단지 구분자를 생성하는 separatorBuilder를 추가하면 된다. 이 녀석도 itemBuilder와 마찬가지로 IndexedWidgetBuilder 타입이므로 람다를 통해서 전달하자.

 

class UsingSeparateListConstructing extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        itemCount: people.length + 1,
        itemBuilder: (context, index) {
          if (index == 0) return HeaderTile();
          return PersonTile(people[index - 1]);
        },
        separatorBuilder: (context, index) {
          if (index == 0) return SizedBox.shrink();
          return const Divider();
        },
    );
  }
}

ListView.builder와 차이점은 separtorBuilder뿐이다. 주의할 사항은

  • itemCount는 itemBuilder에만 영향을 미친다.
  • separator는 item과 item 사이에 그려진다.
    • 따라서 separatorCount = itemCount-1이다

 

부가적으로

  • SizedBox.shrink()는 빈 Widget을 표현하기 위해 사용하였다. Stackoverflow (단순히 빈 Widget을 그려주기 위해 null을 반환하는 것은 안된다.)

마치며

Flutter는 재밌다. + 사이드 프로젝트를 진행해야 좀 더 정확한 감이 잡힐 것 같다. + 내 것 git에도 kotlin 이외의 언어 로그가 찍혔으면 좋겠다.

 

ListView 말고 GridView도 있던데, 이 부분도 공부해 보면 좋을 것 같다.

Comments