📌 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 |