반응형
이 글에선 Riverpod의 사용법을 정리하지 않고 각 Provider의 특징과 Riverpod의 특징에 대해서만 이야기 합니다.
플루터를 공부한다면 상태관리란 이야기를 많이 들어 보셨을 겁니다
- 상태관리 (State Management)
- 앱을 시작하고 사용자가 화면의 콘텐츠를 변경하고나 보고 있다면 이는 모두 앱의 현태 상태입니다.
- 상태는 화면을 구성하고, 사용자의 동작에 따라 데이터가 변경하는 것들.. 모두 상태입니다.
- 이러한 상태를 쉽게 관리해주게 Riverpod입니다.
- 상태관리의 원래
- 현재의 상태가 변경이 되면 그에따른 데이터가 변경을 하게됩니다.
- 데이터가 변경이 되면 변경된 데이터를 바인딩하고 있는 UI가 데이터에 따라 변경이 되게 됩니다.
- 즉, 상태가 변경이 되면 플루터의 Build함수가 다시 불리워 집니다.
- 그럼 왜 Riverpod인가
- 현재 많이 사용되고 있는 상태관리 라이브러리중 Riverpod과 비교해 보겠습니다.
GetX
- 쉽다
- 여러기능이 마구잡이로 있어서 좀 정리가 안된 느낌 (그래서 별로 안좋아함)
- 사용하다보면 플루터의 기본을 잘 지키지 않는다는 생각이 들어서 잘 사용안하게 된다.
BLoC
- 비지니스 로직에서 UI를 분리하기 쉽다.
- Stream을 이용해야 한다면 강력한 라이브러리다.
- UI에서 발생하는 이벤트를 감지한다
- Stream을 이용하기 때문에 러닝커브가 있는 편
Provider
- Flutter 공식 상태관리 라이브러리
- 플루터와 가장 잘 어울리는 라이브러리다
Riverpod
- Provider의 상위호환 라이브러리
- 배우기 간편하고 강력하다. Providerd의 장점을 잘 수용하고 있다.
- Provider라는 기본 타입으로 여러가지 생성자 주입을 간편하게 할 수있다.(각 기본 클래스를 Provider로 만들면 싱글톤이 된다.)
- 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 마이그레이션 용도)
- ref.watch, ref.read
- Riverpod을 사용하면 WidgetRef를 Build에서 활용해야 합니다.
- 필드함수에서 WidgetRef의 ref에 watch와 read를 잘 사용할 줄알아야 합니다.
- ref.watch는 반환값의 업데이트가 있을때 지속적으로 build함수를 다시 실행해 준다.
- ref.watch는 필수적으로 UI관련 코드에만 사용한다.
- ref.read는 실행되는순간 단 한번만 provider값을 가져온다.
- ref.read는 onPressed콜백처럼 특정 액션뒤에 실행되는 함수 내부에서 사용된다.
- 그외 기능
- 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 쉬워질겁니다.
반응형
댓글