2023. 3. 30. 16:11ㆍ모바일어플개발/Flutter
안녕하세요~ totally 개발자입니다.
ListView.builder + Pagination
이번에는 ListView.builder를 사용하여 스크롤을 내렸을 때 다음 페이지, 추가 내용을 가져와서 보여주는 스크롤 페이지네이션(Pagination) 구현 방법에 대해서 살펴보도록 하겠습니다. 보통 RestAPI를 사용하여 데이터를 가져올 때 한 번에 많은 데이터를 가지고 오는 경우는 드문데 그 이유는 한 번에 많은 데이터를 가져오게 되면 로딩 시간도 오래 걸리고 한 화면에 어차피 다 보여지지 못할 데이터들을 굳이 한 번에 가져올 필요는 없기 때문입니다. 그래서 page와 limit을 parameter로 추가하여 데이터를 가지고 오는 경우가 많은데 간단한 방법으로 이것을 구현해보는 실습을 해보도록 하겠습니다.
Step 1: pubspec.yaml에 http 패키지를 설치합니다.

Step 2: main.dart에 다음처럼 import 해줍니다. (이 실습에서는 모든 코드를 main.dart에 작성하여 실습하며 이 모든 코드는 맨 아래에 첨부했습니다)

Step 3: 다음처럼 변수와 initState를 작성해줍니다. 여기서 _limit은 한 번에 가져올 데이터의 숫자이며, _hasNextPage는 다음 페이지가 있는지(추가 데이터가 있는지 여부), _isFirstLoadRunning은 처음으로 데이터 로딩, isLoadMoreRunning은 추가적인 데이터 로딩, _albumList에 데이터를 누적해서 추가해줄 변수입니다. 그리고 스크롤을 조절하는데 필요한 컨트롤러 변수까지 선언해줍니다. 그리고 initState에는 _initLoad()와 컨트롤러에 ..addListener로 리스너를 달아줍니다. 아직은 에러가 나오지만 밑에서 바로 추가할 것이기 때문에 입력해주면 되겠습니다.

Step 4: _initLoad 메소드를 작성해줍니다. 여기에서 주목할 포인트는 먼저 _isFirstLoadRunning을 true로 바꾸어 현재 로딩중이라는 상태로 변경하고 그 다음에 데이터를 가지고 와서 _albumList에 반영해줍니다. 그리고 다 되었다면 _isFirstLoadRunning을 false로 변경하여 로딩이 모두 마무리되었음을 표시합니다.

Step 5: _nextLoad() 메소드를 작성해줍니다. 여기에서 _controller.position.extendAfter < 100가 들어가는데 이거는 쉽게 이야기하여 현재 스크롤 할 수 있는 남은 픽셀 부분이 100 미만이라면 뜻이며 200이나 300으로 하셔도 됩니다. 이 extendAfter는 영어 설명으로 이렇게 되어 있습니다. The quantity of content conceptually "below" the viewport in the scrollable. This is the content below the content described by extentInside.
https://api.flutter.dev/flutter/widgets/ScrollPosition-class.html 참고하시기 바랍니다.

Step 6: dispose 메소드도 추가하여 더 이상 사용되지 않으면 removeListener()해줍니다.

Step 7: Widget build 부분입니다. 여기에 108번째처럼 먼저 처음 로딩중인지를 확인하여 로딩 중이라면 로딩 중 위젯을 표시하고 로딩이 다 되었다면 ListView.builder로 해당 불러온 데이터 부분을 보여주면 됩니다. 그리고 추가적으로 로딩 중이라는 변수가 true라면 CircularProgressIndicator 사용해주면 되며 더 이상 불러올 데이터가 없는 경우, 텍스트를 사용해서 이를 알려주면 됩니다.

Step 8: 아래처럼 결과가 나오면 성공입니다.

[전체 소스 코드]
import 'package:flutter/material.dart'; | |
import 'dart:convert'; | |
import 'package:http/http.dart' as http; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return const MaterialApp(title: 'Flutter App', home: HomePage()); | |
} | |
} | |
class HomePage extends StatefulWidget { | |
const HomePage({Key? key}) : super(key: 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; | |
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 = json.decode(res.body); | |
}); | |
} catch (e) { | |
print(e.toString()); | |
} | |
setState(() { | |
_isFirstLoadRunning = false; | |
}); | |
} | |
void _nextLoad() async { | |
if (_hasNextPage && | |
!_isFirstLoadRunning && | |
!_isLoadMoreRunning && | |
_controller.position.extentAfter < 100) { | |
setState(() { | |
_isLoadMoreRunning = true; | |
}); | |
_page += 1; | |
try { | |
final res = | |
await http.get(Uri.parse("$_url?_page=$_page&_limit=$_limit")); | |
final List fetchedPosts = json.decode(res.body); | |
if (fetchedPosts.isNotEmpty) { | |
setState(() { | |
_albumList.addAll(fetchedPosts); | |
}); | |
} else { | |
// This means there is no more data | |
// and therefore, we will not send another GET request | |
setState(() { | |
_hasNextPage = false; | |
}); | |
} | |
} catch (e) { | |
print(e.toString()); | |
} | |
setState(() { | |
_isLoadMoreRunning = false; | |
}); | |
} | |
} | |
@override | |
void dispose() { | |
_controller.removeListener(_nextLoad); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('ListView Pagination'), | |
), | |
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), | |
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), | |
color: Colors.blue, | |
child: const Center( | |
child: Text('No more data to be fetched.', | |
style: TextStyle(color: Colors.white)), | |
), | |
), | |
], | |
), | |
); | |
} | |
} |
References:
https://www.kindacode.com/article/flutter-listview-pagination-load-more/
[유튜브 강좌 영상]
'모바일어플개발 > Flutter' 카테고리의 다른 글
[048] 플러터 (Flutter) 배우기 - Factory Pattern (팩토리 패턴) 이해하기 (0) | 2023.04.03 |
---|---|
[047] 플러터 (Flutter) 배우기 - Carousel Slider + Indicator (자동 슬라이더 + 인디케이터 구현) (2) | 2023.04.01 |
[045] 플러터 (Flutter) 배우기 - Widget Test (위젯 테스트) (0) | 2023.03.21 |
[044] 플러터 (Flutter) 배우기 - Unit Test (단위 테스트) (0) | 2023.03.21 |
[043] 플러터 (Flutter) 배우기 - MVVM 아키텍처 패턴 적용 (10) | 2023.03.21 |