Infinite Scroll과 Pull-to-Refresh

김동연·2025년 5월 29일

개발기록일지(Flutter)

목록 보기
11/32

1. 무한스크롤 구현

NotificationListener 위젯

  • 하위 위젯에서 특정한 알림(스크롤 이동 등)이 발생 했을 때 onNotification 속성에 정의한 함수 실행
  • onNotification 속성에 들어가는 함수에는 notification 이 파라미터로 들어감
    • 스크롤 시작할 땐 ScrollStartNotification 객체,
    • 스크롤 이동할 땐 ScrollUpdateNotification 객체,
    • 스크롤 끝났을 땐 ScrollEndNotification 객체가 전달됨
  • ScrollUpdateNotification 객체엔 ScrollMetrics 타입의 metrics 속성이 있음
    • metrics.pixels: 현재 스크롤 위치.
    • metrics.maxScrollExtent: 스크롤 가능한 최대 범위.
    • metrics.minScrollExtent: 스크롤 가능한 최소 범위.
    • metrics.extentAfter: 현재 위치 이후 남은 스크롤 거리.
    • metrics.extentBefore: 현재 위치 이전의 스크롤 거리.
  • 현재 스크롤 위치(metrics.pixels)가 스크롤 가능한 최대 범위(metrics.maxScrollExtent)보다 클 때 데이터 추가 요청
  • onNotification 리턴타입 bool ⇒ 이벤트 버블링 취소할지 여부
    • 버블링 : 하위 노드에서 발생한 이벤트가 상위 노드로 전파되는 것 의미 ⇒ false 로 리턴
NotificationListener(
  onNotification: (notification) {
    if (notification is ScrollUpdateNotification) {
      if (notification.metrics.pixels >=
          notification.metrics.maxScrollExtent) {
        // 여기서 업데이트
      }
    }
    return true;
  },
  child: ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
    },
  ),
)

2. 새로고침

RefreshIndicator 위젯

  • 당겨서 새로고침(Pull-to-Refresh) 동작을 쉽게 구현할 수 있는 머티리얼 디자인 위젯
  • onRefresh 속성에 Future<void> Function() 선언하여 구현
import 'package:flutter/material.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

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

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<int> items = List.generate(20, (index) => index).toList();

  // 데이터 불러오는 도중 사용자가 스크롤 이동해서 중복 요청될 경우 방지용
  bool isFetching = false;
  void fetchMore() async {
    if (isFetching) {
      return;
    }
    print("fetchMore");
    isFetching = true;
    await Future.delayed(Duration(seconds: 3));
    final newList =
        List.generate(20, (index) => index + items.last + 1).toList();
    items.addAll(newList);
    setState(() {});
    isFetching = false;
  }

  Future<void> onRefresh() async {
    print("onRefresh");
    // 데이터 새로고침 도중 fetchMore 호출 방지
    if (isFetching) {
      return;
    }
    isFetching = true;
    await Future.delayed(Duration(seconds: 3));
    items = List.generate(20, (index) => index).toList();
    setState(() {});
    isFetching = false;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: NotificationListener(
        onNotification: (notification) {
          if (notification is ScrollUpdateNotification) {
            if (notification.metrics.pixels >=
                notification.metrics.maxScrollExtent) {
              fetchMore();
            }
          }
          return true;
        },
        child: RefreshIndicator(
          onRefresh: onRefresh,
          child: ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              return Container(
                alignment: Alignment.center,
                color: Colors.amber,
                padding: EdgeInsets.all(20),
                margin: EdgeInsets.all(20),
                child: Text('${items[index]}'),
              );
            },
          ),
        ),
      ),
    );
  }
}

0개의 댓글