프로바이더 결합하기
들어가기 앞서 먼저 프로바이더란문서를 먼저 읽어주세요.
이 가이드는 복수의 프로바이더들(providers) 결합하는 방법에 대해서 알아볼 예정입니다.
프로바이더 결합하기
이전에 간단한 프로바이더를 생성하는 방법을 알아보았습니다. 그런데 현실에서는 수많은 상황 중에서 하나의 프로바이더가 다른 프로바이더의 상태를 읽어 사용하는 경우가 많이 있습니다.
여러 프로바이더를 결합하기 위해서 ref 객체를 사용하여 콜백을 전달하거나 watch 메소드를 사용할 수 있습니다.
예를 들어, 아래와 같은 프로바이더가 있다고 고려해 보겠습니다.
final cityProvider = Provider((ref) => 'London');
이제 cityProvider
를 사용하고 싶은 다른 프로바이더를 만들어 보려고 합니다.
final weatherProvider = FutureProvider((ref) async {
// `ref.watch`를 사용해 다른 프로바이더를 읽어올 수 있습니다.
// 그리고 프로바이더를 넘겨줄 수 있습니다. (여기서는 cityProvider가 되겠습니다.)
final city = ref.watch(cityProvider);
// `cityProvider` 값을 기반으로한 무언가의 결과를 반환할 수 있습니다.
return fetchWeather(city: city);
});
이상입니다. 여기서 우리는 다른 프로바이더에 의존하는 한개의 프로바이더를 만드는 방법을 알아보았습니다.
FAQ
구독중인 값이 시간이 지남에 따라 변경되면 어떻게 되나요?
관찰하거나 구독하고 있는 프로바이더에 의존하여 값이 매번 갱신이 될때 마다 새로운 상태값을 얻게 됩니다. 예를 들어, StateNotifierProvider를 구독하고 있거나 ProviderContainer.refresh/ref.refresh 에 의해 강제적으로 갱신이 되었다면 물론 구독중인 상태값은 갱신되어 전달될 것입니다.
watch를 사용할 때, Riverpod은 상태값의 변화를 검출하고 자동적 으로 프로바이더를 필요할 때 재 실행합니다.
이것은 상태들을 계산하기 유용할 수 있습니다. 예를 들어 StateNotifierProvider가 할일 목록(a todo-list)의 값을 가진다고 생각해 봅시다.
class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}
final todoListProvider = StateNotifierProvider((ref) => TodoList());
일반적인 사용방법으로 완료된/완료되지 않은 할일만 표시하도록 UI가 할일 목록을 필터링하도록 하는 것입니다.
이러한 시나리오를 구현하는 쉬운 방법은 다음과 같습니다.
StateProvider를 생성합니다.
filterProvider
는 현재 어떤 필터를 선택하고 있는지에 대한 상태 값을 가집니다.enum Filter {
none,
completed,
uncompleted,
}
final filterProvider = StateProvider((ref) => Filter.none);필터 메소드(
filterProvider
)와 할일 목록(todoListProvider
)을 결합하여 필터링된 할일 목록 값을 제공하는 별도의 프로바이더(filteredTodoListProvider
)를 만듭니다.final filteredTodoListProvider = Provider<List<Todo>>((ref) {
final filter = ref.watch(filterProvider);
final todos = ref.watch(todoListProvider);
switch (filter) {
case Filter.none:
return todos;
case Filter.completed:
return todos.where((todo) => todo.completed).toList();
case Filter.uncompleted:
return todos.where((todo) => !todo.completed).toList();
}
});
그리고, UI에서 filteredTodoListProvider
를 구독하여 필터링된 할일 목록을 받아 올 수 있습니다.
필터와 할일 목록이 각각 갱신(변경)될때 마다 자동적으로 UI 업데이트가 이루어 집니다.
이러한 방법으로 접근한 애플리케이션 샘플은 Todo List example에서 확인해 볼 수 있습니다.
이 행위는 Provider에 국한되지 않습니다. 그리고 모든 프로바이더에 적용하여 사용 가능합니다.
예를 들어, 실시간적으로 값이 변하는 것을 지원하는 검색 기능을 구현하기 위해 FutureProvider와 함께 watch를 결합하여 사용할 수 있습니다.
For example, you could combine watch with FutureProvider to implement a search feature that supports live-configuration changes:
// 현재 검색 필터 값 입니다. ('')
final searchProvider = StateProvider((ref) => '');
/// 매 시간마다 변경될 수 있는 구성(Configurations)
final configsProvider = StreamProvider<Configuration>(...);
final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.future);
final response = await dio.get('${configs.host}/characters?search=$search');
return response.data.map((json) => Character.fromJson(json)).toList();
});
이 코드는 characters
목록을 서비스로 부터 가져옵니다. 그리고 구성(configurations)이 변하거나 검색 쿼리가 변경되면
자동적으로 다시 characters
목록을 가져옵니다.
구독없이 프로바이더를 읽을 수 있나요?
때때로 우린 값이 변경되어도 다시 생성되는 작업 없이 프로바이더의 컨텐츠를 읽기 원합니다. Sometimes, we want to read the content of a provider, but without re-creating the value exposed when the value obtained changes.
예를 들어 인증을 위한 사용자 토큰을 다른 프로바이더로 부터 읽어오는 Repository
프로바이더가 있다고 생각해 봅시다.
여기서 우리는 watch를 사용하고 사용자 토큰이 변경될 때마다 새로운 Repository
를 생성할 수 있지만 그렇게 하는 것은 거의 소용이 없습니다.
이 경우에는, watch와 유사한 기능을 가지는 read를 사용할 수 있습니다. 그러나, 상태 값이 변경될 때 프로바이더가 노출하는 값을 다시 생성하지 않습니다.
이 경우 일반적으로 생성된 객체에 'ref.read'를 전달합니다. 생성된 객체는 원할 때마다 프로바이더를 읽을 수 있습니다.
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// `ref.read` 함수입니다.
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
ref.read
대신에 ref
를 객체에 전달해서 사용하도록 합니다.
final repositoryProvider = Provider((ref) => Repository(ref));
class Repository {
Repository(this.ref);
final Ref ref;
}
그러나 ref.read
를 전달하면 코드가 약간 덜 장황해지고 객체가 ref.watch
를 사용하지 않을 것입니다.
다시 말해, ref
대신 ref.read
를 전달하면 ref.watch
는 사용할 수 없다는 말과 동일합니다.
:::위험 DON'T : read를 프로바이더 내부에서 호출하지 마세요.
final myProvider = Provider((ref) {
// 여기서 'read'를 호출하는 것은 좋지 않습니다.
final value = ref.read(anotherProvider);
});
만약 객체의 원치 않는 재빌드을 방지하기 위해 read를 사용한 경우, 프로바이더 갱신이 너무 자주일어나는데 어떻게 개선하면 좋을까요? 항목을 참고해 주세요.
:::
생성자의 매개변수로 read를 전달 받는 객체는 어떻게 테스트 하면 좋은가요?
만약 구독없이 프로바이더를 읽을 수 있나요?에서 사용한 패턴을 사용한다면, 어떻게 객체를 테스트할지 의문이 들 수 있습니다.
이 시나리오에서는 raw 객체 대신에 프로바이더를 직접 테스트 하는 것이 좋습니다. [ProviderContainer] 클래스를 사용하여 테스트를 진행할 수 있습니다.
final repositoryProvider = Provider((ref) => Repository(ref.read));
test('fetches catalog', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
Repository repository = container.read(repositoryProvider);
await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});
프로바이더 갱신이 너무 자주일어나는데 어떻게 개선하면 좋을까요?
객체가 너무 자주 다시 생성되는 경우 프로바이더가 갱신에 무관계한 요소를 수신하고 있는 것입니다.
에들 들어, Configuration
객체 뿐만 아니라 host
속성을 구독하고 있다고 있다고 가정해 봅시다.
host
의 속성만 변경되었지만 본래 필요없는 값까지 모두 재평가(계산)하여 전반적인 프로바이더의 갱신을 가져올 것 입니다.
해결 방법으로는 분리된 프로바이더를 만드는 것입니다. 즉, 프로바이더를 속성별로 분리하는 것입니다.
다시 말해, Configuration
(즉 host
)에서 필요한 것만 노출하는 별도의 프로바이더를 만드는 것입니다.
AVOID 객체 전반적인 관찰:
final configsProvider = StreamProvider<Configuration>(...);
final productsProvider = FutureProvider<List<Product>>((ref) async {
// 어떤 구성이 변경되었다고 한다면 productsProvider는 products를 다시 가져옵니다.
final configs = await ref.watch(configsProvider.future);
return dio.get('${configs.host}/products');
});
PREFER 실제 사용하는 속성만 관찰:
final configsProvider = StreamProvider<Configuration>(...);
/// 현재 host값만 노출하는 프로바이더
final _hostProvider = FutureProvider<String>((ref) async {
final config = await ref.watch(configsProvider.future);
return config.host;
});
final productsProvider = FutureProvider<List<Product>>((ref) async {
/// 호스트(the host)만 구독.
/// 만약 구성들의 변화가 발생한다면, 무의미하게 재평가하지 않습니다.
final host = await ref.watch(_hostProvider.future);
return dio.get('$host/products');
});