flutter 에는 Bloc, Provider, getX, redux 등 여러가지 상태 관리 라이브러리 등이 있다. 그 중에서 flutter 공식 홈페이지에서 추천하는 것은 Provider 이다. 구글에서 생각하길 여러가지 라이브러리중에 상태 관리 철학에 가장 잘 부합하고 믿을 만한 라이브러라는 반증이라고 생각한다.
riverpod 은 Provider 를 개발한 Remi Rousselet 라는 개발자가 Provier 를 한 단계 더 발전 시켜서 만든 라이브러리라고 한다.
현재도 계속 발전중이고 자체 공식 홈페이지도 제작한 걸 보면 Remi 가 얼마나 공을 들이는지 알 수 있다. (공식 홈페이지에서는 친절하게 한국어도 지원한다.)
이러한 이유들로 riverpod 에 관심을 가지게 되었고 개인적으로 진행하는 프로젝트에 riverpod 을 도입하기로 했다.
참고로 riverpod 이름의 유래는 Provider 에서 P,O,D 글자를 뒤로 뺀 형태라고 한다.
Riverpod 은 여러종류의 패키지가 있다. 각 패키지마다 사용 목적이 다르며, 어떤 패키지를 설치 할지는 만드는 앱의 형태에 따라 다르다.
이중 흥미로운건 hooks_riverpod 이다. 현재 회사에서 react-native 를 사용하는 입자에서 hook 을 통한 개발이 얼마나 간편한지를 체험한지라 기회가 된다면 flutter 에서도 hook 을 이용한 개발을 추천한다.
물론 같이 일하는 팀원들이 hook 에 얼마나 익숙한지와 hook 을 이용한 방식이 얼마나 flutter 와 잘 맞는지는 고려해야 하는 사항이다. 그리고 flutter 를 처음 배우는 입장에서는 표준의 방식을 먼저 익히고 사용하는 것을 추천하기에 이 글에서는 flutter_riverpod
을 이용한 방식을 설명하겠다.
설치 방식은 터미널에서 아래 명령어를 입력하면 된다.
flutter pub add flutter_riverpod
또는 pubspec.yaml 파일에 아래 내용을 추가 하고 pub get 을 해주자
dependencies:
flutter_riverpod: [latest version]
provider 는 riverpod 에서 아주 중요한 파트이다.
A provider is an object that encapsulates a piece of state and allows listening to that state.
프로바이더는 하나의 상태조각의 압축(encapsulates)된 객체이자 상태의 변화를 감시하는 역할을 가지고 있습니다.
음.. 무슨 말인지 정확히 알기 어렵다. 일단은 그렇구나.. 라고 이해하고 예를 들면서 알아보자.
우선 provider 가 동작하기 위해서는 ProviderScope 를 앱의 가장 최상단 root 부모 위젯으로 감싸줘야 한다.
void main() {
runApp(ProviderScope(child: MyApp()));
}
가장 보편적인 counter 앱을 만들기 위한 counter provider 를 이용한다고 해보자
final counterProvider = StateProvider<int>((ref) {
return 0;
});
무슨 말인지 정확히는 모르겠지만 일단 0 을 반환하는 StateProvider 라는 놈을 변수 counterProvider 라고 정의 하였다.
counterProvider 는 상태 0 값을 가지는 counter 의 provider 이다. 이제 이 counter 를 사용하기 위해서는 앱 어디에서든 counterProvider 를 가지고 오면 counter 상태를 알 수 있는 것이다.
위에서 말한 상태조각의 압축된 객체 라는 말은 이것을 의미한다.
그럼 위에서 만든 provider 를 어떻게 사용할 수 있을까?
일단 이것도 예제를 먼저 보면서 이야기 해보자
class HomeView extends ConsumerWidget {
const HomeView({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
// ref를 사용해 프로바이더 구독(listen)하기
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
예제에서 보면 ConsumerWidget
이라는 Widget 을 상속 받아서 widget 을 만들었다. CosumerWidget 은 statefulWidget 을 상속받아서 만들어진 커스텀 widget 으로, 차이점은 build 메소드에서 WidgetRef
를 파라미터로 받아서 사용할 수 있다는 것이다.
그리고 이 WidgetRef 는 provider 를 참조할 수 있도록 도와주는 객체이다.
그런데 저 ref 라는 놈은 provider 를 선언할 때도 있지 않았던가??
그렇다. ref 객체를 얻을 수 있는 방법은 크게 2가지 방법이 있다.
모든 provider 는 ref
객체를 파라미터로 받게 되어 있다.
그리고 이 매개변수는 또 다른 provider 를 참조하는데 사용해도 된다.
final provider = Provider((ref) {
// 다른 프로바이더 객체를 얻기위해 ref를 사용
// 여기서 repositoryProvider 프로바이더를 Provider 에서 읽는 것을 확인
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
또는, StateNotifier 로 전달해서 사용할 수 있다.
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});
class Counter extends StateNotifier<int> {
Counter(this.ref) : super(0);
final Ref ref;
void increment() {
// Counter 클래스는 다른 프로바이더를 읽기 위해 "ref"를 사용할 수 있다.
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
riverpod 에서는 기존의 widget 들을 이용해서 ref 객체를 전달할 수 있도록 새로운 widget 들을 만들어서 제공한다.
StatelessWidget 대신 ConsumerWidget 상속
위에 예시를 든거와 같이 StatelessWidget 대신 ConsumerWidget
을 이용하는 것이다.
StatefullWidget+State 대신 ConsumerStateFulWidget+ConsumerState 상속
ConsumerWidget 과 동일하게 ConsumerStateFulWidget+ConsumerState
도 동일한 방법에 ref 객체를 사용할 수 있다는 것이다.
대신 ref 객체가 파라미터로 전달되는것이 아니라, ConsumerState 객체 속성이라는 점이다.
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}) : super(key: key);
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
void initState() {
super.initState();
// "ref"는 StatefulWidget의 모든 생명주기 상에서 사용할 수 있다.
ref.read(counterProvider);
}
Widget build(BuildContext context) {
// "ref"는 build 메소드 안에서 프로바이더를 구독(listen)하기위해 사용할 수 있다.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
이제 ref
객체의 주요 사용법을 알아보자. ref 는 크게 3가지 주용한 용도가 있다.
provider 의 값을 얻어서 변화를 모니터링 할 때 사용한다. 값이 변경되면, widget 을 rebuild 하거나 값을 구독(subscribed) 하고 있는 위치에 상태 값을 전달한다.
상태 변화시 rebuild 하기 때문에 성능 이슈가 발생 할 수 있지만, 변화를 화면에 즉각 반영해야 할 때 사용한다.
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
// ref를 사용하여 프로바이더 구독하기.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
watch 와 동일하게 변화를 모니터링 하지만 widget 을 rebuild 하거나 다른곳에 상태값을 전달하지 않는다. 상태 변경이 있을 때, 커스텀한 어떤 행위를 취해야 할 경우 사용한다.
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Value is ${current.state}')),
);
});
return Scaffold(...);
}
}
위와 같이 상태 변화시 SnackBar 를 호출 할 때 사용할 수 있다.
어떤 부가적인 효과 없이 provider 의 상태 값을 가지고 올 때 사용합니다. read 는 일반적으로 사용자 상호작용으로 발생가능한 트리거 함수내부에서 주로 사용합니다.
주로 provider 내부의 callback 함수에 접근할 때 사용한다.
final counterProvider =
StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// `Counter`클래스의 increment() 메소드를 호출합니다.
ref.read(counterProvider.notifier).increment();
},
),
);
}
}