(Flutter) Riverpod의 Provider 상태 결합하기

반응형


이번 글에서는 provider 상태를 결합하는 방법에 대해 알아봅니다.

프로바이더 상태 결합하기

프로바이더에서 다른 프로바이더의 상태 값를 읽어야 하는 경우가 많이 있습니다.

이를 위해서는 프로바이더 콜백에 전달된 ref 객체를 사용하고, ref의 watch 메소드를 사용할 수 있습니다.

예를 들어 다음 cityProvider 프로바이더를 살펴보겠습니다:

final cityProvider = Provider((ref) => 'London');

이제 cityProvider를 구독하고 싶은 다른 프로바이더를 만들어 봅니다.

final weatherProvider = FutureProvider((ref) async {
  // `ref.watch`를 사용하여 다른 프로바이더를 구독합니다. 
  // 구독할 프로바이더(cityProvider)를 watch 메소드에 전달합니다.
  final city = ref.watch(cityProvider);
  
  // 그런 다음 'cityProvider'의 값을 기반으로 계산한 값을 반환합니다.
  return fetchWeather(city: city);
});

그게 다입니다. 다른 프로바이더에 의존하는 프로바이더를 만들었습니다.

FAQ

시간이 지남에 따라 구독중인 값이 변경되면 어떻게 되나요?

구독중인 프로바이더는 따라 시간이 지남에 따라 획득한 값이 변경될 수 있습니다.
예를 들어, StateNotifierProvider를 구독하고 있거나 구독 중인 프로바이더가 ProviderContainer.refresh/ref.refresh 에 의해 강제로 새로고침이 되었을 수 있습니다.

watch를 사용하면 Riverpod은 구독 중인 값이 변경된 것을 감지합니다. 그리고 필요할 때 프로바이더를 재생성합니다.

이것은 계산된 상태값을 관리할 때 유용합니다.
예를 들면, 할일 목록(TodoList)을 가지고 있는 StateNotifierProvider가 있다고 생각해 봅시다.

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);
  • 그다음 filterProvidertodoListProvider을 결합하여 필터링된 할일 목록을 제공하는 별도의 프로바이더 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에서 확인할 수 있습니다.

[!info] 정보
이러한 동작 방식은 Provider에만 국한되지 않으며 모든 프로바이더에서 작동합니다.

예를 들어 FutureProviderwatch를 결합하면, 실시간으로 값이 변하는 것을 지원하는 검색 기능을 구현할 수 있습니다.

// 현재 검색 필터
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();
});

위 코드는 서버API에서 characters 목록을 가져옵니다. 그리고 구성(configurations)이 변하거나 검색 쿼리가 변경되면 목록을 자동으로 다시 가져옵니다.

프로바이더를 구독하지 않고도 읽을 수 있나요?

가끔은 프로바이더 내부에서 다른 프로바이더의 상태값를 읽고 싶지만, 획득한 상태값이 변경될 때 프로바이더가 다시 생성되는 것은 피하고 싶을 때가 있습니다.

예를 들면, 사용자 인증을 위해 다른 프로바이더에서 사용자 토큰을 읽어오는 Repository 프로바이더가 있을 수 있습니다. 여기서 우리는 watch를 사용하여 사용자 토큰이 변경될 때마다 Repository를 새로 생성할 수 있습니다. 하지만 그렇게 하는 것은 낭비가 많은 것처럼 보입니다.

이런 경우에 watch와 유사한 기능을 가지는 read를 사용할 수 있습니다. read를 사용하면 상태 값이 변경되어도 프로바이더는 다시 생성되지 않습니다.

이 경우 생성된 객체에 프로바이더의 ref를 전달하는 것이 일반적입니다. 그러면 생성된 객체는 원할 때마다 프로바이더의 상태 값을 읽을 수 있습니다.

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider(Repository.new);

class Repository {
  Repository(this.ref);

  final Ref ref;

  Future<Catalog> fetchCatalog() async {
    String token = ref.read(userTokenProvider);

    final response = await dio.get('/path', queryParameters: {
      'token': token,
    });

    return Catalog.fromJson(response.data);
  }
}

[!error] 프로바이더 내부에서 READ를 호출하지 마세요.

final myProvider = Provider((ref) {
  // 여기서 `read` 를 호출하는 것은 잘못된 방법입니다.
  final value = ref.read(anotherProvider);
});

만약 객체의 원치않는 리빌드를 방지하기 위해 read를 사용한 경우에는, “프로바이더 갱신이 너무 자주 발생하는데 어떻게 해야 하나요?” 항목을 참고하세요.

생성자 파라미터로 read를 전달 받는 객체는 어떻게 테스트하나요?

만약 “프로바이더를 구독하지 않고도 읽을 수 있나요?” 항목에서 사용한 패턴을 사용한다면, 객체를 어떻게 테스트하는지 의문이 들 수 있습니다.

이 시나리오에서는 raw 객체 대신에 프로바이더를 직접 테스트하는 것이 좋습니다. 이때 ProviderContainer 클래스를 사용하면 됩니다:

final repositoryProvider = Provider((ref) => Repository(ref));

test('fetches catalog', () async {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  Repository repository = container.read(repositoryProvider);

  await expectLater(
    repository.fetchCatalog(),
    completion(Catalog()),
  );
});

프로바이더 갱신이 너무 자주 발생하는데 어떻게 해야 하나요?

프로바이더가 너무 자주 재생성되는 경우, 프로바이더가 갱신과 관계없는 객체를 수신하고 있을 가능성이 높습니다.

예를 들어 Configuration 객체를 구독하고 있지만 host 속성만 사용한다고 가정해 봅시다. 하지만 전체 Configuration 객체를 구독하면 host 이외의 속성이 변경되더라도 프로바이더를 재평가(re-evaluated)해야 하므로 원치 않는 결과를 초래할 수 있습니다.

이 문제에 대한 해결 방법은 Configuration에서 필요한 것만 공급하는 별도의 프로바이더를 만드는 것입니다:

[개선 전] 전체 객체를 구독합니다:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // configurations가 변경되면 productsProvider가 products를 다시 가져옵니다.
  final configs = await ref.watch(configProvider.future);

  return dio.get('${configs.host}/products');
});

[개선 후] 객체에서 필요한 속성만 구독합니다:

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // `host`만 구독합니다. 
  // configurations의 다른 속성이 변경되더라도 프로바이더는 재평가하지 않습니다.
  final host = await ref.watch(configProvider.selectAsync((config) => config.host));

  return dio.get('$host/products');
});

이렇게 하면 host가 변경될 때만  productsProvider가 다시 빌드됩니다.



or

[카카오페이로 후원하기] [토스페이로 후원하기]

반응형