본문 바로가기
Flutter

[Flutter] Widget(2) - ListView

by 아마도개발자 2023. 12. 6.
ListView Widget
  • ListView는 가장 기본적인 스크롤링 widget이다. ListView는 itemCount만큼의 자식 요소를 가지고, 하나의 자식 밑에 다른 자식이 스크롤 방향으로 쌓이는 형상을 하고 있다.

 

ListView를 생성하는 4가지 옵션

 

1. 명시적으로 childeren에 List<Widget>을 넘기는 방법.

  • 자식요소의 수가 적은 리스트뷰에 적합하다.
  • 눈에 보이는 자식 뿐만 아니라 목록 보기에 표시될 가능성이 있는(즉, 모든 자식 요소)에 대한 작업이 수행된다.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Sample(),
    );
  }
}

class Sample extends StatelessWidget {
  const Sample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
  	
    return ListView(   
      // 자식요소로 List를 바로 넘겨준 것을 확인할 수 있다.
      children: [ 
        Text("1번 자식"),
        Text("2번 자식"),
        Text("3번 자식"),
        Text("4번 자식"),
        Text("5번 자식"),
        Text("6번 자식"),
      ],
    );
  }
}

 

 

2. ListView.builder를 사용하는 방법.

  • 이 생성자는 IndexedWidgetBuilder를 인자로 받는다.
  • 실제로 보이는 자식에 대해서만 호출하기 때문에 자식요소가 많은 ListView에 적합하다.
  • 자식요소의 Index가 필요한 경우 사용하기 유용하다.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Sample(),
    );
  }
}

class Sample extends StatelessWidget {
  const Sample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        padding: const EdgeInsets.all(8),
        itemCount: 100,
        // 필수 인자로 itemBuilder를 받아야 하며 이 builder는 context, index를 인자로 가진 함수이다.
        itemBuilder: (BuildContext context, int index) {
          return Text("$index번 자식요소");
        });
  }
}

 

여기서 ListView.builder의 정의를 들어가 보면

ListView.builder({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    this.itemExtent,
    this.prototypeItem,
    required IndexedWidgetBuilder itemBuilder,   // <-----------------------
    ChildIndexGetter? findChildIndexCallback,
    int? itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    int? semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  })

 

이렇게 itemBuilder가 IndexedWidgetBuilder 타입인 것을 확인 할 수 있다.

 

 

3. ListView.separted를 사용하는 방법.

  • 이 생성자는 아이템과 아이템 사이에 구분자를 넣어줄 수 있다.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Sample(),
    );
  }
}

class Sample extends StatelessWidget {
  const Sample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: 100,
      itemBuilder: (context, index) {
        return Text("$index번 자식");
      },
      separatorBuilder: (context, index) {
        return const Divider();
      },
    );
  }
}

 

예제와 같이 separatorBuilder를 사용하여 item의 요소 사이사이에 내가 원하는 Widget을 넣어줄 수 있다.

 

4. ListView.custom를 사용하는 방법

  • SliverchildDelegate를 가지며, 이를 통해서 자식요소에서의 추가적인 작업을 가능하게 한다.
  • 가장 큰 장점은 목록을 출력하기 위한 상태를 분리할 수 있다는 점이다.
  • custom과 SliverChildDelegate가 제공하는 속성을 직접 제어하여 ListView의 동작 방식을 새롭게 구현하거나, 최적화를 위한 로직을 추가할 수 있다
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Sample(),
    );
  }
}

class Sample extends StatefulWidget {
  const Sample({Key? key}) : super(key: key);

  @override
  State<Sample> createState() => _SampleState();
}

class _SampleState extends State<Sample> {
  List<String> items = <String>['1', '2', '3', '4', '5'];

  void _reverse() {
    setState(() {
      items = items.reversed.toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.custom(
          // 다른 생성자들과는 다르게 childrenDelegate를 인자로 사용하여 자식 모델을 제어한다.
          childrenDelegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                return KeepAlive(
                  data: items[index],
                  key: ValueKey<String>(items[index]),
                );
              },
              childCount: items.length,
              findChildIndexCallback: (Key key) {
                final ValueKey<String> valueKey = key as ValueKey<String>;
                final String data = valueKey.value;
                return items.indexOf(data);
              }),
        ),
      ),
      bottomNavigationBar: BottomAppBar(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextButton(
              onPressed: () => _reverse(),
              child: const Text('Reverse items'),
            ),
          ],
        ),
      ),
    );
  }
}

class KeepAlive extends StatefulWidget {
  const KeepAlive({
    required Key key,
    required this.data,
  }) : super(key: key);

  final String data;

  @override
  State<KeepAlive> createState() => _KeepAliveState();
}

class _KeepAliveState extends State<KeepAlive>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Text(widget.data);
  }
}

 

 

ListView WIdget 팁

 

  • Null이 아닌 경우 itemExtent 속성은 자식요소가 스크롤 방향으로 지정된 범위만큼 차지하도록 강제한다.
  • Null이 아닌 경우 prototypeItem 속성은 스크롤 방향으로 지정된 위젯만큼 차지하도록 강제한다.
  • itemExtent나 prototypeItem 속성을 사용하는 것은 자식요소가 자신들이 차지하는 공간의 크기를 결정하는데 효과적이다. 예를 들어 스크롤의 위치가 급격하게 변하였을 때, 자식요소의 범위를 쉽게 계산할 수 있다.
  • 빈 리스트 처리 예제
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Empty List Test')),
    body: itemCount > 0  // 삼항연산자로 itemCount가 1이상일 경우에만 표현
      ? ListView.builder(
          itemCount: itemCount,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              title: Text('Item ${index + 1}'),
            );
          },
        )
      : const Center(child: Text('No items')),
  );
}