본문으로 바로가기
  1. Home
  2. Flutter/Package
  3. Flutter Riverpod Concepts(6-2) - Reading a Provider

Flutter Riverpod Concepts(6-2) - Reading a Provider

· 댓글개 · Dev_Whale

이 가이드를 읽기 전에 먼저 Provider에 대해 읽어보시기 바랍니다.

2023.07.18 - [Flutter/Package] - Flutter Riverpod Concepts(6-1) - Providers


이 가이드에서는 Provider를 사용하는 방법을 살펴봅니다.

"ref" 객체 가져오기

무엇보다도 provider를 읽기 전에 "ref" 객체를 가져와야 합니다.

이 객체를 통해 위젯이나 다른 provider와 같은 provider와 상호 작용할 수 있습니다.

provider로부터 ‘ref' 받기

모든 provider는 매개변수로 "ref"를 받습니다:

final valueProvider = Provider((ref) {
  // 다른 provider를 얻으려면 ref를 사용하세요.
  final repository = ref.watch(repositoryProvider);
  return repository.get();
});

이 매개변수는 provider가 노출한 값으로 전달해도 안전합니다.

일반적인 사용 예시는 provider의 "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"를 사용하여 다른 provider를 읽을 수 있습니다.
    final repository = ref.read(repositoryProvider);
    repository.post('...');
  }
}

이렇게 하면 Counter 클래스가 다른 provider를 읽을 수 있습니다.

위젯에서 " ref" 가져오기

위젯에는 기본적으로 ref 매개변수가 없습니다. 하지만 Riverpod는 위젯에서 이를 얻을 수 있는 여러 가지 솔루션을 제공합니다.

  • StatelessWidget 대신 ConsumerWidget 에 Extend을 사용

위젯 트리에서 ref를 얻는 가장 일반적인 방법은 StatelessWidget을 ConsumerWidget으로 대체하는 것입니다.

 

ConsumerWidget은 build 메서드에 "ref" 객체라는 추가 매개 변수가 있다는 점만 다를 뿐 사용 방식은 StatelessWidget과 동일합니다.

일반적인 ConsumerWidget의 모습은 다음과 같습니다:

class HomeView extends ConsumerWidget {
  const HomeView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // use ref to listen to a provider
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

StatefulWidget + State 대신 ConsumerStatefulWidget + ConsumerState 확장하기

 

ConsumerWidget과 유사하게, ConsumerStatefulWidget 및 ConsumerState는 State가 있는 StatefulWidget과 동일하지만, State에 "ref" 객체가 있다는 차이점이 있습니다.

 

이번에는 "ref"가 build method의 매개변수로 전달되지 않고 ConsumerState 객체의 프로퍼티로 전달됩니다:

class HomeView extends ConsumerStatefulWidget {
  const HomeView({super.key});

  @override
  HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
  @override
  void initState() {
    super.initState();
    // "ref"는 StatefulWidget의 모든 라이프사이클에서 사용할 수 있습니다.
    ref.read(counterProvider);
  }

  @override
  Widget build(BuildContext context) {
    // 또한 "ref"를 사용하여 build 메서드 내에서 providers를 수신할 수도 있습니다.
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

ref를 사용하여 provider와 상호 작용하기

이제 'ref'가 생겼으니 사용할 수 있습니다.

 

"ref"의 주요 용도는 세 가지입니다:

  • provider의 값을 가져오고 변경 사항을 수신하여 이 값이 변경되면 해당 값을 구독한 위젯 또는 공급자를 다시 빌드합니다. 
    이 작업을 수행하려면 ref.watch 을 사용합니다.
  • provider에 listeners를 추가하여 해당 provider가 변경될 때마다 새 페이지로 이동하거나 modal을 표시하는 등의 작업을 실행할 수 있습니다. 이 작업은 ref.listen을 사용하여 수행됩니다.
  • 변경 사항을 무시하면서 providers의 값을 가져옵니다. 이는 "클릭 시"와 같은 이벤트에서 provider 값이 필요할 때 유용합니다. 이 작업은 ref.read를 사용하여 수행됩니다.
NOTE)
가능하면 기능을 구현할 때는 ref.read나 ref.listen보다 ref.watch를 사용하는 것이 좋습니다. ref.watch를 사용하면 애플리케이션이 반응적이고 선언적이 되어 유지 관리가 더 쉬워집니다.

ref.watch를 사용하여 provider 관찰하기

ref.watch는 위젯의 빌드 메서드 내부 또는 provider의 body 내부에서 사용하여 widget/provider가 provider를 수신하도록 합니다:

 

예를 들어 provider는 ref.watch를 사용하여 여러 provider와 새로운 값으로 결합할 수 있습니다.

 

예를 들어 할 일 목록을 필터링할 수 있습니다. 두 개의 provider가 있을 수 있습니다:

  • filterTypeProvider, 현재 필터 유형(없음, 완료된 작업만 표시, ...)을 노출하는 provider
  • todosProvider, 전체 작업 목록을 노출하는 provider

그리고 ref.watch를 사용하면 두 provider를 결합하여 필터링된 작업 목록을 만드는 세 번째 provider를 만들 수 있습니다:

final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider =
    StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
  // obtains both the filter and the list of todos
  final FilterType filter = ref.watch(filterTypeProvider);
  final List<Todo> todos = ref.watch(todosProvider);

  switch (filter) {
    case FilterType.completed:
      // return the completed list of todos
      return todos.where((todo) => todo.isCompleted).toList();
    case FilterType.none:
      // returns the unfiltered list of todos
      return todos;
  }
});

이 코드를 사용하면 filteredTodoListProvider가 이제 필터링된 작업 목록을 노출합니다.

 

필터나 작업 목록이 변경되면 필터링된 목록도 자동으로 업데이트됩니다. 동시에 필터나 작업 목록이 모두 변경되지 않은 경우 필터링된 목록은 다시 계산되지 않습니다.

 

마찬가지로 위젯은 ref.watch를 사용하여 provider의 콘텐츠를 표시하고 해당 콘텐츠가 변경될 때마다 사용자 인터페이스를 업데이트할 수 있습니다:

final counterProvider = StateProvider((ref) => 0);

class HomeView extends ConsumerWidget {
  const HomeView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // use ref to listen to a provider
    final counter = ref.watch(counterProvider);

    return Text('$counter');
  }
}

이 스니펫에서는 수(count)를 저장하는 provider를 수신하는 위젯을 보여줍니다. 카운트가 변경되면 위젯이 다시 빌드되고 UI가 새 값을 표시하도록 업데이트됩니다.

CAUTION)
watch 메서드는 비동기적으로 호출해서는 안 됩니다(예: ElevatedButton의 onPressed 내부에서). 또한 initState 및 기타 상태 라이프사이클 내부에서도 사용해서는 안 됩니다. 이러한 경우 ref.read를 대신 사용하는 것이 좋습니다.

ref.listen을 사용하여 공급자 변경에 응답하기

ref.watch와 유사하게 ref.listen을 사용하면 provider를 관찰할 수 있습니다.

 

가장 큰 차이점은 listen을 받는 provider가 변경될 경우 widget/provider를 다시 빌드하는 대신 ref.listen을 사용하면 사용자 정의 함수를 호출한다는 점입니다.

 

이는 오류 발생 시 스낵바 표시와 같이 특정 변경 사항이 발생할 때 작업을 수행하는 데 유용할 수 있습니다.

 

ref.listen 메서드에는 두 개의 position 인수가 필요한데, 첫 번째 인수는 provider이고 두 번째 인수는 상태가 변경될 때 실행하려는 콜백 함수입니다. 콜백 함수가 호출되면 이전 상태의 값과 새 상태의 값, 두 개의 값이 전달됩니다.

 

ref.listen 메서드는 provider 본문 내에서 사용할 수 있습니다:

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});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
      print('The counter changed $newCount');
    });

    return Container();
  }
}
CAUTION)
listen 메서드는 비동기적으로 호출해서는 안 되며, 예를 들어 ElevatedButton의 onPressed 내부에서 호출해서는 안 됩니다. 또한 initState 및 기타 상태 라이프사이클 내부에서도 사용해서는 안 됩니다.

ref.read를 사용하여 provider 상태 가져오기

ref.read 메서드는 provider를 수신하지 않고도 provider의 상태를 가져오는 방법입니다.

 

일반적으로 사용자 상호작용에 의해 트리거되는 함수 내부에서 사용됩니다. 예를 들어 사용자가 버튼을 클릭할 때 ref.read를 사용하여 카운터를 증가시킬 수 있습니다:

final counterProvider =
    StateNotifierProvider<Counter, int>(Counter.new);

class HomeView extends ConsumerWidget {
  const HomeView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Call `increment()` on the `Counter` class
          ref.read(counterProvider.notifier).increment();
        },
      ),
    );
  }
}
NOTE)
ref.read는 반응형이 아니므로 가급적 사용하지 않는 것이 좋습니다. watch 또는 listen를 사용하면 문제가 발생할 수 있는 경우를 위해 존재합니다. 가능하면 거의 항상 watch/listen을 사용하는 것이 좋으며, 특히 watch를 사용하는 것이 좋습니다.

[DON'T] build 메서드 내에서 ref.read를 사용하지 마세요.

ref.read를 사용하여 위젯의 성능을 최적화하고 싶을 수도 있습니다:

final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
  // provider의 업데이트를 무시하려면 “read"를 사용하세요.
  final counter = ref.read(counterProvider.notifier);
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}

하지만 이는 매우 나쁜 방법이며 트래킹하기 어려운 버그를 유발할 수 있습니다.

 

이러한 방식으로 ref.read를 사용하는 것은 일반적으로 “provider가 노출한 값은 절대 변경되지 않으므로 'ref.read'를 사용하는 것이 안전하다"는 생각과 관련이 있습니다. 이 가정에 대한 문제점은 오늘 해당 provider가 실제로 값을 업데이트하지 않을 수도 있지만, 내일도 마찬가지일 것이라는 보장이 없다는 것입니다.

 

소프트웨어는 많이 변하는 경향이 있으며, 미래에는 이전에는 변하지 않았던 가치도 변해야 할 가능성이 높습니다.

 

ref.read를 사용하는 경우 해당 값을 변경해야 할 때 전체 코드베이스를 검토하여 ref.read를 ref.watch로 변경해야 하므로 오류가 발생하기 쉽고 일부 경우를 잊어버릴 가능성이 높습니다.

 

처음부터 ref.watch를 사용하면 리팩토링할 때 발생하는 문제를 줄일 수 있습니다.

 

하지만 ref.read를 사용하면 위젯이 rebuild되는 횟수를 줄일 수 있습니다.

 

목표는 칭찬할 만하지만, ref.watch를 대신 사용하여 똑같은 효과(빌드 횟수 감소)를 얻을 수 있다는 점에 유의해야 합니다.

 

provider에서는 rebuild 횟수를 줄이면서 가치를 얻을 수 있는 다양한 방법을 제공하며, 이를 대신 사용할 수 있습니다.

 

예시 대신에

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(counterProvider)를 호출하면 `StateController` 객체가 다시 생성됩니다.
여기서 ref.read를 사용하면 버튼은 폐기되어 더 이상 사용되지 않는 이전 StateController 인스턴스를 계속 사용하게 됩니다. 반면 ref.watch를 사용하면 버튼이 올바르게 다시 빌드되어 새로운 StateController를 사용합니다.

읽을 항목 결정하기 (Deciding what to read)

수신하려는 provider에 따라 수신할 수 있는 값이 여러 개 있을 수 있습니다.

 

예를 들어 다음 StreamProvider를 생각해 보겠습니다:

final userProvider = StreamProvider<User>(...);

userProvider를 읽을 때:

  • userProvider 자체를 수신하여 현재 상태를 동기적으로 읽습니다:
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.future를 수신하여 마지막으로 생성된 값으로 해결되는 Future를 얻습니다:
Widget build(BuildContext context, WidgetRef ref) {
  Future<User> user = ref.watch(userProvider.future);
}

다른 provider는 다른 대체 값을 제공할 수 있습니다.
자세한 내용은 API 레퍼런스를 참고하여 각 provider의 설명서를 참조하세요.

’select'을 사용하여 rebuild 필터링하기

마지막으로 provider read와 관련하여 언급할 기능은 widget/provider가 ref.watch에서 rebuild하는 횟수 또는 ref.listen이 함수를 실행하는 빈도를 줄일 수 있는 기능입니다.

 

기본적으로 provider 수신은 전체 객체 상태를 수신하므로 이 점을 염두에 두는 것이 중요합니다. 그러나 때로는 widget/provider가 전체 객체가 아닌 일부 프로퍼티의 변경 사항에만 관심을 가질 수도 있습니다.

 

예를 들어 provider가 사용자를 표시할 수 있습니다:

abstract class User {
  String get name;
  int get age;
}

하지만 위젯은 사용자 이름만 사용할 수 있습니다:

Widget build(BuildContext context, WidgetRef ref) {
  User user = ref.watch(userProvider);
  return Text(user.name);
}

ref.watch를 순수하게 사용했다면 사용자의 나이가 변경되면 위젯이 다시 빌드됩니다.

 

해결책은 select를 사용하여 RiverPod이 사용자의 이름 속성만 수신하도록 명시적으로 지정하는 것입니다.

 

업데이트된 코드는 다음과 같습니다:

Widget build(BuildContext context, WidgetRef ref) {
  String name = ref.watch(userProvider.select((user) => user.name));
  return Text(name);
}

select를 사용하면 원하는 프로퍼티를 반환하는 함수를 지정할 수 있습니다.

 

사용자가 변경될 때마다 Riverpod는 이 함수를 호출하여 이전 결과와 새 결과를 비교합니다. 두 결과가 다른 경우(예: 이름이 변경된 경우) Riverpod는 위젯을 다시 빌드합니다.
그러나 두 값이 같으면(예: 나이가 변경된 경우) 리버팟은 위젯을 다시 빌드하지 않습니다.

INFO)
ref.listen과 함께 select를 사용하는 것도 가능합니다:
ref.listen<String>(
  userProvider.select((user) => user.name),
  (String? previousName, String newName) {
    print('The user name changed $newName');
  }
);​

이렇게 하면 이름이 변경될 때만 수신자를 호출합니다.

TIP)
객체의 프로퍼티를 반환할 필요는 없습니다. ==를 재정의하는 모든 값이 작동합니다. 예를 들어 다음과 같이 할 수 있습니다:
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));​
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.