(Flutter) Riverpod의 Modifiers

반응형


Riverpod는 family와 autoDispose 두 가지 수식어(modifiers)를 제공합니다. 이 글에서는 family와 autoDispose 수식어에 대해서 설명합니다.

family

family 의 목적은 외부 파라미터를 전달하여 고유한 프로바이더를 가져오는 것입니다.

일반적인 family 의 사용 사례는 다음과 같습니다.

  • FutureProviderfamily를 결합하여 ID에서 Message를 가져오는 경우.
  • 번역을 처리하기 위해 현재 Locale 값을 프로바이더로 전달하는 경우.

사용방법

family 수식어를 사용하여 프로바이더를 생성하면 파라미터가 추가됩니다. 그러면 프로바이더는 이 파라미터를 사용하여 일부 상태를 계산하는 요소로 사용할 수 있습니다.

예를 들어 FutureProviderfamily 수식어를 사용하면, id값을 외부에서 전달받아 Message를 가져올 수 있습니다.

final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

그리고 messagesFamily 프로바이더를 사용할 때 문법(syntax)이 약간 달라집니다. 일반적인 문법(syntax)은 더 이상 작동하지 않습니다.

Widget build(BuildContext context, WidgetRef ref) {
  // 에러 – messagesFamily는 프로바이더가 아닙니다.
  final response = ref.watch(messagesFamily);
}

다음과 같이 messagesFamily에 파라미터를 전달해야 합니다.

Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}

[!info] 정보
family 를 사용하는 프로바이더에 서로 다른 파라미터를 동시에 전달하는 것도 가능합니다.
예를 들어, titleFamily 프로바이더에 각기 다른 Locale를 파라미터로 전달하여 프랑스어와 영어를 동시에 읽을 수 있습니다.

@override
Widget build(BuildContext context, WidgetRef ref) {
  final frenchTitle = ref.watch(titleFamily(const Locale('fr')));
  final englishTitle = ref.watch(titleFamily(const Locale('en')));

  return Text('fr: $frenchTitle en: $englishTitle');
}

파라미터 제한하기

family가 올바르게 작동하려면 프로바이더에 전달되는 파라미터의 hashCode==가 일치하는 것이 중요합니다. 파라미터는 primitive(bool/int/double/String), constant(providers) 또는 ==hashCode를 오버라이드 할 수 있는 불변(immutable) 객체여야 하는 것이 가장 이상적입니다.

  • primitive 자료형: 컴퓨터 프로그램을 만드는 데 사용되는 기초적인 언어 구성.

[중요] 파라미터가 일정하지 않은 경우에는 autoDispose를 사용하세요.

family를 사용하여 검색 필드에 입력된 값을 프로바이더의 파라미터로 전달할 수 있습니다. 하지만 이 값은 자주 변경될 수 있고, 그 값이 다시는 재사용되지 않을 수도 있습니다. 프로바이더가 더 이상 사용되지 않더라도 기본적으로 프로바이더는 절대 해제(destroyed)되지 않기 때문에 메모리 누수(memory leaks)가 발생할 수 있습니다.

familyautoDispose를 함께 사용하면 메모리 누수가 해결됩니다:

final characters = FutureProviderautoDispose.family<List<Character>, String>((ref, filter) async {
  return fetchCharacters(filter: filter);
});

멀티 파라미터를 프로바이더에 전달하기

family는 기본적으로 멀티 파라미터를 프로바이더에 전달하는것이 불가능합니다. family로 생성한 프로바이더에는 하나의 파라미터만 전달 가능합니다.

하지만 아래의 패키지를 이용하면 여러 값을 하나의 객체에 담아 프로바이더에 전달 할 수 있습니다.

다음은 Freezedequatable를 사용한 샘플 코드입니다.

Freezed

@freezed
abstract class MyParameter with _$MyParameter {
  factory MyParameter({
    required int userId,
    required Locale locale,
  }) = _MyParameter;
}

final exampleProvider = ProviderautoDispose.family<Something, MyParameter>((ref, myParameter) {
  print(myParameter.userId);
  print(myParameter.locale);
  // userId/locale를 사용하여 무언가를 처리
});

@override
Widget build(BuildContext context, WidgetRef ref) {
  int userId; // userId를 어디선가 읽음. 
  final locale = Localizations.localeOf(context);

  final something = ref.watch(
    exampleProvider(MyParameter(userId: userId, locale: locale)),
  );

  ...
}

Equatable

class MyParameter extends Equatable  {
  MyParameter({
    required this.userId,
    required this.locale,
  });

  final int userId;
  final Locale locale;

  @override
  List<Object> get props => [userId, locale];
}

final exampleProvider = Provider.family<Something, MyParameter>((ref, myParameter) {
  print(myParameter.userId);
  print(myParameter.locale);
  // userId/locale를 사용하여 무언가를 처리
});

@override
Widget build(BuildContext context, WidgetRef ref) {
  int userId; // userId를 어디선가 읽음. 
  final locale = Localizations.localeOf(context);

  final something = ref.watch(
    exampleProvider(MyParameter(userId: userId, locale: locale)),
  );

  ...
}

autoDispose

autoDispose의 사용 목적은 더 이상 사용하지 않는 프로바이더를 해제(destroy)하는 것입니다.

일반적인 autoDispose 의 사용 사례는 다음과 같습니다.

  • Firebase를 사용할 때 불필요한 비용 발생을 피하기 위해 연결(connection)을 끊는 경우
  • 사용자가 화면을 나갔다가 다시 들어왔을 때 상태를 초기화 해야하는 경우

사용방법

더 이상 사용하지 않는 프로바이더를 자동으로 해제(destroy)하도록 지시하려면, 프로바이더에 autoDispose 를 추가하기만 하면 됩니다.

final userProvider = StreamProvider.autoDispose<User>((ref) {
	// ⋯
});

이게 다입니다. 이제 userProvider가 더 이상 사용되지 않으면 자동으로 소멸됩니다.

제네릭 파라미터가 autoDispose 이전이 아닌 autoDispose 이후에 전달되는 방식에 주목하세요. autoDispose는 명명된 생성자(a named constructor)가 아닙니다.

[!note] 노트
필요한 경우 autoDipose를 다른 수식어와 결합할 수 있습니다.

final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {
  // ⋯
});

ref.keepAlive

프로바이더에 autoDispose 수식어를 사용하면 ref 객체에 keepAlive 메소드가 추가됩니다.

keepAlive 메소드는 프로바이더가 더 이상 사용되지 않더라도 상태를 유지하도록 Riverpod에 알리는 데 사용됩니다.

다음은 HTTP 요청이 완료된 후 keepAlive 메소드를 실행하는 예제 코드입니다.

final myProvider = FutureProvider.autoDispose((ref) async {
  final response = await httpClient.get(...);
  ref.keepAlive();
  return response;
});

이렇게 하면 HTTP 요청이 완료되기 전, 사용자가 화면을 떠났다가 다시 들어오면 HTTP 요청이 다시 실행됩니다. 그러나 만약 HTTP 요청이 성공적으로 완료된다면 상태는 유지되고 사용자가 화면에 재 진입해도 새로운 요청이 트리거되지는 않습니다.

[!info] 정보
버전 1.0.x에서 keepAlive에 해당하는 속성은 maintainState라는 속성입니다.

예시: 프로바이더를 더 이상 사용하지 않을 때 HTTP 요청 취소하기

FutureProvider와 ref.onDispose를 결합하여 프로바이더가 더 이상 필요하지 않을 때 HTTP 요청을 쉽게 취소할 수 있습니다.
 
우리의 목표는 다음과 같습니다.

  • 사용자가 화면에 들어왔을 때 HTTP 요청을 시작합니다.
  • 만약 HTTP 요청이 완료되기 전에 사용자가 화면을 나가면 HTTP 요청을 취소합니다.
  • 만약 HTTP 요청이 성공했다면, 화면을 나갔다가 다시 들어와도 새로운 요청을 시작하지 않습니다.

코드로 구현해 본다면 아래와 같습니다.

final myProvider = FutureProvider.autoDispose((ref) async {
  // http 요청을 취소할 수 있는 package:dio의 객체
  final cancelToken = CancelToken();
  // 프로바이더가 해제된 경우, HTTP 요청을 취소합니다.
  ref.onDispose(() => cancelToken.cancel());

  // 데이터를 가져오고 취소하기 위한 'cancelToken'을 전달합니다.
  final response = await dio.get('path', cancelToken: cancelToken);
  // 만약 HTTP 요청이 성공적으로 완료되었다면 상태를 유지합니다.
  ref.keepAlive();
  return response;
});

인수 유형 'AutoDisposeProvider’는 매개 변수 유형 ‘AlwaysAliveProviderBase’ 에 할당할 수 없습니다.

autoDispose를 사용할 때, 아래와 같은 유사한 에러와 함께 컴파일이 되지 않는 상황이 발생할 수 있습니다.

The argument type ‘AutoDisposeProvider’ can’t be assigned to the parameter type ‘AlwaysAliveProviderBase’

걱정할 필요가 없습니다! 이 에러는 버그가 발생할 가능성이 높기 때문에 발생하는 오류입니다.

원인은 다음과 같이 autoDispose를 사용하지 않은 프로바이더에서 autoDispose를 사용한 프로바이더를 구독하려고 시도한 경우에 발생할 수 있습니다.

final firstProvider = Provider.autoDispose((ref) => 0);

final secondProvider = Provider((ref) {
  // 'AutoDisposeProvider<int>'인자 값을
  // 'AlwaysAliveProviderBase<Object, Null>' 파라미터 타입으로 할당할 수 없습니다. 
  ref.watch(firstProvider);
});

위의 코드의 경우에는 firstProvider가 절대로 해제(disposed)되지 않으므로 바람직하지 않습니다.

이 문제를 해결하려면, 다음과 같이 secondProvider에도 autoDispose를 표시하는 것이 좋습니다.

final firstProvider = Provider.autoDispose((ref) => 0);

final secondProvider = Provider.autoDispose((ref) {
  ref.watch(firstProvider);
});


or

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

반응형