반응형

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 리스트에 추가하고, 페이지 수를 증가시킵니다.

더 이상 가져올 데이터가 없을 경우 _hasMoreDatafalse로 설정합니다.

 

다른 예제(실습)

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 Documentation - ScrollController

Flutter HTTP Package

반응형

+ Recent posts