[Flutter] ScrollController 사용해보기

leeeeeoy·2021년 9월 23일
3

이 글은 유튜브 강의와 블로그 자료를 보고 정리한 글입니다

ScrollContorller

ScrollContoller는 이름에서도 알 수 있듯이 스크롤이 가능한 위젯을 제어하는 클래스이다. 공식 사이트를 보면 일반적으로 ListView, GridView, CustomScrollView와 주로 함께 사용된다고 한다. 이 글에서는 ScrollController를 이용해 Infinite scroll과 스크롤 방향에 따라 BottomNavigationBar를 숨기는 동작을 구현해봤다.


기본속성

사실 스크롤 화면을 구성하면서 핵심적으로 사용한 속성은 position이다. position은 ScrollPosition을 알려주는데 이 값을 통해서 스크롤 방향이나 현재 위치 등을 알 수 있다.
이 글에서 사용한 속성들은 다음과 같다

  • position.pixels: 축 방향의 반대 방향으로 움직일 수 있는 픽셀 수 이다.
  • position.maxScrollExtent: 픽셀의 최대값이다 (스크롤 할 수 있는 최대 픽셀).
  • position.userScrollDirection: 사용자가 변경하려고 하는 방향이다.

즉 현재 스크롤 위치, 최대 스크롤 위치와 사용자가 스크롤하는 방향을 탐지해서 스크롤 동작에 따라 제어를 할 수 있다. 사실 이렇게만 보면 무슨 소린지 처음엔 잘 이해가 가지 않았다.
실제 코드를 작성해보면서 조금 더 정리해보자.

코드작성

1. Controller 작성

my_scroll_controller.dart

class MyScrollController extends GetxController {
  var scrollController = ScrollController().obs;
  var data = <int>[].obs;
  var isLoading = false.obs;
  var hasMore = false.obs;
  var isShow = true.obs;
}

GetxController를 이용해 스크롤을 제어할 Controller를 만들었다.

  • data: 스크롤 화면에 뿌려질 임시 데이터이다 (단순 숫자)
  • isLoading: 무한 스크롤에서 다음 데이터가 들어올 때 상태를 위한 변수이다.
  • hasMore: 들어올 데이터가 더 있는지에 대한 변수이다 (글에서는 50개로 제한했다).
  • isShow: 스크롤 방향에 따라 BottomNavigationBar를 조절하기 위한 변수이다.

기본적으로 일정 개수(10개)의 데이터를 화면에 출력하고 스크롤을 아래로 내리면 추가적으로 계속해서 데이터를 불러오도록 구현했다. 그리고 스크롤을 내리면 BottomNavigationBar가 사라지고 다시 스크롤을 올리면 나오도록 구현했다. 후에 동작예시를 보면 더 이해가 잘 될듯 싶다...

  
  void onInit() {
    _getData();

    scrollController.value.addListener(() {
      if (scrollController.value.position.pixels ==
              scrollController.value.position.maxScrollExtent &&
          hasMore.value) {
        _getData();
      }

      final direction = scrollController.value.position.userScrollDirection;
      if (direction == ScrollDirection.forward) {
        isShow.value = true;
      } else {
        isShow.value = false;
      }
    });

    super.onInit();
  }

  _getData() async {
    isLoading.value = true;

    await Future.delayed(Duration(seconds: 1));

    int offset = data.length;

    var appendData = List<int>.generate(10, (i) => i + 1 + offset);

    data.addAll(appendData);

    isLoading.value = false;

    hasMore.value = data.length < 50;
  }

먼저 getData 함수는 사용자가 데이터를 받아오는 함수이다. 예시 코드에서는 10개 단위로 계속해서 데이터를 생성하도록 작성했다. 실제로 통신을 한다고 가정하고 상태를 로딩중으로 바꾸고 약간의 딜레이를 주기 위해 1초 간격으로 딜레이를 주었다. 후에 현재 데이터 길이에 새로 생성된 10개의 데이터를 합치고 로딩 상태를 종료한다. 만약 데이터가 50개가 되면 더이상 불러오지 않도록 작성했다.

onInit 함수에서는 처음 먼저 Controller가 생성되면 데이터를 한 번 받아온다. 그 후 scrollContoller에 listener 함수를 등록해주는데 첫번째 조건은 사용자가 맨 밑까지 스크롤을 했을 때, 데이터의 개수가 50개가 되지 않는다면 데이터를 더 불러오는 로직을 작성했다. 2번째 조건은 사용자의 현재 스크롤 방향이 아래 방향이라면 isShow를 false로 바꾸어 BottomNavigationBar를 숨기고 다시 위로 올리면 나오는 로직을 작성했다.

2. Result Page 작성

my_scroll_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_study/pages/infinite_scroll/my_scroll_controller.dart';
import 'package:get/get.dart';

class MyScrollPage extends StatelessWidget {
  final controller = Get.put<MyScrollController>(MyScrollController());

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ScrollController'),
      ),
      body: Obx(
        () => Padding(
          padding: const EdgeInsets.all(10.0),
          child: ListView.separated(
            controller: controller.scrollController.value,
            separatorBuilder: (_, index) => Divider(),
            itemCount: controller.data.length + 1,
            itemBuilder: (_, index) {
              if (index < controller.data.length) {
                var datum = controller.data[index];
                return Material(
                  elevation: 10.0,
                  child: Container(
                    padding: const EdgeInsets.all(10.0),
                    color: Colors.lightBlue[100 * (index % 9)],
                    child: ListTile(
                      leading: Icon(Icons.android_sharp),
                      title: Text('$datum 번째 데이터'),
                      trailing: Icon(Icons.arrow_forward_outlined),
                    ),
                  ),
                );
              }
              if (controller.hasMore.value || controller.isLoading.value) {
                return Center(child: RefreshProgressIndicator());
              }
              return Container(
                padding: const EdgeInsets.all(10.0),
                child: Center(
                  child: Column(
                    children: [
                      Text('데이터의 마지막 입니다'),
                    ],
                  ),
                ),
              );
            },
          ),
        ),
      ),
      bottomNavigationBar: Obx(() => AnimatedContainer(
            decoration: BoxDecoration(
              color: Colors.lightBlue,
            ),
            curve: Curves.fastLinearToSlowEaseIn,
            duration: Duration(milliseconds: 200),
            height: controller.isShow.value ? 60 : 0,
            child: Container(
              child: Center(
                  child: Text(
                'BottomNavigationBar',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              )),
            ),
          )),
    );
  }
}

ListView와 BottomNavigationBar를 포함하는 페이지다. 각각 반응형 상태관리를 사용했기 때문에 Obx를 사용해서 관리를 해주었다. ListView에서는 데이터가 50개가 되면 마지막 데이터라는 표시를 해줬다. BottomNavigationBar의 경우 AnimatedContainer를 이용하여 스크롤 방향에 따라 높이가 변하도록 작성했다.

결과화면


스크롤을 아래로 내리면 데이터를 불러오고 BottomNavigationBar가 사라졌다가 다시 스크롤을 올리면 BottomNavigationBar가 올라오는 것을 볼 수 있다. 애뮬레이터로 촬영해서 움직임이 부드럽지가 않다.

정리

ScrollController를 이용해서 간단하게 스크롤 화면들을 제어해봤다. 사실 무한 스크롤을 구현하는 방법은 여러가지가 있지만 요즘 GetX를 계속 사용하고 있어서 GetX를 이용해서 간단한게 구현해봤다. 곧 진행하는 프로젝트에서 사용할 것 같아서 정리를 해봤는데 navigation_page에서 controller를 놓고 하위 page에서 조절하는 식으로도 구현이 가능할 것 같다. 그나저나 이미지 크기는 여전히 바뀌지 않는다...하


소스코드 https://github.com/leeeeeoy/flutter_personal_study/tree/master/lib/pages/infinite_scroll

참고자료

profile
100년 후엔 풀스택

1개의 댓글

comment-user-thumbnail
2023년 5월 19일

참고가 많이 됐습니다 감사합니다!

답글 달기