Flutter에서 스크롤 뷰를 사용한 페이지네이션은 앱이 많은 양의 데이터를 표시할 때 유용한 패턴입니다.
이 패턴은 사용자가 스크롤할 때 새로운 데이터를 동적으로 로드하여 사용자 경험을 향상시킵니다.
이를 구현하는 데 필요한 주요 단계와 예제를 정리해보겠습니다.
주요 개념
1. ScrollController: 스크롤 위치를 추적하고 스크롤 이벤트를 처리합니다.
2. ListView.builder: 동적으로 리스트 아이템을 생성하여 성능을 최적화합니다.
3. HTTP 요청: 데이터를 페이징하여 서버에서 가져옵니다.
4. 상태 관리: 현재 페이지, 로딩 상태, 데이터 리스트 등을 관리합니다.
단계별 구현
1. 프로젝트 설정:
• 필요한 패키지 추가: http, provider (옵션)
2. ScrollController 설정:
• 스크롤 이벤트를 감지하고, 사용자가 리스트 끝에 도달하면 새로운 데이터를 로드합니다.
3. ListView.builder 사용**:
• 리스트 아이템을 동적으로 생성하여 메모리 사용을 최적화합니다.
4. 데이터 로드:
• 서버에서 데이터를 페이징하여 가져옵니다.
5. 상태 관리:
• StatefulWidget 또는 상태 관리 패키지(예: provider)를 사용하여 현재 페이지, 로딩 상태, 데이터 리스트 등을 관리합니다.
코드 예제
아래는 위 개념을 바탕으로 한 간단한 코드 예제입니다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ScrollController _scrollController = ScrollController();
List _data = [];
int _page = 1;
bool _isLoading = false;
bool _hasMoreData = true;
@override
void initState() {
super.initState();
_fetchData();
_scrollController.addListener(() {
if (_scrollController.position.extentAfter < 300 && !_isLoading && _hasMoreData) {
_fetchData();
}
});
}
Future<void> _fetchData() async {
setState(() {
_isLoading = true;
});
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums?_page=$_page&_limit=10'));
if (response.statusCode == 200) {
List newData = json.decode(response.body);
setState(() {
_data.addAll(newData);
_isLoading = false;
if (newData.length < 10) {
_hasMoreData = false;
} else {
_page++;
}
});
} else {
setState(() {
_isLoading = false;
_hasMoreData = false;
});
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pagination Example'),
),
body: _data.isEmpty && _isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
controller: _scrollController,
itemCount: _data.length + (_hasMoreData ? 1 : 0),
itemBuilder: (context, index) {
if (index == _data.length) {
return Center(child: CircularProgressIndicator());
}
return ListTile(
title: Text('Item ${_data[index]['id']}'),
subtitle: Text(_data[index]['title']),
);
},
),
);
}
}
주요 포인트
1. ScrollController 설정:
• 스크롤 이벤트를 감지하고, 사용자가 리스트 끝에 도달하면 _fetchData 함수를 호출하여 다음 페이지 데이터를 로드합니다.
2. ListView.builder:
• itemCount를 설정하여 로딩 중인 상태 표시를 위한 추가 아이템을 처리합니다.
• itemBuilder에서 로딩 인디케이터를 표시하여 더 많은 데이터를 로드 중임을 사용자에게 알립니다.
3. 데이터 로드 및 상태 관리:
• 데이터를 로드하는 동안 _isLoading 상태를 업데이트하여 UI에서 로딩 인디케이터를 표시합니다.
• 새로운 데이터를 가져와서 _data 리스트에 추가하고, 페이지 수를 증가시킵니다.
• 더 이상 가져올 데이터가 없을 경우 _hasMoreData를 false로 설정합니다.
다른 예제(실습)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _url = 'https://jsonplaceholder.typicode.com/albums';
int _page = 1;
final int _limit = 20;
bool _hasNextPage = true; // 다음 페이지가 있는지 여부
bool _isFirstLoadRunning = false; // 첫번째 페이지 로딩중
bool _isLoadMoreRunning = false; // 다음페이지 로딩중
List _albumList = [];
late ScrollController _controller;
@override
void initState() {
super.initState();
initLoad();
_controller = ScrollController()..addListener(_nextLoad);
}
void initLoad() async {
setState(() {
_isFirstLoadRunning = true;
});
try {
final res = await http.get(Uri.parse("$_url?_page=$_page&_limit=$_limit"));
setState(() {
_albumList = jsonDecode(res.body);
});
} catch(e) {
print(e.toString());
}
setState(() {
_isFirstLoadRunning = false;
});
}
void _nextLoad() async {
print("nextLoad");
if(_hasNextPage && !_isFirstLoadRunning && !_isLoadMoreRunning
&& _controller.position.extentAfter < 100) {
setState(() {
_isLoadMoreRunning = true;
});
_page += 1;
try {
final res = await http.get(Uri.parse("$_url?_page=$_page&_list=$_limit"));
final List fetchedAlbums = json.decode(res.body);
if(fetchedAlbums.isNotEmpty) {
setState(() {
_albumList.addAll(fetchedAlbums);
});
} else { // 데이터가 비어있는 경우
setState(() {
_hasNextPage = false;
});
}
} catch(e) {
print(e.toString());
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
// 페이지 종료 시 종료해 주는 기능
@override
void dispose() {
super.dispose();
_controller.removeListener(_nextLoad);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("test title"),
),
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: 0, horizontal: 10),
child: ListTile(
title: Text(_albumList[index]["id"].toString()),
subtitle: Text(_albumList[index]["title"]),
),
)
)
),
if (_isLoadMoreRunning == true)
Container(
padding: const EdgeInsets.all(30),
child: const Center(
child: CircularProgressIndicator(),
)
),
if (_hasNextPage == false)
Container(
padding: const EdgeInsets.all(20),
child: const Center(
child: Text(
"No more data to be fetched",
style: TextStyle(
color: Colors.white
)
),
)
)
],
)
);
}
}
참고 자료
• Flutter Documentation - ListView
'프론트엔드 > Flutter' 카테고리의 다른 글
flutter firebase 를 이용한 이메일 로그인 구현 (2) | 2024.07.23 |
---|---|
flutter todo-app 만들기 (1) | 2024.07.23 |
flutter mvvm 패턴 정리 및 예제 (0) | 2024.07.22 |
flutter form 제출 및 라우터 이동 (0) | 2024.07.22 |
flutter webview 사용방법 (0) | 2024.07.22 |