목차
- 사용된 기능 설명
- 사용 예시
✅ 사용된 기능 설명
- ScrollController() : 스크롤 위치를 감시하거나 제어할 때 사용하는 컨트롤러
- position : 현재 스크롤 상태를 나타내는 ScrollPosition 객체를 반환 (스크롤 위치, 범위 등을 확인 가능)
- pixels : 현재 스크롤 오프셋 (맨 위 = 0, 아래로 갈수록 값 증가)
ex) controller.position.pixels == 100.0 → 스크롤이 100px 내려감 - maxScrollExtent : 스크롤 가능한 최대 위치 (맨 아래)
ex) position.pixels == maxScrollExtent → 맨 아래 도달 - minScrollExtent : 스크롤 가능한 최소 위치 (보통 0).
ex) 스크롤이 맨 위에 있는지 확인할 때 사용 - extentBefore : 현재 뷰포트(Viewport) 위쪽에 있는 콘텐츠의 길이
ex) 200px로 내려왔으면 extentBefore = 200 - extentAfter : 현재 뷰포트 아래쪽에 남아 있는 콘텐츠의 길이.
ex) 무한 스크롤 로직에서 사용
- pixels : 현재 스크롤 오프셋 (맨 위 = 0, 아래로 갈수록 값 증가)
- jumpTo(double offset) : 스크롤을 즉시 특정 위치로 이동
- animateTo(double offset, ...) : 스크롤을 애니메이션과 함께 이동
ex) 부드럽게 맨 아래로 스크롤
- position : 현재 스크롤 상태를 나타내는 ScrollPosition 객체를 반환 (스크롤 위치, 범위 등을 확인 가능)
- addListener(_nextLoad) : ChangeNotifier 또는 Listenable 을 상속한 객체에서 공통적으로 제공되는 메서드. 해당 코드에선 스크롤이 움직일 때마다 _nextLoad 메서드를 실행
- "$_url?_page=$_page&_limit=$_limit" : 쿼리 파라미터. API 요청할 때 추가 조건(페이지, 개수 등)을 URL 뒤에 붙여 서버로 전달
** 실제로 담긴 데이터 "https://jsonplaceholder.typicode.com/albums?_page=1&_limit=20" - addAll() : 리스트에 여러 요소를 한 번에 추가. add()는 한 개, addAll()은 여러 개
- 객체.dispose() : 객체의 메모리 정리 메소드 (페이지를 벗어나면 더 이상 필요 없는 리소스를 해제 해주어 메모리 누수 방지)
- removeListener() : 컨트롤러는 유지하면서 특정 이벤트만 제거.
- removeListener(initLoad)를 안하는 이유 : _initLoad는 앱 시작할 때 한 번만 실행되는 함수. 이벤트 리스너가 아니라 단순 메서드 실행이므로 제거할 필요 없음 (_nextLoad는 스크롤 이벤트와 연결된 리스너이므로 제거 필요)
- 매개변수 내에서 ${} vs 그냥 값 : 문자열 보간법(${}). 문자열 안에서 변수를 섞어 쓸 때 필요하며 문자열이 아닌 경우는 그냥 바로 넘겨주면 됨
- if문에서 중괄호 {} 생략 : Dart에서는 if 뒤에 실행할 코드가 하나의 문(statement)이면 {} 생략 가능. 여러 문(statements)이면 반드시 {} 필요.
** 중괄호 생략 예시 if(true) Container(...)
** 중괄호 사용 예시 if (true) { Container(...); TextButton(...); }
✅ 사용 예시
1️⃣ pubspec.yaml

2️⃣ main.dart (import)

3️⃣ main.dart (전역변수 선언 및 초기화, initState)
- 전역변수 선언 및 초기화
- initState

4️⃣ main.dart (최초 로딩 시 작동)

5️⃣ main.dart (다음 페이지 호출 시 작동)

6️⃣ main.dart (dispose)

7️⃣ main.dart (Scaffold 작성)


8️⃣ 테스트
✔️ ios 문제로 http 에서 데이터 안받아와졌는데 갑자기 또 되네 왜지...?

✔️ 사용된 코드
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _url = 'https://jsonplaceholder.typicode.com/albums';
int _page = 1; // 초기 시작할 때 페이지 넘버링
final int _limit = 20; // 한 번에 가져올 데이터의 양
bool _hasNextPage = true; // 다음 페이지 존재 여부 (true : 있다 / false : 없다)
bool _isFirstLoadRunning = false; // 첫 번째 로딩 여부 (true : 맞다 / false : 아니다)
bool _isLoadMoreRunning = false; // 추가로 더 로딩할 데이터가 있는지 여부 (true : 있다 / false : 없다)
List _albumList = [];
late ScrollController _controller; // 스크롤 위치 확인용
@override
void initState(){
super.initState(); // 부모 초기화 로직을 반드시 실행하도록 보장하는 코드 (항상 첫 줄에서 호출)
_initLoad();
// ScrollController() : 스크롤 위치를 감시하거나 제어할 때 사용하는 컨트롤러
// addListener(_nextLoad) : 스크롤이 움직일 때마다 _nextLoad 메서드를 실행
_controller = ScrollController()..addListener(_nextLoad);
}
void _initLoad() async{ // 최초 로딩 시 작동
// setState(){ ... } : 문법 오류
// setState(() { ... }); : setState에 콜백 함수를 넘기는 문법.
setState((){
_isFirstLoadRunning = true; // 최초 로딩이기 때문에 true
});
try{
// "$_url?_page=$_page&_limit=$_limit" : 쿼리 파라미터(Query Parameter). API 요청할 때 **추가 조건(페이지, 개수 등)**을 URL 뒤에 붙여 서버로 전달
final res = await http.get(Uri.parse("$_url?_page=$_page&_limit=$_limit"));
setState((){
_albumList = json.decode(res.body); // url 데이터를 flutter에서 사용가능한 객체(JSON)로 변환
});
}catch(e){
print(e.toString());
}
setState((){
_isFirstLoadRunning = false; // 최초 로딩이 아니다
});
}
void _nextLoad() async{ // 다음 페이지 호출할 때 작동
if(_hasNextPage
&& !_isFirstLoadRunning
&& !_isLoadMoreRunning
&& _controller.position.extentAfter < 100 // Scroll 되는 강도 (숫자가 낮을 수록 만감)
){ // 다음 페이지가 있고, 초기 로딩이 아니고, 추가로 로딩할 데이터가 없을 때 작동
setState(() {
_isLoadMoreRunning = true;
});
_page += 1; // 다음 페이지로 넘어갈 때마다 페이지 증가
try { // http 데이터를 정상적으로 받아와 JSON 데이터로 변환이 됐을 떄
final res = await http.get(Uri.parse("$_url?_page=$_page&_limit=$_limit"));
final List fetchedAlbums = json.decode(res.body);
if(fetchedAlbums.isNotEmpty){ // 데이터가 비어있지 않으면 .addAll()
setState((){
_albumList.addAll(fetchedAlbums); // 리스트에 여러 요소를 한 번에 추가. add()는 한 개, addAll()은 여러 개
});
}else{ // 더 이상 데이터가 없으면 마지막 페이지로 초기화 (_hasNextPage = false)
setState((){
_hasNextPage = false;
print("마지막 페이지"); // 더 이상 데이터 없으면 끝, Circle... 안됨
});
}
}catch(e){ // http 데이터를 정상적으로 받아 올 수 없거나, JSON 데이터로 변환에 실패했을 때
print(e.toString()); // 실제 앱에서는 팝업이나 alert 띄워주기
}
setState((){
_isLoadMoreRunning = false; // 추가로 로딩할 데이터가 없다
});
}
}
@override
void dispose() { // 해당 페이지를 벗어났을 때 연결 해제 (페이지가 여러 개 일 경우 마지막에 필수로 작성)
// _controller.dispose() : 컨트롤러 자체를 메모리에서 정리
// removeListener() : 컨트롤러는 유지하면서 특정 이벤트만 제거.
// 컨트롤러를 계속 쓸 거라면 removeListener()만 사용
// removeListener(initLoad)를 안하는 이유 : _initLoad는 앱 시작할 때 한 번만 실행되는 함수. 이벤트 리스너가 아니라 단순 메서드 실행이므로 제거할 필요 없음 (_nextLoad는 스크롤 이벤트와 연결된 리스너이므로 제거 필요)
_controller.removeListener(_nextLoad);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _isFirstLoadRunning ? // 삼항 연산자
const Center(
child: CircularProgressIndicator(), // 초기 데이터 호출 시 로딩
)
: Column(
children: [
Expanded( // 공간을 다 채울 때 사용 (반응형)
child: ListView.builder(
controller: _controller,
itemCount: _albumList.length,
itemBuilder: (context, index) => Card(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), // symmetric : 대칭
child: ListTile(
// 매개변수 내에서 ${} vs 그냥 값 : 문자열 보간법(${}). 문자열 안에서 변수를 섞어 쓸 때 필요하며 문자열이 아닌 경우는 그냥 바로 넘겨주면 됨
title: Text(_albumList[index]['id'].toString()),
subtitle: Text(_albumList[index]['title'].toString()),
),
),
),
),
// if문에서 중괄호 {} 생략 : Dart에서는 if 뒤에 실행할 코드가 하나의 문(statement)이면 {} 생략 가능. 여러 문(statements)이면 반드시 {} 필요.
if(_isLoadMoreRunning)
Container(
padding: const EdgeInsets.all(30),
child: const Center(
child: CircularProgressIndicator(),
),
),
if(_hasNextPage == false)
Container(
padding: const EdgeInsets.all(20),
color: Colors.blue,
child: const Center(
child: Text(
"No more data to be fetched",
style: TextStyle(color: Colors.white),
),
),
)
],
),
);
}
}
'IT 언어 > Flutter' 카테고리의 다른 글
| [Flutter] Firebase 연동 및 이메일 로그인 [맥북💻] (0) | 2025.10.04 |
|---|---|
| [Flutter] Carousel Silder (자동 슬라이드) [맥북💻] (0) | 2025.10.03 |
| [Flutter] MVVM 패턴 (Consumer, ChangeNotifierProvider)[맥북💻] (0) | 2025.09.29 |
| [Flutter] Form 양식 제출 (GlobalKey, TextFormField, ModalRoute) [맥북💻] (0) | 2025.09.29 |
| [Flutter] 웹뷰 (webview_flutter) [맥북💻] (0) | 2025.09.28 |