주요 콘텐츠로 건너뛰기

요청 결합하기

지금까지는 요청이 서로 독립적인 경우만 보았습니다. 하지만 일반적인 사용 사례는 다른 요청의 결과에 따라 요청을 트리거해야 하는 경우입니다.

이를 위해 provider의 결과를 다른 provider에게 매개변수(parameter)로 전달하여 요청에 인자 전달하기 메커니즘을 사용할 수 있습니다.

하지만 이 접근 방식에는 몇 가지 단점이 있습니다:

  • 구현 세부 정보가 유출(leaks)됩니다. 이제 UI는 다른 provider가 사용하는 모든 providers에 대해 알아야 합니다.
  • 매개변수(parameter)가 변경될 때마다 완전히 새로운 상태가 만들어집니다. 매개변수를 전달하면 매개변수가 변경될 때 이전 상태를 유지할 수 있는 방법이 없습니다.
  • 요청을 결합(combining requests)하기가 더 어려워집니다.
  • 따라서 툴링의 유용성이 떨어집니다. 개발 도구는 providers 간의 관계에 대해 알 수 없습니다.

이를 개선하기 위해 Riverpod은 요청을 결합하는 다른 접근 방식을 제공합니다.

기본 사항: "ref" 획득하기

요청을 결합하는 모든 방법에는 한 가지 공통점이 있습니다. 모두 Ref 객체를 기반으로 한다는 점입니다.

Ref 객체는 모든 providers가 접근할 수 있는 객체입니다. 이 객체는 다양한 라이프사이클 리스너에 대한 액세스 권한을 부여할 뿐만 아니라, providers를 결합하는 다양한 메서드도 제공합니다.

Ref를 얻을 수 있는 위치는 provider 타입에 따라 다릅니다.

함수형 provider의 경우, Ref는 provider의 함수에 매개변수로 전달됩니다:


int example(ExampleRef ref) {
// 다른 provider를 읽으려면 여기에서 "Ref"를 사용할 수 있습니다.
final otherValue = ref.watch(otherProvider);

return 0;
}

클래스 변형에서 Ref는 Notifier 클래스의 속성입니다:


class Example extends _$Example {

int build() {
// 다른 provider를 읽으려면 여기에서 "Ref"를 사용할 수 있습니다.
final otherValue = ref.watch(otherProvider);

return 0;
}
}

ref를 사용하여 provider를 읽습니다.

ref.watch 메소드

이제 Ref를 얻었으므로 이를 사용하여 요청을 결합할 수 있습니다. 이를 수행하는 주된 방법은 ref.watch를 사용하는 것입니다.
일반적으로 유지 관리가 더 쉽기 때문에 일반적으로 다른 옵션보다 ref.watch를 사용할 수 있도록 코드를 설계하는 것이 좋습니다.

ref.watch 메서드는 provider를 받아 현재 상태를 반환합니다. 그러면 리스닝된 provider가 변경될 때마다 provider가 무효화(invalidated)되고 다음 프레임 또는 다음 읽기(read) 시 다시 빌드됩니다.

ref.watch를 사용하면 로직이 "reactive"이면서 "declarative"이게 됩니다.
즉, 필요할 때 로직이 자동으로 다시 계산(recompute)된다는 뜻입니다. 그리고 업데이트 메커니즘이 'on change'와 같은 부작용(side-effects)에 의존하지 않습니다. 이는 StatelessWidgets의 작동 방식과 유사합니다.

예를 들어 사용자의 위치를 수신하는 provider를 정의할 수 있습니다. 그런 다음 이 위치를 사용하여 사용자 근처의 레스토랑 목록을 가져올 수 있습니다.


Stream<({double longitude, double latitude})> location(LocationRef ref) {
// TO-DO: 현재 위치를 가져오는 Stream을 반환합니다.
return someStream;
}


Future<List<String>> restaurantsNearMe(RestaurantsNearMeRef ref) async {
// "ref.watch"를 사용하여 최신 위치를 가져옵니다.
// 공급자 뒤에 ".future"를 지정하면 코드가 적어도 하나의 위치를 사용할 수 있을 때까지 기다립니다.
final location = await ref.watch(locationProvider.future);

// 이제 해당 위치를 기반으로 네트워크 요청을 할 수 있습니다.
// 예를 들어 Google 지도 API를 사용할 수 있습니다:
// https://developers.google.com/maps/documentation/places/web-service/search-nearby
final response = await http.get(
Uri.https('maps.googleapis.com', 'maps/api/place/nearbysearch/json', {
'location': '${location.latitude},${location.longitude}',
'radius': '1500',
'type': 'restaurant',
'key': '<your api key>',
}),
);
// JSON에서 레스토랑 이름 가져오기
final json = jsonDecode(response.body) as Map;
final results = (json['results'] as List).cast<Map<Object?, Object?>>();
return results.map((e) => e['name']! as String).toList();
}
정보

수신 중인 provider가 변경되어 요청이 다시 계산되면 새 요청이 완료될 때까지 이전 상태가 유지됩니다.
동시에 요청이 보류(pending)되는 동안 "isLoading" 및 "isReloading" 플래그가 설정됩니다.

이를 통해 UI에 이전 상태 또는 로딩 표시기를 표시하거나 둘 다 표시할 수 있습니다.

정보

ref.watch(locationProvider) 대신 ref.watch(locationProvider.future)를 사용한 것을 주목하세요. locationProvider가 비동기적이기 때문입니다. 따라서 초기 값을 사용할 수 있을 때까지 기다려야 합니다.

.future를 생략하면 locationProvider의 현재 상태에 대한 스냅샷인 AsyncValue를 받게 됩니다. 하지만 아직 사용할 수 있는 위치가 없다면 아무 것도 할 수 없습니다.

주의

"명령형 문법(imperatively)"으로 실행되는 코드 내에서 ref.watch를 호출하는 것은 나쁜 습관으로 간주됩니다. 이는 provider의 빌드 단계에서 실행되지 않을 가능성이 있는 모든 코드를 의미합니다. 여기에는 "listener" 콜백(callbacks)이나 Notifier의 메서드가 포함됩니다:


int example(ExampleRef ref) {
ref.watch(otherProvider); // Good!
ref.onDispose(() => ref.watch(otherProvider)); // Bad!

final someListenable = ValueNotifier(0);
someListenable.addListener(() {
ref.watch(otherProvider); // Bad!
});

return 0;
}


class MyNotifier extends _$MyNotifier {

int build() {
ref.watch(otherProvider); // Good!
ref.onDispose(() => ref.watch(otherProvider)); // Bad!

return 0;
}

void increment() {
ref.watch(otherProvider); // Bad!
}
}

ref.listen/listenSelf 메소드

ref.listen 메서드는 ref.watch의 대안입니다.
이 메서드는 기존의 "listen"/"addListener" 메서드와 유사합니다. 이 메서드는 provider와 callback을 받으며, provider의 콘텐츠가 변경될 때마다 해당 callback을 호출합니다.

ref.listen 대신 ref.watch를 사용할 수 있도록 코드를 리팩토링하는 것이 일반적으로 권장되는데, 전자는 명령형으로 인해 오류가 발생하기 쉽기 때문입니다.
하지만 ref.listen는 큰 리팩토링을 하지 않고도 빠른 로직을 추가하는 데 유용할 수 있습니다.

ref.watch 예제를 다시 작성하여 ref.listen을 대신 사용할 수 있습니다.


int example(ExampleRef ref) {
ref.listen(otherProvider, (previous, next) {
print('Changed from: $previous, next: $next');
});

return 0;
}
정보

provider의 빌드 단계에서 ref.listen을 사용하는 것은 전적으로 안전합니다. provider가 어떻게든 다시 계산되면 이전 리스너가 제거됩니다.

또는 ref.listen의 반환 값을 사용하여 원할 때 리스너를 수동으로 제거할 수 있습니다.

ref.read 메소드

마지막으로 사용할 수 있는 옵션은 ref.read입니다. 이 옵션은 provider의 현재 상태를 반환한다는 점에서 ref.watch와 유사합니다. 하지만 ref.watch와 달리 공급자를 수신(listen)하지 않습니다.

따라서 ref.read는 Notifier의 메서드 내부와 같이 ref.watch를 사용할 수 없는 곳에서만 사용해야 합니다.


class MyNotifier extends _$MyNotifier {

int build() {
// Bad! 여기서는 reactive가 아니므로 'read'를 사용하지 마세요.
ref.read(otherProvider);

return 0;
}

void increment() {
ref.read(otherProvider); // 여기서 'read'를 사용해도 괜찮습니다.
}
}
주의

provider에서 ref.read를 사용할 때는 주의하세요. provider를 수신(listen)하지 않으므로, 해당 provider가 수신(listen)하지 않으면 상태(state)를 파괴(destroy할 수 있습니다.