Performing side effects
이전 글에서는 provider로 네트워크 요청 하는 방법을 학습하였다.
GET HTTP 요청만 사용해서 개발을 하였는데 POST 요청과 같은 side-effect가 발생하는 경우는 어떻게 처리할지 학습해보겠습니다.
일반적으로 업데이트 요청은 로컬 캐시를 업데이트 해야 한다.
로컬 캐시도 업데이트하여 UI에 새 상태가 반영되도록 하는 것이 일반적이다.
conusmer내에서 provider의 상태(state)를 어떻게 업데이트 할 수 있는지 알아보자.
Notifier 정의하기
간단한 GET API 요청을 통해 알아보자.
아래 코드는 할일 목록을 가져오는 코드이다.
@riverpod
Future<List<Todo>> todoList(Ref ref) async {
// 네트워크 요청을 시뮬레이션합니다. 이것은 일반적으로 실제 API에서 오는 것입니다.
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
할 일 목록을 가져왔으니 새 할 일을 추가하는 방법을 살펴보자.
상태(state) 수정을 위해 공개 API를 노출하도록 provider를 수정해야 한다. 이 작업은 provider를 “notifier”로 변환하여 수행한다.
‘Notifiers’는 providers의 “상태저장 위젯(stateful widget)”이다.
아래 예제 코드를 보며 이해를 해보자.
@riverpod
class MyNotifier extends _$MyNotifier {
@override
Result build() {
<your logic here>
}
<your methods here>
}
| 어노테이션(annotation) | 모든 providers는 @riverpod 또는 @Riverpod()로 어노테이션해야 합니다. 이 어노테이션은 전역 함수나 클래스에 배치할 수 있습니다. 이 어노테이션을 통해 provider를 설정(config)할 수 있습니다. 예를 들어, @Riverpod(keepAlive: true)를 작성하여 "auto-dispose"(나중에 살펴볼 것임)를 비활성화할 수 있습니다. | | --- | --- | | Notifier | @riverpod 어노테이션이 클래스에 배치되면 해당 클래스를 "Notifier"라고 부릅니다. 클래스는 _$NotifierName을 확장해야 하며, 여기서 NotifierName은 클래스 이름입니다. Notifiers는 provider의 상태(state)를 수정하는 메서드를 노출할 책임이 있습니다. 이 클래스의 공개 메서드는 ref.read(yourProvider.notifier).yourMethod()를 사용하여 consumer가 액세스할 수 있습니다. | | The build method | 모든 notifiers는 build 메서드를 재정의(override)해야 합니다. 이 메서드는 일반적으로 notifier가 아닌 provider(non-notifier provider)에서 로직을 넣는 위치에 해당합니다. 이 메서드는 직접 호출해서는 안 됩니다. |
위의 표는 공식문서에서 문법을 알려주는 표이다. 한 번 분석해보자.
- annotation
- provider와 마찬가지로 notifier를 사용하려면 @riverpod || @Riverpod으로 어노테이션 작성 해야 한다. provider는 함수이기에 함수위에 어노테이션을 작성하였지만 notifier는 클래스이기에 클래스위에 배치한다.
- Notifier
- 클래스위에 @riverpod이 어노테이션되면 해당 클래스를 Notifier라고 한다.
- 해당 클래스는 extends _$NotifierName을 해야 한다.
- Notifier 클래스는 provider의 상태(state)를 수정하는 메서드를 노출해야 한다. 이 클래스의 공개 메서드는 **ref.read(yourProvider.notifier).yourMethod()**를 사용하여 consumer가 액세스할 수 있다.
- The build method
- 모든 notifiers는 build 메서드를 재정의(override)해야 한다.
- 이 메서드는 일반적으로 notifier가 아닌 provider(non-notifier provider)에서 로직을 넣는 위치에 해당한다.
- 이 메서드는 직접 호출하지 않는다.
Notifier 클래스를 만들어서 provider를 notifier로 변환하는 방법을 살펴보자.
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
// 이전에 FutureProvider에 있던 로직이 이제 build 메서드에 있습니다.
return [
Todo(description: 'Learn Flutter', completed: true),
Todo(description: 'Learn Riverpod'),
];
}
}
주의 사항
- notifier class의 생성자에 로직을 넣으면 안된다.
- ref 및 기타 프로퍼티는 아직 사용할 수 없으므로 Notifier에는 생성자가 없어야하고 build 메서드에 로직을 넣는다.
// 예시코드
class MyNotifier extends ... {
MyNotifier() {
// ❌ 이렇게 하지 마세요.
// 이 경우 예외가 발생합니다.
state = AsyncValue.data(42);
}
@override
Result build() {
// ✅ 대신 이렇게 하세요.
state = AsyncValue.data(42);
}
}
Post 요청을 수행하는 메서드 노출하기
이제 Notifier가 생겼으니 side-effect를 수행할 수 있는 메서드를 추가 할 수 있다. 추가해보자.
@riverpod // notifiers 클래스로 하기 위해 어노테이션 해야 한다.
class TodoList extends _$TodoList { // extends _$notifier로 해야 한다.
@override
Future<List<Todo>> build() async => [/* ... */];
// side effect
Future<void> addTodo(Todo todo) async {
await http.post(
Uri.https('your_api.com', '/todos'),
// We serialize our Todo object and POST it to the server.
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
}
}
그런 다음 첫 번째 provider/네트워크 요청 만들기에서 보았던 것과 동일한 Consumer/ConsumerWidget을 사용하여 UI에서 이 메서드를 호출할 수 있다.
아래 코드는 한 위젯 클래스가 ConsumerWidget을 extends하여 사용하고 있어 WidgetRef를 사용할 수 있다.
ref에서 이전에 만든 addTodo라는 메서드를 사용할 수 있다.
class Example extends ConsumerWidget {
const Example({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// Using "ref.read" combined with "myProvider.notifier", we can
// obtain the class instance of our notifier. This enables us
// to call the "addTodo" method.
ref
.read(todoListProvider.notifier)
.addTodo(Todo(description: 'This is a new todo'));
},
child: const Text('Add todo'),
);
}
}
ref.watch와 ref.read의 차이점
- ref.watch
- 특정 상태(state)를 구독(subscribe)하는데 사용한다.
- 메서드를 호출하면 해당 상태가 변경될 때마다 위젯이 자동으로 다시 빌드된다.
- 상태가 변경되면 해당 위젯이 다시 빌드되고, UI가 항상 최신 상태를 반영한다. 즉, 항상 Data State(상태)가 UI에 최신 상태를 보여주려면 watch를 해야 한다.
- ref.read
- 특정 상태를 읽지만, 구독하지 않습니다
- 상태(state)가 변경되어도 해당 위젯은 다시 빌드되지 않습니다.
- 이벤트 핸들러(예: 버튼 클릭 시)와 같이 상태를 읽고 그 값을 기반으로 로직을 수행할 때 사용한다. 예를 들어, 화면에 표시하지않고 로직에만 사용되는 Data는 UI를 다시 빌드할 필요가 없기 때문에 ref.read가 적합한다.
- 상태를 읽는 순간의 값을 가져오며, 이후 상태가 변경되더라도 해당 위젯은 영향을 받지 않는다.
- 결론
- 즉, 메서드를 호출할 때 ref.watch 대신 ref.read를 사용하고 있다,물론 ref.watch도 기술적으로는 작동할 수 있지만, "onPressed"와 같은 이벤트 핸들러에서 로직이 수행될 때는 ref.read를 사용하는 것이 좋다. 이런 차이점을 알아 잘 사용하게되면 불필요한 리빌드를 방지할 수 있다.
1. API 응답에 맞춰 로컬 캐시 업데이트하기
이제 버튼을 누르면 POST 요청을 보내게 된다.
아직 UI를 업데이트 하지 않고 있기에 로컬 캐시가 서버 상태와 일치하게 만들도록 해보자.
Future<void> addTodo(Todo todo) async {
// POST 요청은 새 애플리케이션 상태와 일치하는 List<Todo>를 반환합니다.
final response = await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// API 응답을 디코딩하여 List<Todo>로 변환합니다.
List<Todo> newTodos = (jsonDecode(response.body) as List)
.cast<Map<String, Object?>>()
.map(Todo.fromJson)
.toList();
// 새 상태와 일치하도록 로컬 캐시를 업데이트합니다.
// 이렇게 하면 모든 리스너에게 알림이 전송됩니다.
state = AsyncData(newTodos);
}
위의 코드는 POST 요청을 보내면 새 할 일을 추가하고 그 이후, 새 할 일 목록을 반환한다.
이를 하는 방법이 state = AsyncData(newTodos) 이다.
장점
- UI는 가능한 가장 최신 상태로 유지하기에 다른 사용자가 할 일을 추가하면 우리도 볼 수 있다.
- 이 접근 방식을 사용하면 클라이언트는 할 일 목록에서 새 할 일을 어디에 삽입해야 하는지 알 필요가 없다.
- 로컬 캐시를 업데이트 하기에 단 한 번의 네트워크 요청만 필요하다.
단점
- 이 접근 방식은 서버가 특정 방식으로 구현된 경우에만 작동한다. 서버가 새 상태를 반환하지 않으면 이 접근 방식은 작동하지 않는다.
- 필터/소팅이 있는 경우와 같이 연결된 GET 요청이 더 복잡한 경우에는 여전히 작동하지 않을 수 있다.
2. ref.invalidateSelf()`를 사용하여 provider를 새로고침
- provider를 새로 고치기 위해 ref.invalidateSelf()를 사용할 수 있다.
- 한 가지 방법은 POST 요청을 수행한 후에 GET 요청을 다시 실행하도록 프로바이더가 재실행되도록 하는
- 이는 POST 요청 후에 ref.invalidateSelf()를 호출함으로써 가능하다.
Future<void> addTodo(Todo todo) async {
// We don't care about the API response
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// Once the post request is done, we can mark the local cache as dirty.
// This will cause "build" on our notifier to asynchronously be called again,
// and will notify listeners when doing so.
ref.invalidateSelf();
// (Optional) We can then wait for the new state to be computed.
// This ensures "addTodo" does not complete until the new state is available.
await future;
}
장점
- UI는 가능한 가장 최신 상태로 유지된다.
- 이 접근 방식을 사용하면 클라이언트는 할 일 목록에서 새 할 일을 어디에 삽입해야 하는지 알 필요가 없다.
- 이 접근 방식은 서버 구현에 관계없이 작동한다. 필터/소팅이 포함된 경우와 같이 GET 요청이 더 복잡한 경우에 특히 유용할 수 있다.
단점
- 추가적으로 GET 요청을 수행한다.
3. 로컬 캐시 수동 업데이트
또 다른 방법으로는 로컬 캐시를 수동으로 업데이트하는 방법이다.
백엔드가 새 항목을 처음에 삽입하는지 아니면 마지막에 삽입하는지에 따라 달라진다.
Future<void> addTodo(Todo todo) async {
// API 응답은 중요하지 않습니다.
await http.post(
Uri.https('your_api.com', '/todos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(todo.toJson()),
);
// 그런 다음 로컬 캐시를 수동으로 업데이트할 수 있습니다.
// 이를 위해서는 이전 상태를 가져와야 합니다.
// 주의: 이전 상태가 여전히 로딩 중이거나 오류 상태일 수 있습니다.
// 이를 처리하는 우아한 방법은 `this.state` 대신
// `this.future`를 읽어서 로딩 상태를 기다리게 하고
// 상태가 오류 상태인 경우 오류를 던지는 것입니다.
final previousState = await future;
// 그런 다음 새 상태 객체를 생성하여 상태를 업데이트할 수 있습니다.
// 그러면 모든 리스너에게 알림이 전송됩니다.
state = AsyncData([...previousState, todo]);
}
final previousState = await future;
// 이전 할 일 목록을 변경합니다.
previousState.add(todo);
// 리스너에게 수동으로 알림을 보냅니다.
ref.notifyListeners();
장점
- 이 접근 방식은 서버 구현에 관계없이 작동이 된다.
- 네트워크 요청은 단 한 번만 필요하다.
단점
- 로컬 캐시가 서버의 상태와 일치하지 않을 수 있다. 다른 사용자가 할 일을 추가한 경우 이를 볼 수 없다.
- 이 접근 방식은 백엔드의 로직을 효과적으로 복제하고 구현하기가 더 복잡할 수 있다.
스피너 표시 및 오류 처리(error handling)
이제 POST 요청 버튼을 누르면 요청이 수행되고 있는 표시(Spinner)를 보여주고 요청이 실패하게 된다면 오류처리를 해보겠습니다.
여러 방법이 있지만 그 중 한 가지 방법은 컬 위젯 상태에 addTodo가 반환한 Future를 저장한 다음 해당 Future를 수신하여 스피너 또는 오류 메시지를 표시하는 것이다.
아래 화면을 보면 요청하는동안 spinner가 표시된다.
class Example extends ConsumerStatefulWidget {
const Example({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ExampleState();
}
class _ExampleState extends ConsumerState<Example> {
// 보류중(pending)인 addTodo 작업입니다. 또는 보류중인 작업이 없는 경우 null입니다.
Future<void>? _pendingAddTodo;
@override
Widget build(BuildContext context) {
return FutureBuilder(
// 보류 중인 작업을 수신하여 그에 따라 UI를 업데이트합니다.
future: _pendingAddTodo,
builder: (context, snapshot) {
// 오류 상태가 있는지 여부를 계산합니다.
// 연결 상태 확인은 연산을 다시 시도할 때 처리하기 위해 여기에 있습니다.
final isErrored = snapshot.hasError && snapshot.connectionState != ConnectionState.waiting;
return Row(
children: [
ElevatedButton(
style: ButtonStyle(
// 오류가 있는 경우 버튼이 빨간색으로 표시됩니다.
backgroundColor: WidgetStatePropertyAll(
isErrored ? Colors.red : null,
),
),
onPressed: () {
// addTodo가 반환한 future를 변수에 보관합니다.
final future = ref
.read(todoListProvider.notifier)
.addTodo(Todo(description: 'This is a new todo'));
// 그 future를 로컬 상태(state)에 저장합니다.
setState(() {
_pendingAddTodo = future;
});
},
child: const Text('Add todo'),
),
// 작업이 보류 중입니다. 진행률 표시기를 표시해 보겠습니다.
if (snapshot.connectionState == ConnectionState.waiting) ...[
const SizedBox(width: 8),
const CircularProgressIndicator(),
]
],
);
},
);
}
}
위의 코드를 한 번 분석해보자.
- ConsumerStatefulWidget
- Widget은 ConsumerStatefulWidget을 extends(상속)하여 ConsumerStatefulWidget 클래스의 모든 속성과 메서드를 사용할 수 있게 한다.
- snapshot
- builder에 snapshot을 넣어 오류 상태가 있는지 여부를 계산한다.
- 작업 연결 요청 중 일때는 Spinner를 표시한다.
- final isErrored = snapshot.hasError && snapshot.connectionState != ConnectionState.waiting;
- future
- addTodo가 반환한 future를 변수에 보관한다.
- 그 future를 로컬 상태(state)에 저장한다.
Passing arguments to your requests
이제 HTTP 요청은 외부 매개변수에 의존하는 경우가 많다.
예를 들어, 이전에는 사용자에게 무작위 액티비티를 제안하기 위해 Bored API를 사용했다.
하지만 사용자가 원하는 액티비티 타입을 필터링하거나 가격 요구 사항 등을 원할 수 있다.
이러한 매개변수는 미리 알 수 없기에 매개변수를 UI에서 providers에 전달하는 방법이 필요하다.
providers에 arguments를 허용하기
아래코드는 이전 provider, notifier에서 사용한 것과 같이 정의했다.
// "함수형" provider
@riverpod
Future<Activity> activity(Ref ref) async {
// TODO: 네트워크 요청을 수행하여 활동을 가져옵니다.
return fetchActivity();
}
// 또는 "notifier"
@riverpod
class ActivityNotifier2 extends _$ActivityNotifier2 {
@override
Future<Activity> build() async {
// TODO: 네트워크 요청을 수행하여 활동을 가져옵니다.
return fetchActivity();
}
}
provider에게 매개변수(parameters)를 전달하려면 어노테이션에 달린 함수 자체에 매개변수를 추가하면 된다.
아래 코드는 원하는 액티비티 타입에 해당하는 String 인수를 받도록 provider를 업데이트하는 코드이다.
@riverpod
Future activity(
Ref ref,
// provider에 인수를 추가할 수 있습니다.
// 매개변수의 유형은 원하는 대로 지정할 수 있습니다.
String activityType,
) async {
// "activityType" 인수를 사용하여 URL을 작성할 수 있습니다.
// "<https://boredapi.com/api/activity?type=>"을 가리키게 됩니다.
final response = await http.get(
Uri(
scheme: 'https',
host: 'boredapi.com',
path: '/api/activity',
// 쿼리 매개변수를 수동으로 인코딩할 필요 없이 "Uri" 클래스가 자동으로 인코딩합니다.
queryParameters: {'type': activityType},
),
);
final json = jsonDecode(response.body) as Map<string, dynamic="">;
return Activity.fromJson(json);
}
@riverpod
class ActivityNotifier2 extends _$ActivityNotifier2 {
/// Notifier arguments는 build 메서드에 지정됩니다.
/// 원하는 개수만큼 지정할 수 있고, 이름도 지정할 수 있으며, optional/named도 지정할 수 있습니다.
@override
Future build(String activityType) async {
// 인수는 "this."으로도 사용할 수 있습니다.
print(this.activityType);
// TODO: 네트워크 요청을 수행하여 활동을 가져옵니다.
return fetchActivity();
}
}
</string,>
즉, providers 함수에는 parameter를 추가해주는 것이고 notifier class에서는 빌드 메서드에 인자로 추가하여 사용할 수 있다.
arguments를 전달하게 UI 변경하기
아래 코드는 위젯이 provider(제공)를 consume(소비)하는 syntax구문이다.
AsyncValue<Activity> activity = ref.watch(activityProvider);
하지만 위의 코드는 provider에 arguments를 받지 않는 코드이다.
요청된 매개변수를 사용하여 호출하는 함수를 작성해보자.
AsyncValue<Activity> activity = ref.watch(
// 이제 provider는 activity type을 기대하는 함수입니다.
// 단순화를 위해 지금은 상수 문자열을 전달하겠습니다.
activityProvider('recreational'),
);
provider에게 전달된 매개변수(parameters)는 어노테이션이 달린 함수의 매개변수에서 "ref" 매개변수를 뺀 값에 해당한다.
서로 다른 arguments를 가진 동일한 provider를 동시에 수신하는 방법
아래 코드는 recreational Activity 와 cooking Activity를 모두 렌더링(데이터를 시각적으로 편하는 과정)하는 방법이다.
return Consumer(
builder: (context, ref, child) {
final recreational = ref.watch(activityProvider('recreational'));
final cooking = ref.watch(activityProvider('cooking'));
// 그러면 두 활동을 모두 렌더링할 수 있습니다.
// 두 요청이 모두 병렬로 발생하고 올바르게 캐시됩니다.
return Column(
children: [
Text(recreational.valueOrNull?.activity ?? ''),
Text(cooking.valueOrNull?.activity ?? ''),
],
);
},
);
캐싱 고려 사항 및 매개변수 제한 사용
매개변수를 providers에 전달할 때 계산은 여전히 캐시된다.
차이점은 계산이 인수별로 캐시된다는 것이다.
즉, 두 개의 위젯이 동일한 매개변수를 동일한 provider를 사용하는 경우 네트워크 요청은 한번만 이루어진다.
하지만 두 위젯이 서로 다른 매개변수를 가진 동일한 provider를 사용하는 경우 두 번의 네트워크 요청이 이루어진다.
Riverpod은 매개변수의 == 연산자에 의존한다. 즉, provider에게 전달되는 매개변수가 일관된 동일성을 갖는 것이 중요하다.
주의할 점
흔히 저지르는 실수는 새 객체가 ==를 재정의하지 않는데도 새 객체를 provider의 매개변수로 직접 인스턴스화하는 것이다. 예를 들어, List를 이렇게 전달하고 싶은 유혹을 받을 수 있습니다:
// 대신 문자열 목록을 허용하도록 activityProvider를 업데이트할 수 있습니다.
// 그런 다음 감시 호출에서 직접 해당 목록을 만들 수 있습니다.
ref.watch(activityProvider(['recreational', 'cooking']));
위 코드의 문제점은 ['recreational', 'cooking'] == ['recreational', 'cooking']가 false라는 것.
따라서 Riverpod은 두 매개변수가 다르다고 판단하고 새로운 네트워크 요청을 시도하게 되고 네트워크 요청이 무한 반복되어 사용자에게 진행률 표시기가 영구적으로 표시된다.
이 문제를 해결하려면 const (const ['recreational', 'cooking'])를 사용하거나 ==를 재정의하는 사용자 정의 목록 구현을 사용하면 된다.
Passing arguments to your requests
이제 HTTP 요청은 외부 매개변수에 의존하는 경우가 많다.
예를 들어, 이전에는 사용자에게 무작위 액티비티를 제안하기 위해 Bored API를 사용했다.
하지만 사용자가 원하는 액티비티 타입을 필터링하거나 가격 요구 사항 등을 원할 수 있다.
이러한 매개변수는 미리 알 수 없기에 매개변수를 UI에서 providers에 전달하는 방법이 필요하다.
providers에 arguments를 허용하기
아래코드는 이전 provider, notifier에서 사용한 것과 같이 정의했다.
// "함수형" provider
@riverpod
Future<Activity> activity(Ref ref) async {
// TODO: 네트워크 요청을 수행하여 활동을 가져옵니다.
return fetchActivity();
}
// 또는 "notifier"
@riverpod
class ActivityNotifier2 extends _$ActivityNotifier2 {
@override
Future<Activity> build() async {
// TODO: 네트워크 요청을 수행하여 활동을 가져옵니다.
return fetchActivity();
}
}
provider에게 매개변수(parameters)를 전달하려면 어노테이션에 달린 함수 자체에 매개변수를 추가하면 된다.
아래 코드는 원하는 액티비티 타입에 해당하는 String 인수를 받도록 provider를 업데이트하는 코드이다.
@riverpod
Future activity(
Ref ref,
// provider에 인수를 추가할 수 있습니다.
// 매개변수의 유형은 원하는 대로 지정할 수 있습니다.
String activityType,
) async {
// "activityType" 인수를 사용하여 URL을 작성할 수 있습니다.
// "<https://boredapi.com/api/activity?type=>"을 가리키게 됩니다.
final response = await http.get(
Uri(
scheme: 'https',
host: 'boredapi.com',
path: '/api/activity',
// 쿼리 매개변수를 수동으로 인코딩할 필요 없이 "Uri" 클래스가 자동으로 인코딩합니다.
queryParameters: {'type': activityType},
),
);
final json = jsonDecode(response.body) as Map<string, dynamic="">;
return Activity.fromJson(json);
}
@riverpod
class ActivityNotifier2 extends _$ActivityNotifier2 {
/// Notifier arguments는 build 메서드에 지정됩니다.
/// 원하는 개수만큼 지정할 수 있고, 이름도 지정할 수 있으며, optional/named도 지정할 수 있습니다.
@override
Future build(String activityType) async {
// 인수는 "this."으로도 사용할 수 있습니다.
print(this.activityType);
// TODO: 네트워크 요청을 수행하여 활동을 가져옵니다.
return fetchActivity();
}
}
</string,>
즉, providers 함수에는 parameter를 추가해주는 것이고 notifier class에서는 빌드 메서드에 인자로 추가하여 사용할 수 있다.
arguments를 전달하게 UI 변경하기
아래 코드는 위젯이 provider(제공)를 consume(소비)하는 syntax구문이다.
AsyncValue<Activity> activity = ref.watch(activityProvider);
하지만 위의 코드는 provider에 arguments를 받지 않는 코드이다.
요청된 매개변수를 사용하여 호출하는 함수를 작성해보자.
AsyncValue<Activity> activity = ref.watch(
// 이제 provider는 activity type을 기대하는 함수입니다.
// 단순화를 위해 지금은 상수 문자열을 전달하겠습니다.
activityProvider('recreational'),
);
provider에게 전달된 매개변수(parameters)는 어노테이션이 달린 함수의 매개변수에서 "ref" 매개변수를 뺀 값에 해당한다.
서로 다른 arguments를 가진 동일한 provider를 동시에 수신하는 방법
아래 코드는 recreational Activity 와 cooking Activity를 모두 렌더링(데이터를 시각적으로 편하는 과정)하는 방법이다.
return Consumer(
builder: (context, ref, child) {
final recreational = ref.watch(activityProvider('recreational'));
final cooking = ref.watch(activityProvider('cooking'));
// 그러면 두 활동을 모두 렌더링할 수 있습니다.
// 두 요청이 모두 병렬로 발생하고 올바르게 캐시됩니다.
return Column(
children: [
Text(recreational.valueOrNull?.activity ?? ''),
Text(cooking.valueOrNull?.activity ?? ''),
],
);
},
);
캐싱 고려 사항 및 매개변수 제한 사용
매개변수를 providers에 전달할 때 계산은 여전히 캐시된다.
차이점은 계산이 인수별로 캐시된다는 것이다.
즉, 두 개의 위젯이 동일한 매개변수를 동일한 provider를 사용하는 경우 네트워크 요청은 한번만 이루어진다.
하지만 두 위젯이 서로 다른 매개변수를 가진 동일한 provider를 사용하는 경우 두 번의 네트워크 요청이 이루어진다.
Riverpod은 매개변수의 == 연산자에 의존한다. 즉, provider에게 전달되는 매개변수가 일관된 동일성을 갖는 것이 중요하다.
주의할 점
흔히 저지르는 실수는 새 객체가 ==를 재정의하지 않는데도 새 객체를 provider의 매개변수로 직접 인스턴스화하는 것이다. 예를 들어, List를 이렇게 전달하고 싶은 유혹을 받을 수 있습니다:
// 대신 문자열 목록을 허용하도록 activityProvider를 업데이트할 수 있습니다.
// 그런 다음 감시 호출에서 직접 해당 목록을 만들 수 있습니다.
ref.watch(activityProvider(['recreational', 'cooking']));
위 코드의 문제점은 ['recreational', 'cooking'] == ['recreational', 'cooking']가 false라는 것.
따라서 Riverpod은 두 매개변수가 다르다고 판단하고 새로운 네트워크 요청을 시도하게 되고 네트워크 요청이 무한 반복되어 사용자에게 진행률 표시기가 영구적으로 표시된다.
이 문제를 해결하려면 const (const ['recreational', 'cooking'])를 사용하거나 ==를 재정의하는 사용자 정의 목록 구현을 사용하면 된다.
결론
Riverpod에서 HTTP 요청에 매개변수를 전달하는 방법을 알아보았다. provider 함수나 notifier의 build 메서드에 매개변수를 추가하여 구현할 수 있으며, UI에서는 provider 호출 시 해당 매개변수를 전달하면 된다. 중요한 점은 매개변수별로 계산이 캐시되며, 동일한 매개변수를 사용할 경우 네트워크 요청이 한 번만 이루어진다는 것이다. 하지만 매개변수로 새 객체를 직접 인스턴스화할 때는 주의가 필요하다. == 연산자를 재정의하지 않은 객체를 사용하면 무한 네트워크 요청이 발생할 수 있으므로, const 사용이나 == 연산자를 재정의하는 등의 적절한 처리가 필요하다.
'Flutter' 카테고리의 다른 글
[Flutter] RiverPod - provider 알아보기(with 네트워크 요청) (0) | 2025.01.06 |
---|---|
[Flutter] RiverPod 학습 - State management (0) | 2025.01.03 |
[Flutter] [Error Handling] Result Pattern (간편로그인 적용) (0) | 2024.12.26 |
[Flutter] [Error Handling] Result Pattern (1) | 2024.12.24 |
[Flutter] [Firbase] 간편 로그인 구현하기(Apple🍎) (0) | 2024.12.24 |