본문으로 바로가기
  1. Home
  2. Flutter/Package
  3. Flutter Riverpod 상태관리 (3) - Provider 유저를 위한 Riverpod

Flutter Riverpod 상태관리 (3) - Provider 유저를 위한 Riverpod

· 댓글개 · Dev_Whale

Provider 유저를 위한 Riverpod

이 문서는 Provider 패키지에 익숙한 분들 중 Riverpod에 대해 배우고자 하는 분들을 위해 작성되었습니다.

Riverpod과 Provider 간의 관계

Riverpod는 Provider의 뒤를 잇는 프로그램으로 설계되었습니다. 따라서 "'Riverpod"라는 이름은 "Provider"의 애너그램입니다.

Riverpod는 Provider가 직면한 다양한 기술적 한계에 대한 해결책을 모색하는 과정에서 탄생했습니다. 원래 Riverpod는 이 문제를 해결하기 위한 방법으로 Provider의 주요 버전이 될 예정이었습니다. 그러나 이는 상당히 큰 변화이며, Provider는 가장 많이 사용 되는 Flutter 패키지 중 하나이기 때문에 반대하기로 결정했습니다.

 

하지만 개념적으로 Riverpod과 Provider는 상당히 유사합니다.
두 패키지 모두 비슷한 역할을 수행합니다. 둘 다 다음을 시도합니다:

  • 일부 상태 저장 객체를 캐시하고 폐기합니다.
  • 테스트 중에 해당 객체를 모의(mock)하는 방법을 제공합니다.
  • 위젯이 간단한 방법으로 해당 객체를 수신할 수 있는 방법을 제공합니다.

동시에 Riverpod이 몇 년 동안 계속 발전했다면 어떤 Provider가 될 수 있었을지 생각해 보세요.

 

Riverpod은 다음과 같은 Provider의 여러 가지 근본적인 문제를 해결합니다:

  • " provider"의 결합을 대폭 간소화합니다. Riverpod는 ref.watch및 ref.listen와 같은 간단하면서도 효과적인 기능들을 제공합니다.
  • 여러 "provider"가 동일한 유형의 값을 노출할 수 있습니다.
    따라서 일반 int나 String을 사용해도 잘 작동할 때 사용자 정의 클래스를 정의할 필요가 없습니다.
  • 테스트 내에서 Provider를 다시 정의할 필요가 없습니다. Riverpod를 사용하면 기본적으로 테스트 내부에서 Provider를 사용할 수 있습니다.
  • 객체를 폐기하는 대체 방법을 제공함으로써 객체를 폐기하는 "범위 지정"에 대한 과도한 의존도 감소(자동 폐기,autoDispose) 강력한 기능이지만, 프로바이더 범위를 지정하는 것은 상당히 복잡하고 올바르게 설정하기 어렵습니다.

Riverpod의 유일한 단점은 작동하려면 위젯 유형을 변경해야 한다는 것입니다 :

  • Riverpod를 사용하면 StatelessWidget을 확장(extend)을 하는 대신 ConsumerWidget을 확장(extend)해야 합니다.
  • Riverpod를 사용하면 StatefulWidget을 확장(extend)하는 대신 ConsumerStatefulWidget을 확장(extend)해야 합니다.

그러나 이러한 불편함은 큰 틀에서 보면 상당히 사소한 부분입니다. 그리고 언젠가는 이 요구 사항이 사라질 수도 있습니다.

 

따라서 다음과 같은 질문에 답하기 위해 스스로에게 물어볼 수 있습니다 :

Provider를 사용해야 하나요, 아니면 Riverpod를 사용해야 하나요?

 

아마 Riverpod을 사용해야 할 것입니다.

Riverpod은 더 나은 설계를 통해 로직을 대폭 간소화할 수 있습니다.

Provider와 Riverpod의 차이점

Provider 정의 방법

두 패키지의 주요 차이점은 " provider"가 정의되는 방식입니다.

Provider에서는 Provider가 위젯이므로 위젯 트리 내부(일반적으로 MultiProvider 내부)에 배치됩니다:

class Counter extends ChangeNotifier {
 ...
}

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<Counter>(create: (context) => Counter()),
      ],
      child: MyApp(),
    )
  );
}

Riverpod에서 Provider는 위젯이 아닙니다. 대신 일반 다트 객체입니다.
마찬가지로 provider는 위젯 트리 외부에서 정의되며, 대신 전역 최종 변수로 선언됩니다.

또한 Riverpod가 작동하려면 전체 애플리케이션 위에 ProviderScope 위젯을 추가해야 합니다. 따라서 Riverpod를 사용하는 Provider 예제와 동일합니다:

// Providers are now top-level variables
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());

void main() {
  runApp(
    // This widget enables Riverpod for the entire project
    ProviderScope(
      child: MyApp(),
    ),
  );
}

provider의 정의가 조금 올라간 것만 봐도 알 수 있습니다.

INFO)
Riverpod provider는 일반 Dart 객체이므로 Flutter 없이도 Riverpod를 사용할 수 있습니다. 예를 들어, 명령줄 애플리케이션을 작성하는 데 Riverpod를 사용할 수 있습니다.

Reading providers: BuildContext

Provider를 사용하면 providers를 읽는 한 가지 방법은 위젯의 BuildContext를 사용하는 것입니다.

예를 들어, provider가 다음과 같이 정의된 경우:

Provider<Model>(...);

설정하면 Provider를 사용하여 읽기가 완료됩니다:

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Model model = context.watch<Model>();
    
  }
}

Riverpod에서 이에 해당하는 것은 다음과 같습니다:

final modelProvider = Provider<Model>(...);

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    Model model = ref.watch(modelProvider);

  }
}

방법:

  • Riverpod의 스니펫은 StatelessWidget 대신 ConsumerWidget을 확장합니다. 위젯 유형이 다르면 빌드 함수에 매개변수(WidgetRef)가 하나 더 추가됩니다.
  • Riverpod에서는 BuildContext.watch 대신 ConsumerWidget에서 가져온 WidgetRef를 사용하여 WidgetRef.watch를 수행합니다.
  • Riverpod는 제네릭 타입에 의존하지 않습니다. 대신 provider 정의를 사용하여 생성된 변수에 의존합니다.

문구가 얼마나 비슷한지도 주목하세요. Provider와 Riverpod는 모두 "watch" 키워드를 사용하여 "이 위젯은 값이 변경되면 다시 빌드해야 합니다"라고 설명합니다.

Riverpod는 Provider와 동일한 용어를 사용하여 Provider를 읽습니다.
BuildContext.watch -> WidgetRef.watch
BuildContext.read -> WidgetRef.read

context.watch와 context.read에 대한 규칙은 Riverpod에도 적용됩니다: build 메서드 내에서 "watch"를 사용합니다. 클릭 핸들러 및 기타 이벤트 내부에서는 "read"를 사용합니다.

Reading providers: Consumer

Provider는 선택적으로 Provider를 읽기 위해 Consumer라는 위젯(및 Consumer2와 같은 변형)과 함께 제공됩니다.

 

Consumer는 위젯 트리를 보다 세부적으로 리빌드하여 상태가 변경될 때 해당 위젯만 업데이트할 수 있으므로 성능 최적화에 도움이 됩니다.

따라서 provider가 다음과 같이 정의된 경우:

Provider<Model>(...);

Provider는 Consumer를 사용하여 해당 Provider를 읽을 수 있도록 허용합니다:

Consumer<Model>(
  builder: (BuildContext context, Model model, Widget? child) {

  }
)

Riverpod도 같은 원리를 가지고 있습니다. Riverpod에도 똑같은 목적을 위해 Consumer라는 위젯이 있습니다.

Provider를 다음과 같이 정의했다면:

final modelProvider = Provider<Model>(...);

그런 다음 Consumer를 사용하면 됩니다:

Consumer(
  builder: (BuildContext context, WidgetRef ref, Widget? child) {
    Model model = ref.watch(modelProvider);

  }
)

Consumer가 어떻게 WidgetRef 객체를 제공하는지 주목하세요. 이것은 이전 파트에서 ConsumerWidget과 관련된 것과 동일한 객체입니다.

Provider 결합하기: stateless 객체와 ProxyProvider 결합

Provider를 사용할 때 Provider를 결합하는 공식적인 방법은 ProxyProvider 위젯(또는 ProxyProvider2와 같은 변형)을 사용하는 것입니다.

예를 들어 다음과 같이 정의할 수 있습니다:

class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),

거기에서 두 가지 옵션이 있습니다. UserIdNotifier를 결합하여 새로운 " stateless" provider(일반적으로 ==를 재정의할 수 있는 불변 값)를 생성할 수 있습니다. 예를 들어:

ProxyProvider<UserIdNotifier, String>(
  update: (context, userIdNotifier, _) {
    return 'The user ID of the the user is ${userIdNotifier.userId}';
  }
)

ProxyProvider는 UserIdNotifier.userId가 변경될 때마다 자동으로 새 문자열을 반환합니다.

 

Riverpod에서도 비슷한 작업을 수행할 수 있지만 문법이 다릅니다.

먼저, Riverpod에서는 UserIdNotifier의 정의가 다음과 같습니다:

class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
  (ref) => UserIdNotifier(),
);

거기에서 userId를 기반으로 문자열을 생성하려면 다음과 같이 할 수 있습니다:

final labelProvider = Provider<String>((ref) {
  UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
  return 'The user ID of the the user is ${userIdNotifier.userId}';
});

ref.watch(userIdNotifierProvider)를 수행하는 줄을 주목하세요.

이 코드 줄은 Riverpod가 userIdNotifierProvider의 콘텐츠를 가져오고 해당 값이 변경될 때마다 labelProvider도 다시 계산하도록 명령합니다. 따라서 labelProvider가 내보내는 문자열은 userId가 변경될 때마다 자동으로 업데이트됩니다.

이 ref.watch 줄도 비슷하게 느껴질 것입니다. 이 패턴은 앞서 위젯 내부에서 providers를 읽는 방법을 설명할 때 다루었습니다. 실제로 provider는 이제 위젯이 하는 것과 같은 방식으로 다른 provider를 수신할 수 있습니다.

Provider 결합하기: stateful 객체가 있는 ProxyProvider

Provider를 결합할 때 또 다른 대안적인 사용 사례는 ChangeNotifier 인스턴스와 같은 상태 저장 객체를 노출하는 것입니다.

 

이를 위해 ChangeNotifierProxyProvider(또는 ChangeNotifierProxyProvider2 같은 변형)를 사용할 수 있습니다.
예를 들어 다음과 같이 정의할 수 있습니다:

class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),

그런 다음 UserIdNotifier.userId를 기반으로 하는 새로운 ChangeNotifier를 정의할 수 있습니다. 예를 들어 다음과 같이 할 수 있습니다:

class UserNotifier extends ChangeNotifier {
  String? _userId;

  void setUserId(String? userId) {
    if (userId != _userId) {
      print('The user ID changed from $_userId to $userId');
      _userId = userId;
    }
  }
}

// ...

ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
  create: (context) => UserNotifier(),
  update: (context, userIdNotifier, userNotifier) {
    return userNotifier!
      ..setUserId(userIdNotifier.userId);
  },
);

 

이 새 provider는 UserNotifier의 단일 인스턴스(재구성[re-constructed]되지 않음)를 생성하고 사용자 ID가 변경될 때마다 문자열을 출력합니다.

 

provider에서 동일한 작업을 수행하는 방식은 다릅니다. 먼저, Riverpod에서는 UserIdNotifier의 정의가 다음과 같습니다:

class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// ...

final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
  (ref) => UserIdNotifier(),
),

거기에서 ChangeNotifierProxyProvider는 이전과 동일합니다:

class UserNotifier extends ChangeNotifier {
  String? _userId;

  void setUserId(String? userId) {
    if (userId != _userId) {
      print('The user ID changed from $_userId to $userId');
      _userId = userId;
    }
  }
}

// ...

final userNotifierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
  final userNotifier = UserNotifier();
  ref.listen<UserIdNotifier>(
    userIdNotifierProvider,
    (previous, next) {
      if (previous?.userId != next.userId) {
        userNotifier.setUserId(next.userId);
      }
    },
  );

  return userNotifier;
});

이 스니펫의 핵심은 ref.listen 줄입니다.
이 ref.listen 함수는 providers를 수신하고 providers가 변경될 때마다 함수를 실행할 수 있는 유용한 함수입니다.

해당 함수의 previous 및 next 매개변수는 provider가 변경되기 전의 마지막 값과 변경된 후의 새 값에 해당합니다.

💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

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