본문 바로가기
[개발] 이야기/[Flutter] 이야기

flutter 상태관리 중 가강 강력하고 쉬운 라이브러리 Riverpod - a to z

by 헤이나우
반응형

 

이 글에선 Riverpod의 사용법을 정리하지 않고 각 Provider의 특징과 Riverpod의 특징에 대해서만 이야기 합니다.

플루터를 공부한다면 상태관리란 이야기를 많이 들어 보셨을 겁니다

  • 상태관리 (State Management)
    • 앱을 시작하고 사용자가 화면의 콘텐츠를 변경하고나 보고 있다면 이는 모두 앱의 현태 상태입니다.
    • 상태는 화면을 구성하고, 사용자의 동작에 따라 데이터가 변경하는 것들.. 모두 상태입니다.
    • 이러한 상태를 쉽게 관리해주게 Riverpod입니다.
  • 상태관리의 원래
    • 현재의 상태가 변경이 되면 그에따른 데이터가 변경을 하게됩니다.
    • 데이터가 변경이 되면 변경된 데이터를 바인딩하고 있는 UI가 데이터에 따라 변경이 되게 됩니다.
    • 즉, 상태가 변경이 되면 플루터의 Build함수가 다시 불리워 집니다.
  1. 그럼 왜 Riverpod인가
    1. 현재 많이 사용되고 있는 상태관리 라이브러리중 Riverpod과 비교해 보겠습니다.

GetX

  • 쉽다
  • 여러기능이 마구잡이로 있어서 좀 정리가 안된 느낌 (그래서 별로 안좋아함)
  • 사용하다보면 플루터의 기본을 잘 지키지 않는다는 생각이 들어서 잘 사용안하게 된다.

BLoC

  • 비지니스 로직에서 UI를 분리하기 쉽다.
  • Stream을 이용해야 한다면 강력한 라이브러리다.
  • UI에서 발생하는 이벤트를 감지한다
  • Stream을 이용하기 때문에 러닝커브가 있는 편

Provider

  • Flutter 공식 상태관리 라이브러리
  • 플루터와 가장 잘 어울리는 라이브러리다

Riverpod

  • Provider의 상위호환 라이브러리
  • 배우기 간편하고 강력하다. Providerd의 장점을 잘 수용하고 있다.
  • Provider라는 기본 타입으로 여러가지 생성자 주입을 간편하게 할 수있다.(각 기본 클래스를 Provider로 만들면 싱글톤이 된다.)
  1. Riverpod의 Provider 종류
  • 모든 Provider는 글로벌하게 선언한다.

Provider아무 타입이나 반환 가능, 싱글톤 객체를 만들때 사용

StateProvider 단순한 형태의 데이터만 관리(int, double, String)
StateNotifierProvider 가장 많이 사용됨, 클래스를 상태관리 할때 사용한다.
FutureProvider Future타입만 사용함
StreamProvider Stream타입만 반환 가능
CangeNotifierProvider 사용안함 Provider 마이그레이션 용도
  • Provider
    • read만 가능하다
    • 가장 기본 베이스가 되는 Provider
    • 아무 타입이나 반환가능
    • Service, 계산한 값등을 반환할때 사용한다.
    • 반환값을 캐싱할떄 유용하게 사용된다.
      • 빌드 횟수 최소화 가능
    • 여러 Provider의 값들을 묶어서 한번에 반환값을 만들어낼 수 있다.
    • 여러개의 Provider들을 묶어서 한곳에서 관리할 수 있다.
    final stateIntProvider = StateProvider<int>((ref) {
      return 0;
    });
    final stateNotifierProvider =
        StateNotifierProvider<ShoppingProvider, List<Shopping>>(
            (ref) => ShoppingProvider());
    //예를 들어 들어온 리스트 중에 필터 조건 등등
    final provider = Provider<List<Shopping>>((ref) {
      ref.watch(stateIntProvider);
      ref.watch(stateNotifierProvider);
      print("provider");
      return ref.read(stateNotifierProvider.notifier).state;
    });
    
  • StateProvider
    • UI에서 “직접적으로” 데이터를 변경할 수 있도록 하고 싶을때 사용
    • 단순한 형태의 데이터만 관리(int, double, String등)
    • Map, List등 복잡한 형태의 데이터는 다루지 않음 (가능은 함)
      • number++ 정도의 간단한 로직으로만 한정
    final stateIntProvider = StateProvider<int>((ref) {
      return 0;
    }); 
    
    //read
    final provider = ref.watch(numberProvider);
    // update 하는 방법
    ref.read(numberProvider.notifier).update((state) => state + 1);
    ref.read(numberProvider.notifier).state -= 1;
    
  • StateNotifierProvider
    • StateProvider와 마찬가지로 UI에서 “직접적으로” 데이터를 변경할 수 있도록 하고 싶을 떄 사용
    • 복잡한 형태의 데이터 관리 가능(클래스의 메소드를 이용한 상태관리)
    • StateNotifier를 상속한 클래스 반환
    final stateNotifierProvider =
        StateNotifierProvider<ShoppingProvider, List<Shopping>>(
            (ref) => ShoppingProvider());
    class ShoppingProvider extends StateNotifier<List<Shopping>> {
      ShoppingProvider() : super([]);
      addItem(Shopping item) {
        state = [...state, item];
      }
    
      toggle(List<String> name) {
        state = state.map<Shopping>((e) {
          if (name.contains(e.name)) {
            return e.copyWith(isSpicy: false);
          }
          return e;
        }).toList();
      }
    }
    
    class Shopping {
      final String name;
      final int count;
      final bool hasBought;
      final bool isSpicy;
      Shopping({
        required this.name,
        required this.count,
        required this.hasBought,
        required this.isSpicy,
      });
      copyWith({
        String? name,
        int? count,
        bool? hasBought,
        bool? isSpicy,
      }) {
        return Shopping(
            name: name ?? this.name,
            count: count ?? this.count,
            hasBought: hasBought ?? this.hasBought,
            isSpicy: isSpicy ?? this.isSpicy);
      }
    
      @override
      String toString() {
        return "$name, $count, $hasBought, $isSpicy\\n";
      }
    }
    
  • FutureProvider
    • Future타입만 반환 가능
    • API요청의 결과를 반활할때 자주 사용
    • 복잡한 로직 또는 사용자의 특정 행동뒤에 Future를 재실행하는 기능이 없음
      • 필요할경우 StateNotifierProvider사용
    final multiplesFutureProvider = FutureProvider<List<int>>((ref) async {
      await Future.delayed(Duration(seconds: 2));
      return [
        1,
        2,
        3,
        4,
        5,
      ];
    });
    
    state.when(
                data: (data) {
                  return Text(
                    data.toString(),
                    textAlign: TextAlign.center,
                  );
                },
                error: (error, stack) {
                  return Text(error.toString());
                },
                loading: () => Center(
                  child: CircularProgressIndicator(),
                ),
              )
    
  • StreamProvider
    • Stream타입만 반환가능
    • API요청의 결과를 Stream으로 반환할때 자주 사용
      • Socket등
    final streamFutureProvider = StreamProvider<int>((ref) async* {
      for (var i = 0; i < 10; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i;
      }
    });
    
    state.when(
                data: (data) {
                  return Text(
                    data.toString(),
                    textAlign: TextAlign.center,
                  );
                },
                error: (error, stack) {
                  return Text(error.toString());
                },
                loading: () => Center(
                  child: CircularProgressIndicator(),
                ),
              )
    
  • ChangeNotifierProvider(사용안함 Provider 마이그레이션 용도)
  1. ref.watch, ref.read
    • Riverpod을 사용하면 WidgetRef를 Build에서 활용해야 합니다.
    • 필드함수에서 WidgetRef의 ref에 watch와 read를 잘 사용할 줄알아야 합니다.
    • ref.watch는 반환값의 업데이트가 있을때 지속적으로 build함수를 다시 실행해 준다.
    • ref.watch는 필수적으로 UI관련 코드에만 사용한다.
    • ref.read는 실행되는순간 단 한번만 provider값을 가져온다.
    • ref.read는 onPressed콜백처럼 특정 액션뒤에 실행되는 함수 내부에서 사용된다.
  2. 그외 기능
  • family
    • watch할때 인수를 넘겨줄 수 있다 모든 프로바이더에 적용 가능
    • 넘겨지는 인수별로 메모리 할당이 다르게 지정이 된다.
    final familyModifierProvider =
        FutureProvider.family<List<int>, int>((ref, data) async {
      await Future.delayed(Duration(seconds: 2));
      return List.generate(5, (index) => index * data);
    });
    
  • autoDispose
    • provider.autoDispose를 하면 해당 프로바이더가 사용되지 않으면 메모리에서 제거한다.
  • listenProvider
    • 빌드 할떄 리슨하고 있으면 변경전과 후 값을 받을 수 있따.
    final state = ref.listen(listenProvider, (previous, next) {
          print("priv : $previous, next : $next");
          tabController.animateTo(next);
        });
    
  • select
    • 특정 값만 select해서 watch할 수 있다.
    • 최적화할때 사용한다.
    final state = ref.watch(selectProvider.select((value) => value.isSpicy));
    

여기서 정리한 내용말고도 코드에 어노테이션을 붙여서 코드를 자동으로 생성하게 해주는 기능이 업데이트 되었는데 개인적으론 좀 수고스럽더라도 일일이 코드를 작성해서 만드는 것이 더 좋은거 같다.

여기까지 Riverpod의 개념들을 정리해 보았는데 저도 많이 사용하기 전까지는 개념들이 복잡했지만 몇번 사용하다 보니 아주 쉬웠습니다. 해당글을 몇번 읽다보면 감이 잡히면서 Riverpod 쉬워질겁니다.

반응형

댓글