[046] 플러터 (Flutter) 배우기 - ListView.builder + 스크롤 Pagination 적용

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)),
),
),
],
),
);
}
}
view raw 046.dart hosted with ❤ by GitHub

 

References

 

https://www.kindacode.com/article/flutter-listview-pagination-load-more/

 

[유튜브 강좌 영상]

 

https://youtu.be/-0QokPwX3qA

 

반응형