[Flutter] Riverpod 사용해보기 #2

leeeeeoy·2021년 10월 8일
3
post-custom-banner

Riverpod

Flutter의 패키지 중 하나로, GetX, Provider, BLoC처럼 상태관리를 위한 패키지이다. 쉽게 생각하면 Provider의 확장판(?) 정도로 생각할 수 있는 것 같다. 실제 Provider와 Riverpod의 개발자는 같은 사람인데, 공식 문서에 따르면 Riverpod에서는 Provider에서 발생한 여러가지 문제점을 해결했다고 한다. 다만 Provider를 완전히 대체하는 것은 아직 아니며, 실제 프로덕션 단계에서의 사용은 조금 주의를 기울여야 한다고 적혀있다.

저번글에서는 Riverpod의 기본적인 사용법에 대해 익혀봤다. 이번엔 Riverpod를 이용해서 infinite scroll을 구현해봤다. 사실 저번에 ScrollController 글에서도 언급했지만 infinite scroll을 구현하는 방식은 다양하다. 이 글에서는 infinite_scroll_pagination을 이용해서 riverpod와 같이 적용해봤다.


Packages 설정

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
    
  flutter_riverpod: 
  infinite_scroll_pagination:

riverpod와 infinite_scroll_pagination을 설치해준다.

코드 작성

user.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

class User {
  int id;
  String name;

  User({
    required this.name,
    required this.id,
  });
}

class UserState extends StateNotifier<List<User>> {
  UserState() : super([]);

  Future<List<User>> makeUser(int nextIndex) async {
    await Future.delayed(const Duration(seconds: 1));
    var newUsers = List.generate(20, (index) {
      var curIndex = index + nextIndex;
      return User(name: 'user $curIndex', id: curIndex);
    });
    state = [...state, ...newUsers];
    return newUsers;
  }
}

final userProvider =
    StateNotifierProvider<UserState, List<User>>((ref) => UserState());

간단하게 User 클래스와 Provierd로 사용할 UserState를 정의했다. 사실 사용하는 방법은 여러가지가 있겠지만 이 글에서는 같이 사용을 해본다는 목적으로 최대한 간단하게 작성했다.

state = [...state, ...newUsers];

이 부분은 해당 provider의 state를 업데이트 해주는 부분인데, 후에 state의 user의 갯수로 스크롤을 제한하기 위해 작성해주었다.


scroll_page.dart

class ScrollPage extends ConsumerStatefulWidget {
  const ScrollPage({Key? key}) : super(key: key);

  
  _ScrollPageState createState() => _ScrollPageState();
}

class _ScrollPageState extends ConsumerState<ScrollPage> {
  final PagingController<int, User> _pagingController = PagingController(
    firstPageKey: 0,
  );

  
  void initState() {
    _pagingController.addPageRequestListener((pageKey) {
      addUsers(pageKey);
    });

    super.initState();
  }

  Future<void> addUsers(int num) async {
    var newUsers = await ref.read(userProvider.notifier).makeUser(num);
    var last = ref.read(userProvider).length >= 100 ? true : false;
    try {
      if (last) {
        _pagingController.appendLastPage(newUsers);
      } else {
        final nextNum = num + 20;
        _pagingController.appendPage(newUsers, nextNum);
      }
    } catch (error) {
      _pagingController.error = error;
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Scroll'),
      ),
      body: PagedListView<int, User>(
        pagingController: _pagingController,
        builderDelegate: PagedChildBuilderDelegate<User>(
          itemBuilder: (context, item, index) {
            return ListTile(
              title: Text(
                item.name + ': ${item.id}',
                style: const TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
              onTap: () {},
            );
          },
        ),
      ),
    );
  }

  
  void dispose() {
    _pagingController.dispose();
    super.dispose();
  }
}

scroll을 보여주는 화면이다. 패키지 공식 사이트의 예제를 보고 참고해서 작성했는데 아마 기존에 다른 방식으로 infinite scroll을 구현했던 사람이라면 어렵지 않게 이해할 것 같다.

  • PagingController: scroll을 제어하는 Controller이다. pageKey 타입과 scroll data 타입을 지정할 수 있다. 그리고 생성하면서 예시 코드와 같이 첫번째로 동작할 pageKey를 넘겨줄 수 있다.
  • addPageRequestListener: Controller에 pageKey를 이용해서 실행시킬 함수를 listener에 정의할 수 있다.
  • appendPage: Controller에 data와 다음 pageKey를 입력한다.
  • appendLastPage: Controller에 마지막 data를 넣어준다. 함수 설명을 보니 pageKey는 null값이 들어가는 것 같다.

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Scroll'),
      ),
      body: PagedListView<int, User>(
        pagingController: _pagingController,
        builderDelegate: PagedChildBuilderDelegate<User>(
          itemBuilder: (context, item, index) {
            return ListTile(
              title: Text(
                item.name + ': ${item.id}',
                style: const TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
              onTap: () {},
            );
          },
        ),
      ),
    );
  }

scrollView 형태는 List, Grid, Sliver 다 가능하다. 예시 코드에서는 ListView 형태를 작성했다. controller와 builderDelegate를 인자로 받는데 기존의 ListView를 사용하는 형태와 비슷해서 크게 어렵진 않은 것 같다.

Future<void> addUsers(int num) async {
    var newUsers = await ref.read(userProvider.notifier).makeUser(num);
    var last = ref.read(userProvider).length >= 100 ? true : false;
    try {
      if (last) {
        _pagingController.appendLastPage(newUsers);
      } else {
        final nextNum = num + 20;
        _pagingController.appendPage(newUsers, nextNum);
      }
    } catch (error) {
      _pagingController.error = error;
    }
  }

controller에는 위에 provider로 정의한 List를 넣어주었다. 최대 list 갯수는 100개로 제한하였고, 한번에 20개씩 페이징 했다.

결과



정리

간단한게 Riverpod를 사용해서 infinite scroll을 구현해봤다. (사실 작성을 하고 보니 이렇게 사용하는게 썩 좋아보이진 않았다.) Github 소스들을 조금 뒤지다 보니 Stream을 이용한 코드도 있었고, 저번처럼 ScrollController를 이용해서도 구현이 가능한데, 사실 어느 것이 더 나은 건지는 상황에 따라 조금씩 다를 것 같다. 계속해서 Riverpod를 공부하고 있는데 공부하다보니 확실히 GetX가 쓰기 편한건 부정할 수 없는 것 같다. 다만 Riverpod도 GetX와는 조금 다른 점에서 매력적인 부분들이 있는 것 같다 (조금 익숙해지면 작성할 코드도 적어지고 provider간의 결합도 익숙해질 것 같다.)

그리고 Riverpod를 사용하다보니 느낀점인데, freezed와 잘 어울리는 것 같다. freezed는 기본적으로 여러 메소드를 정의해주기도 하고(특히 copyWith), 불변성을 기본으로 하기 때문에 state를 바꿔야 하는 Riverpod와 제법 잘 어울리는 것 같다!!!


소스코드 https://github.com/leeeeeoy/riverpod-flutter/tree/master/lib/riverpod/topics/infinite_scroll


참고자료

profile
100년 후엔 풀스택
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 12월 31일

정성글 잘 봤습니다^^
마지막에 Freezed와 잘 어울릴 것 같다고 하셨는데
마침 둘 다 같은 개발자가 작성한 라이브러리더라구요 ㅎㅎ

1개의 답글