Framework/Flutter

[Flutter]무한 스크롤 구현하기

  • -
반응형

최근 Flutter로 모바일 게시판을 개발하게 됐는데 페이징 처리를 고민하다가 무한 스크롤 방식으로 결정했습니다. 패키지 중에 infinite_scroll_pagination이 문서도 잘 나와있고 사용하기도 편했습니다.

infinite_scroll_pagination을 적용한 앱(농실농실 보러가기) 화면 중 공지사항 페이지를 예제로 소개합니다. 실제 개발중인 프로젝트의 코드를 가져왔기 때문에 중간중간 import가 불가능한 코드들이 존재합니다. 페이징 컨트롤러, 위젯의 생김새, _fetchPage 함수 부분들만 참고하시고 각자의 프로젝트에 맞게 적용해서 사용하시길 바랍니다.

 

개발 환경
  • Flutter 3.16.5
  • Dart 3.2.3
  • Spring boot 2.7.5

flutter pub add infinite_scroll_pagination

 

 

상태 변경이 가능한 위젯 NoticeList를 추가합니다.

프로젝트에서 Riverpod을 사용하고 있어 ConsumerStatefulWidget을 사용했지만 StatefulWidget을 사용해도 괜찮습니다.

class NoticeList extends ConsumerStatefulWidget { const NoticeList({ super.key, }); @override ConsumerState<NoticeList> createState() => _NoticeList(); } class _NoticeList extends ConsumerState<NoticeList> { final _perPage = 15; final PagingController<int, Notice> _noticeController = PagingController(firstPageKey: 1); Future<void> _fetchPage(int page) async { try { List<Notice> noticeList = await NoticeService().getNoticeList(page: page, perPage: _perPage); final isLastPage = noticeList.length != _perPage; if (isLastPage) { _noticeController.appendLastPage(noticeList); } else { final nextPage = page + 1; _noticeController.appendPage(noticeList, nextPage); } } catch (e) { _noticeController.error = e; } } @override void initState() { _noticeController.addPageRequestListener((pageKey) { _fetchPage(pageKey); }); super.initState(); } @override void dispose() { _noticeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final screen = MediaQuery.of(context).size; return Scaffold( backgroundColor: Colors.white, appBar: BoardHeader( height: screen.height, ), body: Container( padding: EdgeInsets.symmetric(horizontal: screen.width * p15), child: PagedListView<int, Notice>( pagingController: _noticeController, builderDelegate: PagedChildBuilderDelegate<Notice>( firstPageProgressIndicatorBuilder: (_) => MyLoading(color: widgetMainColor), newPageProgressIndicatorBuilder: (_) => MyLoading(color: widgetMainColor), noItemsFoundIndicatorBuilder: (context) => const Center( child: EmptyBox( emptyText: '등록된 공지사항이 없습니다.', ), ), itemBuilder: (context, item, index) { Notice notice = item; return NoticeItem(notice: notice); }), ), )); } }

 

PagingController를 통해 스크롤이 마지막에 도달할 때 자동으로 페이징을 처리할 수 있게 됩니다.

_noticeController_fetchPage 함수를 등록하고 페이지 번호를 받아 공지사항 목록을 가져오게 됩니다.

final PagingController<int, Notice> _noticeController = PagingController(firstPageKey: 1); @override void initState() { _noticeController.addPageRequestListener((pageKey) { _fetchPage(pageKey); }); super.initState(); }

 

공지사항은 데이터를 15개씩 가져오도록 했습니다.(_perPage : 15) NoticeService().getNoticeList를 통해 가져온 공지사항 리스트는 _noticeController에 추가됩니다. 이 때, 데이터 개수가 15보다 작으면 마지막 페이지로 간주하고 그게 아니라면 다음 페이지 번호를 설정합니다. 

Future<void> _fetchPage(int page) async { try { List<Notice> noticeList = await NoticeService().getNoticeList(page: page, perPage: _perPage); final isLastPage = noticeList.length != _perPage; if (isLastPage) { _noticeController.appendLastPage(noticeList); } else { final nextPage = page + 1; _noticeController.appendPage(noticeList, nextPage); } } catch (e) { _noticeController.error = e; } }

 

리스트의 각 아이템을 정의할  NoticeItem 위젯을 추가합니다. NoticeItem은 ConsumerWidget(Stateless)을 상속받습니다.

class NoticeItem extends ConsumerWidget { final Notice notice; const NoticeItem({ super.key, required this.notice, }); @override Widget build(BuildContext context, WidgetRef ref) { final screen = MediaQuery.of(context).size; String createTime = FormatUtil.dateFormat(notice.createDate, 'time'); return AccordionHeaderItem( child: Padding( padding: EdgeInsets.symmetric( vertical: screen.width * p10, horizontal: screen.width * p15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyTextbox( textField: notice.noticeTitle, fontSize: screen.width * f16, fontWeight: FontWeight.w600, ), const SizedBox(height: 5.0), MyTextbox( textField: createTime, fontSize: screen.width * f12, fontWeight: FontWeight.w400, fontColor: grayFontColor, ), ], )), children: [ AccordionItem( child: Padding( padding: EdgeInsets.symmetric( vertical: screen.width * p15, horizontal: screen.width * p20), child: MyTextbox( textField: notice.noticeContent, fontSize: screen.width * f14, ), )), ], ); } }

 

여기에는 simple_accordion이 사용됐는데 아래와 같은 UI 개발시 유용합니다.

 

 

데이터를 조회하는 서비스를 등록합니다. 

class NoticeService { final noticeApi = '${dotenv.env['NONGSIL_API_URL']}/notice'; /// 공지사항 리스트 조회 Future<List<Notice>> getNoticeList({ required int page, required int perPage, }) async { List<Notice> resultList = []; try { final response = await ApiService().callApi( 'GET', '$noticeApi/list?page=$page&perPage=$perPage', {}, ); await Future.delayed(const Duration(milliseconds: 800)); if (response.statusCode == 200) { Map<String, dynamic> resultData = response.data; if (resultData['message'] == 'success') { List<dynamic> dataList = resultData['result']; if (dataList.isNotEmpty) { dataList.forEach((data) { resultList.add(Notice.fromMap(data)); }); } logger.d(':: 공지사항 리스트 조회 :: $dataList'); } else { throw Exception(":: 공지사항 리스트 조회 오류 :: $resultData"); } } } catch (e) { logger.e(e.toString()); } return resultList; } }

 

Notice 모델은 다음과 같습니다.

class Notice { final int noticeSeq; final String noticeTitle; final String noticeContent; final String createDate; Notice({ required this.noticeSeq, required this.noticeTitle, required this.noticeContent, required this.createDate, }); ...

 

데이터 연결 부분은 List 타입을 반환하게 하여 본인 프로젝트에 맞도록 수정해서 사용하시면 됩니다.

중간에 Future.delayed를 주는 것은 페이지 처리 중 보이는 로딩창이 보일 수 있도록 하기 위해서입니다. 데이터를 가져오는 시간이 오래 걸린다면 로딩창이 보이겠지만 보통은 아주 잠깐 보였다가 사라집니다.

firstPageProgressIndicatorBuilder: (_) => MyLoading(color: widgetMainColor), newPageProgressIndicatorBuilder: (_) => MyLoading(color: widgetMainColor),

 

 

공지사항 테이블에 데이터를 추가하고 테스트해보겠습니다. 한 페이지에 불러올 데이터는 15개입니다.

 

 

최초 공지사항 조회시 마지막 번호(NOTICE_SEQ)가 21로 시작하는 것이 보이며 15개가 조회됩니다.

 

아래로 스크롤하게 되면 다시 조회가 실행되고 반환된 번호는 6으로 확인되며, 페이징이 정상적으로 동작하는 것을 알 수 있습니다.

 

참고문서
 

infinite_scroll_pagination | Flutter package

Lazily load and display pages of items as the user scrolls down your screen.

pub.dev

반응형

포스팅 주소를 복사했습니다.

이 글이 도움이 되었다면 공감 부탁드립니다.