프로바이더 읽기
본 가이드를 읽기전에 프로바이더란에 대해서 먼저 알아보는 것을 추천합니다.
이 가이드 문서는 프로바이더를 어떻게 사용/소비(consume)하는지에 대해 알아봅니다.
"ref" 객체 얻기
프로바이더를 읽기전 맨 처음 "ref" 라는 객체를 얻을 필요가 있습니다.
이 객체('ref')는 프로바이더간 상호작용을 도와주고 위젯이나 다른 프로바이더에서 얻을 수 있습니다.
프로바이더로 부터 "ref" 객체 얻기
모든 프로바이더들은 "ref"객체를 파라미터로서 받게 됩니다.
final provider = Provider((ref) {
// 다른 프로바이더 객체를 얻기위해 ref를 사용합니다.
// 여기서 repositoryProvider 프로바이더를 Provider 에서 읽는 것을 확인합니다.
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
이 매개변수는 프로바이더가 노출한 값으로 전달하는 것이 안전합니다.
예를 들어, 통상적으로 프로바이더의 "ref"를 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('...');
}
}
StateNotifier를 상속 받은 Counter
클래스는 ref
객체를 통해
다른 프로바이더를 사용하거나 읽을 수 있게 되었습니다.
위젯에서 "ref" 객체 얻기
위젯들(Widgets)은 "ref" 파라미터를 가지고 있지 않습니다. 그러나, Riverpod에서는 위젯에서 "ref" 객체를 얻기위한 다양한 솔루션을 제공합니다.
StatelessWidget 대신 ConsumerWidget으로 상속받기
가장 일반적인 해결방법은 StatelessWidget를 ConsumerWidget의로 바꿔서 위젯을 생성하는 것입니다. (여기서 말하고자하는 해결방법이란 위에서 언급한 위젯에서 "ref" 객체를 얻는 방법입니다.)
ConsumerWidget은 StatelessWidget와 기본적으로 동일합니다. 차이점은 build 메소드에서 추가적으로 "ref" 객체를 받는 것입니다.
전형적인 ConsumerWidget 사용방법은 아래 예시 코드와 같습니다.
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// ref를 사용해 프로바이더 구독(listen)하기
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
StatefulWidget+State 대신에 ConsumerStatefulWidget+ConsumerState 상속받기
ConsumerWidget과 유사하게, ConsumerStatefulWidget과 ConsumerState는 StatefulWidget과 State와 동일합니다. 차이점은 "ref" 객체를 상태로 가진다는 점 입니다.
이번에는 "ref"객체는 build 메소드의 파라미터로서 전달되지 않습니다. "ref" 객체는 ConsumerState객체의 속성이 됩니다.
아래의 예시 코드를 확인해 봅시다.
class HomeView extends ConsumerStatefulWidget {
const HomeView({super.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');
}
}
HookWidget 대신 HookConsumerWidget 상속받기
이 방법은 flutter_hooks를 사용하는 사용자에게 한정됩니다. flutter_hooks은 HookWidget을 상속받는게 필요하기 때문에 hooks을 사용하기위해 위젯에서 ConsumerWidget을 상속하는것은 불가능합니다.
하나의 해결 방안으로 hooks_riverpod 패키지를 사용하면 됩니다. 이 패키지는 HookWidget를 HookConsumerWidget으로 대체할 수 있는 해결방안을 제공합니다.
HookConsumerWidget은 ConsumerWidget과 HookWidget의 기능을 포함하고 있습니다. 즉, 프로바이더를 구독하는것과 hooks을 사용하는 것이 모두 가능합니다.
예제를 확인해 봅시다.
class HomeView extends HookConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget은 build 메소드 안에서 hooks을 사용할 수 있도록 도와줍니다.
final state = useState(0);
// 프로바이더를 사용/구독하기 위해서 ref 매개변수도 사용할 수 있습니다.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
Consumer 와 HookConsumer 위젯
위젯(widgets)안에서 "ref" 객체를 얻기 위한 마지막 방안으로 Consumer/HookConsumer 위젯을 사용하는 것이 있습니다.
이 클래스들은(Consumer/HookConsumer) "ref"를 얻기위해 사용하는 위젯들입니다. ConsumerWidget/HookConsumerWidget로서 동일한 속성을 가집니다.
이 위젯들은 "ref"를 얻기위해 별도의 class를 정의할 필요가 없습니다. 아래의 예시 코드를 통해 확인해 봅시다.
Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// HookConsumerWidget과 같이, builder안에서 hooks을 사용할 수 있습니다.
final state = useState(0);
// 프로바이더를 사용/구독(listen)하기 위해서 ref 매개변수도 사용할 수 있습니다.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);
ref
를 사용해서 프로바이더와 상호작용하기
이제 "ref" 겍체와 사용방법에 대해 알아봅시다. "ref"는 3개지 주요한 용도가 있습니다.
ref.watch
= 프로바이더의 값을 취득하고 변화를 구독합니다. 값의 변경이 발생하면, 위젯(widget)을 다시 빌드하거나 값을 구독(subscribed)하고 있는 위치에 상태 값을 전달 및 제공합니다.ref.listen
= 프로바이더의 상태 값을 구독하거나 상태값이 변했을때 어떠한 행위를 취해야할 경우 사용합니다.ref.read
= 프로바이더의 상태값을 취득합니다. 이벤트 콜백함수에 사용하기 유용한데 예를들어 버튼의onPressed
콜백 함수에서 프로바이더의 필요한 상태값을 얻기위해서 사용할 수 있습니다.
기능을 구현할때는 가급적 ref.read
또는 ref.listen
보다 ref.watch
사용을 권장합니다.
ref.watch
을 사용하게되면 reactive(리엑티브)와 declarative(선언형)에 가까워 지고
애플리케이션을 더 유지보수 하기 편리하게 만들어 줍니다.
ref.watch
를 사용해서 프로바이더 관찰하기
위젯의 build
메소드 내부 또는 프로바이더 내부에서 ref.watch
를 사용함으로서
프로바이더의 상태 값을 구독(listen)할 수 있습니다.
예를 들어, 프로바이더에서 ref.watch
를 사용하여 복수의 프로바이더와 결합해 새로운 값을
생성 할 수도 있습니다.
할일 목록(a todo-list)에서 필터링하는 방법의 한가지 예시 입니다. 여기서 2개의 프로바이더를 사용했습니다.
An example would be filtering a todo-list.
We could have two providers:
filterTypeProvider
: 현재 설정한 필터타입(none, show only completed tasks, ...)에대한 상태를 나타내는 프로바이더입니다.todosProvider
: 할일(tasks)에 대한 목록 전체 값을 가지는 프로바이더 입니다.
그리고 ref.watch
를 사용하여 두개의 프로바이더를 결합해 필터링된 작업 목록 생성하는 세 번째 프로바이더를 만들 수 있습니다.
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider =
StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// 할일(todos)목록과 필터(filter)상태 값을 취득합니다.
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// 완료된(completed) 할일 목록을 반환합니다.
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// 필터링되지 않은 목록을 반환합니다.
return todos;
}
});
위의 코드에서 확인할 수 있듯이 filteredTodoListProvider
프로바이더는 필터링된 할일 목록을 가집니다.
필터링된 목록은 할일 목록이 변경되거나 필터 상태값이 변경되면 자동적으로 갱신될 것 입니다. 그러나, 필터 상태가 동일하거나 할일 목록이 동일하다면 다시 계산되거나 갱신되지 않습니다.
이와 유사하게 위젯에 ref.watch
를 사용하는 경우 프로바이더상에 의존하는 사용자 인터페이스를
표시할 수 있습니다.
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// ref를 사용하여 프로바이더 구독하기.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
이 예시코드는 카운터 상태값을 저장하고 있는 프로바이더를 구독하고 있는 위젯을 보여줍니다. 그리고 만약 카운터 값이 변경되면 위젯을 다시 빌드되고 UI에 새로 갠신된 값을 표기할 것 입니다.
watch
메소드는 비동기처리(asynchronously)에 호출하지 마세요.
예를 들어 ElevatedButton의 onPressed
콜백 함수 안이나 initState
그리고 다른 State의 생명주기 안에서는
watch
메소드를 호출하면 안됩니다.
이때는 ref.watch
대신 ref.read
메소드를 사용하는 것을 권장합니다.
ref.listen
을 사용하여 프로바이더 변화에 대응하기
ref.watch
와 유사하게 프로바이더를 관찰하기 위해 ref.listen
을 사용할 수 있습니다.
ref.watch
와 ref.listen
의 주요한 차이점은 ref.watch
는 프로바이더 상태값이 변경이되면 widget/provider을 다시 빌드하지만
ref.listen
은 함수를 호출한다는 점입니다. 함수는 커스텀된 함수이며 사용자에 따라 다르게 정의해서 사용할 수 있습니다.
예를 들어 에러가 발생할때 스낵바(snackbar)를 표시하거나 어떠한 반응의 변화에 대응해야 할때 유용하게 사용할 수 있습니다.
ref.listen
메소드는 2개의 위치인자(positional arguments)가 필요합니다.
첫번째는 프로바이더이고 두번쨰는 콜백함수 입니다. 콜백함수는 상태변화에 대응하여 수행할 함수 입니다.
콜백함수로 2개의 값이 전달됩니다. 하나는 이전 상태 값이고 나머지 하는 갱신된 상태 값입니다.
ref.listen
메소드는 프로바이더 내부에서도 사용할 수 있습니다.
아래의 코드를 확인해 볼 수 있습니다.
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
또는 build
메소드 안에서 사용할 수 있습니다.
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
ElevatedButton의 onPressed
콜백함수 내부, initState
내부 그리고 다른 상태주기의 State상에서와 같이
listen
메소드 또한 비동기(asynchronously)로 호출 가능한 곳에서는 사용을 피해야합니다.
ref.read
을 사용하여 프로바이더의 상태를 한번 취득하기
ref.read
메소드는 어떠한 부가적인 효과 없이 프로바이더의 상태를 얻기위한 방법을 제공합니다.
ref.read
는 일반적으로 사용자 상호작용으로 발생가능한 트리거 함수내부에서 주로 사용합니다.
예를들어 버튼을 눌렀을떄 카운터의 값이 증가시키고 싶을때 ref.read
메소드를 사용할 수 있습니다.
final counterProvider =
StateNotifierProvider<Counter, int>(Counter.new);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// `Counter`클래스의 increment() 메소드를 호출합니다.
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
가능한 ref.read
사용을 피해주세요.
ref.read
메소드는 watch
또는 listen
이 어려운곳에서 사용할 수 있는 대응책으로 사용하되,
가능한 watch
/listen
를 사용해주세요. 여기서 더 추천하는 메소드는 watch
메소드입니다.
[DONT'T] ref.read
를 build
메소드 안에서 사용하지 마세요.
위젯의 성능 최적화를 위해 아래의 예시처럼 ref.read
를 사용하고 있을 수 있습니다.
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
// 프로바이더 상태 값 갱신을 무시하기위해서 "read" 사용
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
그러나, 위의 예시는 나쁜 예시를 나타내고 있습니다. 그리고 이렇게 사용할 경우 다루기 어려운 버그들을 유발수 있습니다.
ref.read
를 사용하기위해 이 방법은 일반적으로 다음과 같이 생각해서 발생할 수 있습니다.
"프로바이더의 상태값이 변경되지 않으니까 'ref.read'를 사용하는것이 안전하겠다."
그러나, 오늘은 비록 상태 값이 갱신되지 않더라도 내일 상태 값이 같을 것이라고 보장할 수 없기 때문입니다.
소프트웨어는 수 없는 변화가 일어나는 경향이 있습니다.
그리고 미래에는 현재의 절대 바뀌지 않을 값이 변경될 가능성이 있습니다.
그러나, 만약 값의 변화가 시작될때, ref.read
를 사용하게 되면 모든 ref.read
메소드를
전반적으로 ref.watch
로 변경해야합니다. (이 작업에서 수많은 에러가 발생할 수 있고 특정 케이스에
대한 변경 처리를 까먹을 수 도 있습니다.)
반면 만약 ref.watch
를 사용하게되면 어떠한 문제도 걱정도 할 필요없을 것입니다.
그래도 난 위젯이 다시 빌드되는걸 줄이고 싶고 처음부터 ref.read
를 사용하고 싶어!
While the goal is commendable, it is important to note that you can achieve the
exact same effect (reducing the number of builds) using ref.watch
instead.
Providers offer various ways to obtain a value while reducing the number of rebuilds, which you could use instead.
예를 들어
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
위의 코드 대신에 아래의 예시코드처럼 사용하는게 좋습니다.
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
두 코드의 효과는 동일합니다. 카운트 숫자 값이 변경되어도 버튼 위젯은 다시 빌드되지 않을 것입니다.
반면, 후자의 접근 방법은 카운터 값이 초기화되어도 대응이 가능합니다. 예를 들어, 애플리케이션의 다른 파트에서 아래의 메소드가 호출되면
ref.refresh(counterProvider);
ref.refresh
가 호출됨으로 StateController
객체를 재생성할 것입니다.
만약, 여기서 ref.read
를 사용한다면 버튼은 여전이 이전 상태의 StateController
인스턴스를 사용할 것입니다(disposed되었거나 더이상 사용하지 않는).
ref.watch
를 사용하면 새로운 StateController
가 가능한 상태로 버튼이 재빌드 됩니다.
무엇을 읽을지 결정하기
사용하는 프로바이더에 따라서, 취득 가능한 값의 종류가 다양해 질 수 있습니다. 예를 들어 StreamProvider를 사용한다고 생각해 봅시다.
final userProvider = StreamProvider<User>(...);
userProvider
를 읽으려고 할때 아래와 같이 사용할 수 있습니다.
userProvider
자체를 구독하는 것으로 동기된(synchronously) 현재 상태 값을 읽을 수 있습니다.Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}userProvider.stream
을 사용하여 연결된 Stream을 얻을 수 있습니다.Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}userProvider.future
를 사용해 가장 최근 상태값을 가진 Future를 얻을 수 있습니다.Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
다른 프로바이더들은 또 다른 대안 값들(alternative values)을 제공할 수 있습니다. 더 자세한 정보를 원한다면 API reference 에 설명되어 있는 각각의 프로바이더에대한 상세한 정보를 참고하세요.
"select"를 사용하여 재빌드 필터링하기
마지막 특징으로 a widget/provider의 재빌드를 수를 감소하거나
제한하는 방법을 안내합니다. (얼마나 자주 ref.listen
실행하는지를 포함하여)
프로바이더를 감시하는 것은, 그 프로바이더가 공개하는 객체의 모든 속성을 감시하는 것입니다. 그러나 특정 경우에서 구독 범위를 좁히고 특정 속성만 모니터링 대상으로 만들 수 있습니다.
예를 들어, 프로바이더는 User
상태값을 가진다고 가정해 봅시다.
abstract class User {
String get name;
int get age;
}
그런데 위젯에서는 단순히 User
의 name
값만 사용하고 있습니다.
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
만약 ref.watch
를 사용하면 User
의 age
속성이 변경되면 위젯이 재빌드 될것입니다.
여기서 해결방법은 select
를 사용하는 것입니다.
select
는 Riverpod에서 User
의 특정 속성만 구독/관찰하고 싶을때 사용합니다.
코드를 다음과 같이 개선해 볼 수 있습니다.
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
select
를 통해 관찰하고 싶은 상태값을 선택하여
선택한 속성 값의 변화가 발생했을때 사용할 수 있습니다.
그리고 User
값이 변하면, Riverpod은 이 함수를 호출하여 이전 값과 새로운 값을 비교합니다.
만약 상태값이 다르다면(예를 들어 name
이 변경되었다면), Riverpod은 위젯을 다시 빌드하는 작업을 처리할 겁니다.
그러나 만약 값이 같다면 (예를 들어 age
만 변경되었다면), Riverpod은 위젯을 재빌드 하지 않을 겁니다.
select
는 ref.listen
과 함께 사용할 수 있습니다.
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);
name
이 변경되었을 때 호출되어 사용할 수 있는 구조를 가지게 됩니다.
select
를 사용하는 경우 반환하는 값이 반드시 객체일 필요는 없습니다.
==
연자자의 오버라이드(overrides)로 객체가 동일하다고 정의된다면 반환 값으로 무엇이 오든 상관없습니다.
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));