[Flutter] 블로그 만들기 - pull_to_refresh 라이브러리 사용

2025. 2. 11. 09:50·Flutter/App
728x90

📌 pull_to_refresh 라이브러리 적용 (새로고침 & 무한 스크롤)

이번 글에서는 pull_to_refresh 라이브러리를 활용하여 게시글 목록에서 새로고침(Pull-to-Refresh)과 무한 스크롤(Pagination) 기능을 적용합니다.

  • 사용자가 화면을 아래로 당기면 새로고침되어 최신 데이터를 가져오고,
  • 스크롤을 끝까지 내리면 추가 데이터를 자동으로 로드하는 기능을 구현합니다.
  • 또한 메모리 누수(Memory Leak) 방지를 위해 dispose()를 활용하는 방법도 설명합니다.

1️⃣ pull_to_refresh 라이브러리 설치 및 설정

Flutter에서 pull_to_refresh 라이브러리를 사용하면 ListView에서 쉽게 새로고침 기능을 추가할 수 있습니다.

  • 아래로 스와이프 시 새로고침 (Pull-to-Refresh)
  • 스크롤이 끝까지 내려가면 추가 데이터 로딩 (Infinite Scroll)

📌 라이브러리 설치

       Flutter 프로젝트의 pubspec.yaml에 추가한 후, flutter pub get으로 설치합니다.

dependencies:
  pull_to_refresh: ^2.0.0

 

📌 패키지 가져오기

      Flutter 프로젝트에서 사용하려면 아래처럼 import 해야 합니다.

import 'package:pull_to_refresh/pull_to_refresh.dart';

 

📌 공식 문서

 

pull_to_refresh | Flutter package

a widget provided to the flutter scroll component drop-down refresh and pull up load.

pub.dev


디자인 시안


2️⃣ PostListBodyTemp (게시글 목록 + 새로고침 & 무한 스크롤)

 

게시글 목록을 표시하고, 새로고침 및 무한 스크롤을 적용합니다.

 

📌 PostListBodyTemp (기본 적용 코드)

// 로컬 상태 관리 (해당 페이지에서면 변경되는 데이터가 있다)
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

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

  @override
  State<PostListBodyTemp> createState() => _PostListBodyTempState();
}

class _PostListBodyTempState extends State<PostListBodyTemp> {
  // 사용자가 당기기, 사용자가 밑에서 올리기
  // 거기에 맞는 콜백 이벤트 메서드를 호출해야 사용이 가능하다.
  // _refreshController.refreshCompleted() <-- 새로 고침 완료 후 호출
  // loadCompleted() <-- 추가 데이터 로드 완료 후 호출
  RefreshController _refreshController = RefreshController();

  // 샘플 데이터
  List<Map<String, dynamic>> _posts = [
    {'id': 1, 'title': '1 번째 게시글', 'content': '내용 내용 1'},
    {'id': 2, 'title': '2 번째 게시글', 'content': '내용 내용 1'},
    {'id': 3, 'title': '3 번째 게시글', 'content': '내용 내용 1'},
    {'id': 4, 'title': '4 번째 게시글', 'content': '내용 내용 1'},
    {'id': 5, 'title': '5 번째 게시글', 'content': '내용 내용 1'},
    {'id': 6, 'title': '6 번째 게시글', 'content': '내용 내용 1'},
    {'id': 7, 'title': '7 번째 게시글', 'content': '내용 내용 1'},
    {'id': 8, 'title': '8 번째 게시글', 'content': '내용 내용 1'},
    {'id': 9, 'title': '9 번째 게시글', 'content': '내용 내용 1'},
    {'id': 10, 'title': '10 번째 게시글', 'content': '내용 내용 1'},
    {'id': 11, 'title': '11 번째 게시글', 'content': '내용 내용 1'},
    {'id': 12, 'title': '12 번째 게시글', 'content': '내용 내용 1'},
    {'id': 13, 'title': '13 번째 게시글', 'content': '내용 내용 1'},
  ];

  @override
  Widget build(BuildContext context) {
    return SmartRefresher(
      controller: _refreshController,
      enablePullDown: true,
      onRefresh: _onRefresh,
      enablePullUp: true,
      onLoading: _onLoading,
      child: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) => ListTile(
          title: Text('${_posts[index]['title']}'),
          subtitle: Text('${_posts[index]['content']}'),
        ),
      ),
    );
  }

  Future<void> _onRefresh() async {
    // 통신 가정
    await Future.delayed(Duration(seconds: 1));
    // 데이터가 새로 들어 옴
    setState(() {
      _posts = [
        ..._posts,
        {'id': 14, 'title': '14 번째 게시글', 'content': '내용 내용 1'},
        {'id': 15, 'title': '15 번째 게시글', 'content': '내용 내용 1'},
      ];
    });
    _refreshController.refreshCompleted();
  }

  // 페이징 동작 처리 (무한 스크롤)
  // 사용자가 리스트를 맨 아래로 스크롤 할 때 이벤트 리스너 동작
  // 새로운 데이터를  API 호출해서 상태 갱신을 해 주어야 한다.
  Future<void> _onLoading() async {
    // 통신 가정
    await Future.delayed(Duration(seconds: 1));
    setState(() {
      // 기존 있던 이터에 추가로 값을 넣어서 화면 갱신
      // 기존에 데이터 타입 -- 통으로 List 이다.
      // 새로운 API 호출 시 ---> 데이터 타입은 10개 ---. List 이다.
      // 기존에 리스트에서 + 리스트 하는 방법
      // _posts = _post + [];
      _posts.addAll([
        {'id': 21, 'title': '14 번째 게시글', 'content': '내용 내용 1'},
        {'id': 22, 'title': '15 번째 게시글', 'content': '내용 내용 1'},
        {'id': 23, 'title': '14 번째 게시글', 'content': '내용 내용 1'},
        {'id': 24, 'title': '15 번째 게시글', 'content': '내용 내용 1'},
        {'id': 25, 'title': '14 번째 게시글', 'content': '내용 내용 1'},
        {'id': 26, 'title': '15 번째 게시글', 'content': '내용 내용 1'},
        {'id': 27, 'title': '14 번째 게시글', 'content': '내용 내용 1'},
        {'id': 28, 'title': '15 번째 게시글', 'content': '내용 내용 1'},
      ]);
    });
    _refreshController.loadComplete();
  }

  // 화면이 종료 될 때 호출 되는 생명 주기를 가지고 있다.
  // 스트림이 내부적으로 동작을 한다.
  // refreshController 위젯이 제거 될 때 메모리에서 해제를 해야 한다.
  // 왜? 메모리 릭이 발생할 수 있다. (메모리 누수)
  @override
  void dispose() {
    _refreshController.dispose(); // 메모리 해제
    super.dispose();
  }
  // 해제를 안하면
  // 화면을 이동해서 스트림 리스너가 계속 실행된다.
  // 중첩이 되면 메모리가 점점 증가하면서 앱이 느려진다.
}

2️⃣ PostListBodyTemp 코드 설명

📌 PostListBodyTemp는 게시글 목록을 관리하며, 사용자가 새로고침 및 스크롤 시 새로운 데이터를 불러오는 기능을 포함합니다.

// 로컬 상태 관리 (해당 페이지에서 변경되는 데이터가 있다)
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

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

  @override
  State<PostListBodyTemp> createState() => _PostListBodyTempState();
}

✅ StatefulWidget 사용

  • 동적으로 데이터가 변경되므로 StatefulWidget을 사용
  • State 객체에서 게시글 데이터를 관리합니다.

3️⃣ 새로고침 및 페이징 컨트롤러 설정

class _PostListBodyTempState extends State<PostListBodyTemp> {
  // 사용자가 당기기, 사용자가 밑에서 올리기
  // 거기에 맞는 콜백 이벤트 메서드를 호출해야 사용이 가능하다.
  // _refreshController.refreshCompleted() <-- 새로 고침 완료 후 호출
  // loadCompleted() <-- 추가 데이터 로드 완료 후 호출
  RefreshController _refreshController = RefreshController();

 

📌 설명

  • RefreshController를 사용하여 새로고침 및 무한 스크롤을 제어합니다.
  • refreshCompleted() → 새로고침 완료 후 호출
  • loadComplete() → 추가 데이터 로드 완료 후 호출

4️⃣ 샘플 데이터

 
  // 샘플 데이터
  List<Map<String, dynamic>> _posts = [
    {'id': 1, 'title': '1 번째 게시글', 'content': '내용 내용 1'},
    {'id': 2, 'title': '2 번째 게시글', 'content': '내용 내용 1'},
    {'id': 3, 'title': '3 번째 게시글', 'content': '내용 내용 1'},
    {'id': 4, 'title': '4 번째 게시글', 'content': '내용 내용 1'},
    {'id': 5, 'title': '5 번째 게시글', 'content': '내용 내용 1'},
    {'id': 6, 'title': '6 번째 게시글', 'content': '내용 내용 1'},
    {'id': 7, 'title': '7 번째 게시글', 'content': '내용 내용 1'},
    {'id': 8, 'title': '8 번째 게시글', 'content': '내용 내용 1'},
    {'id': 9, 'title': '9 번째 게시글', 'content': '내용 내용 1'},
    {'id': 10, 'title': '10 번째 게시글', 'content': '내용 내용 1'},
  ];

 

📌 설명

  • 기본적인 샘플 게시글 데이터를 생성합니다.
  • 실제 프로젝트에서는 서버 API를 통해 데이터를 받아오는 방식으로 변경해야 합니다.

5️⃣ SmartRefresher 위젯 적용

  @override
  Widget build(BuildContext context) {
    return SmartRefresher(
      controller: _refreshController,
      enablePullDown: true, // 사용자가 아래로 당기면 새로고침 실행
      onRefresh: _onRefresh, // 새로고침 이벤트 핸들러
      enablePullUp: true, // 스크롤이 끝까지 내려가면 추가 로드 실행
      onLoading: _onLoading, // 추가 데이터 로드 이벤트 핸들러
      child: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) => ListTile(
          title: Text('${_posts[index]['title']}'),
          subtitle: Text('${_posts[index]['content']}'),
        ),
      ),
    );
  }

 

📌 설명

  • SmartRefresher를 사용하여 새로고침 및 무한 스크롤 기능을 추가합니다.
  • enablePullDown: true → 당겨서 새로고침 가능
  • enablePullUp: true → 스크롤을 끝까지 내리면 추가 데이터 로드 가능

6️⃣ 새로고침 기능 (Pull-to-Refresh)

  Future<void> _onRefresh() async {
    // 통신 가정 (서버에서 데이터를 가져온다고 가정)
    await Future.delayed(Duration(seconds: 1));

    // 새로운 데이터 추가
    setState(() {
      _posts.insert(0, {'id': 11, 'title': '새로운 게시글', 'content': '새로운 내용'});
    });

    // 새로고침 완료
    _refreshController.refreshCompleted();
  }

 

📌 설명

  • _onRefresh()는 사용자가 리스트를 아래로 당길 때 실행됩니다.
  • setState()를 호출하여 새로운 게시글을 추가하고, UI를 업데이트합니다.
  • 새로고침이 완료되면 _refreshController.refreshCompleted()를 호출합니다.

7️⃣ 무한 스크롤 기능 (페이징)

  // 페이징 동작 처리 (무한 스크롤)
  Future<void> _onLoading() async {
    // 통신 가정 (서버에서 추가 데이터를 가져온다고 가정)
    await Future.delayed(Duration(seconds: 1));

    setState(() {
      // 새로운 데이터 추가
      _posts.addAll([
        {'id': 12, 'title': '더 많은 게시글', 'content': '추가된 내용'},
        {'id': 13, 'title': '더 많은 게시글', 'content': '추가된 내용'},
      ]);
    });

    // 추가 데이터 로드 완료
    _refreshController.loadComplete();
  }

 

📌 설명

  • _onLoading()은 사용자가 스크롤을 끝까지 내릴 때 실행됩니다.
  • 새로운 데이터를 _posts 리스트에 추가하고, UI를 업데이트합니다.
  • 추가 데이터 로드 완료 후 _refreshController.loadComplete()를 호출합니다.

8️⃣ 메모리 누수 방지

  // 화면이 종료될 때 호출되는 생명주기 함수
  @override
  void dispose() {
    _refreshController.dispose(); // 컨트롤러 메모리 해제
    super.dispose();
  }

 

📌 설명

  • dispose()는 위젯이 사라질 때 호출됩니다.
  • refreshController.dispose();를 호출하여 메모리 해제
  • 메모리 누수 방지를 위해 dispose() 필수!

9️⃣ Stream 활용해 보기 - 기본 개념

📌 Future vs Stream

  • Future<T> → 한 번만 실행되고 결과가 반환되면 종료
  • Stream<T> → 여러 개의 데이터를 순차적으로 전달
import 'dart:async';

// Future<T>는 한 번 실행되고 완료되는 작업
// Stream<T>는 여러 개의 데이터를 순차적으로 전달하는 구조
void main() async {
  // 데이터를 보내는 역할
  Stream<int> numberStream() async* {
    for (int i = 0; i < 3; i++) {
      await Future.delayed(Duration(seconds: 1));
      yield i; // 데이터를 하나씩 보냄
    }
  }

  // 스트림을 구독해 보자 (데이터를 받음)
  await for (var number in numberStream()) {
    print('스트림 수신 : $number');
  }
}

 

📌 설명

  • async* + yield를 활용하여 데이터를 순차적으로 전송
  • await for을 사용하여 스트림 데이터를 순차적으로 처리

🔟 StreamController 활용하기

📌 StreamController를 사용하여 직접 이벤트를 추가

import 'dart:async';

// async* + yield 간단한 스트림 생성
// StreamController는 조금 더 복잡한 스트림을 생성할 때 사용 가능
void main() async {
  // 1. 스트림 컨트롤러 생성
  StreamController<String> streamController = StreamController();

  // 리스너 등록 (데이터를 수신하는 부분)
  streamController.stream.listen((event) {
    print('이벤트 수신: $event');
  });

  // 2. 스트림을 통해 이벤트 전달
  await Future.delayed(Duration(seconds: 1));
  streamController.add('데이터 1');

  await Future.delayed(Duration(seconds: 1));
  streamController.add('데이터 2');

  await Future.delayed(Duration(seconds: 1));
  streamController.add('데이터 3');

  // 3. 스트림 종료
  streamController.close();
}

 

📌 설명

  • StreamController를 사용하면 데이터를 원하는 시점에 전송 가능
  • stream.listen((event) { ... }) → 이벤트를 구독하여 처리
  • streamController.add('데이터') → 새로운 데이터를 추가

✅ 마무리: 구현된 기능 정리

✔ pull_to_refresh 라이브러리를 활용한 새로고침 및 무한 스크롤 기능 구현
✔ _onRefresh()로 새로운 데이터 로드, _onLoading()으로 추가 데이터 페이징 처리
✔ dispose()를 활용하여 메모리 누수 방지
✔ Stream을 활용한 비동기 데이터 처리 및 실시간 이벤트 관리

 

🚀 이제 비동기 데이터 처리와 UI 업데이트를 효율적으로 할 수 있습니다!

 

다음 글에서는 게시글 목록을 효과적으로 관리하기 위해 PostListPage를 구성하고, AutoDisposeNotifier를 활용하여 메모리 최적화 및 상태 관리 방법을 살펴보겠습니다.


게시글 작성 기능 구현이 궁금하다면??

 

2025.02.09 - [Flutter/App] - [Flutter] 블로그 만들기 - 게시글 작성 기능 구현

 

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

[Flutter] 블로그 만들기 - 게시글 모델링  (0) 2025.02.11
[Flutter] 블로그 만들기 - 게시글 작성 기능 구현  (0) 2025.02.10
[Flutter] 블로그 만들기 - 게시글 관리 및 로그아웃 기능 구현  (0) 2025.02.10
[Flutter] 블로그 만들기 -자동 로그인 기능 구현 및 UI  (0) 2025.02.10
[Flutter] 블로그 만들기 - 회원가입 기능 구현 및 UI  (1) 2025.02.09
'Flutter/App' 카테고리의 다른 글
  • [Flutter] 블로그 만들기 - 게시글 모델링
  • [Flutter] 블로그 만들기 - 게시글 작성 기능 구현
  • [Flutter] 블로그 만들기 - 게시글 관리 및 로그아웃 기능 구현
  • [Flutter] 블로그 만들기 -자동 로그인 기능 구현 및 UI
공돌이 출신 개발자
공돌이 출신 개발자
공돌이 출신 개발자입니다
  • 공돌이 출신 개발자
    공돌이 출신 개발자
    공돌이 출신 개발자
  • 전체
    오늘
    어제
    • 분류 전체보기 (124)
      • Database (0)
        • SQL (0)
        • 1일 1쿼리 (9)
      • Flutter (40)
        • Dart 언어 (18)
        • App (22)
      • Git (0)
      • Http 기초 지식 (14)
      • HTML5 & CSS3 (0)
      • Java (33)
      • JSP (0)
      • JavaScript (0)
      • Linux (0)
      • MSA (0)
      • Project (0)
      • React (0)
      • Spring (19)
      • 설치 메뉴얼 (1)
      • [Flutter] 프로젝트 (눈길) (8)
        • 작업일지 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    프로그래밍
    Java
    android studio
    데이터
    안드로이드
    객체지향
    프로젝트
    개발
    spring boot
    객체
    SQL
    JAVA 기초
    SQLD
    jsp
    dart
    코딩
    로그인
    메서드
    flutter
    1일1쿼리
    앱 개발
    HTTP
    Android
    회원가입
    앱개발
    공부
    블로그 만들기
    플러터
    클래스
    안드로이드 앱 개발
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
공돌이 출신 개발자
[Flutter] 블로그 만들기 - pull_to_refresh 라이브러리 사용
상단으로

티스토리툴바