주요 콘텐츠로 건너뛰기

Provider

provider는 Riverpod의 핵심 기능입니다. Riverpod을 쓴다는 것은 곧 provider를 쓴다는 뜻입니다.

provider란?​

provider는 본질적으로 편의 기능을 더한 "메모이즈된(memoized) 함수"입니다.
즉, 같은 매개변수로 호출하면 캐시된 값을 반환하는 함수입니다.

provider의 가장 흔한 사용 사례는 네트워크 요청입니다.
API에서 사용자 정보를 가져오는 함수를 생각해 봅시다.

Future<User> fetchUser() async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}

이 함수를 위젯 안에서 사용하려면 한 가지 문제가 있습니다. 결과를 직접 캐시해야 하고, 그 값이 필요한 모든 위젯이 공유할 방법도 따로 마련해야 합니다.

바로 여기서 provider가 등장합니다. provider는 함수를 감싸는 래퍼입니다. 함수의 결과를 캐시하고, 여러 위젯이 같은 값에 접근할 수 있게 해 줍니다.

// fetchUser 함수와 같지만, 결과가 캐시됩니다.
// userProvider를 여러 번 사용해도 같은 값을 반환합니다.
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
});

provider는 기본적인 캐싱 외에도 더 강력하게 쓸 수 있도록 다양한 기능을 제공합니다.

  • 내장된 캐시 무효화 메커니즘
    특히 Ref.watch를 사용하면 여러 캐시를 조합하고, 필요한 부분을 자동으로 무효화할 수 있습니다.
  • 자동 폐기
    provider는 더 이상 필요하지 않은 리소스를 자동으로 해제할 수 있습니다.
  • 데이터 바인딩
    provider를 사용하면 FutureBuilder나 StreamBuilder가 필요 없습니다.
  • 자동 에러 처리
    provider는 에러를 자동으로 잡아서 UI에 전달할 수 있습니다.
  • 모킹 지원
    테스트 등의 목적으로 모든 provider를 모킹할 수 있습니다.Provider 오버라이드를 참고하세요.
  • 오프라인 영속성 (실험적)
    provider의 결과를 디스크에 저장해 두었다가, 앱을 다시 시작할 때 자동으로 불러올 수 있습니다.
  • Mutation (실험적)
    provider는 폼 제출 같은 부수 효과를 처리할 때 UI가 로딩 스피너나 에러 상태를 표시할 수 있는 방법을 기본으로 제공합니다.

provider는 6가지 종류가 있습니다.

동기(Synchronous)FutureStream
수정 불가ProviderFutureProviderStreamProvider
수정 가능NotifierProviderAsyncNotifierProviderStreamNotifierProvider

처음에는 복잡해 보일 수 있습니다. 하나씩 살펴보겠습니다.

Sync vs Future vs Stream:
표의 각 열은 함수에 쓰이는 Dart의 내장 타입을 나타냅니다.

int synchronous() => 0;
Future<int> future() async => 0;
Stream<int> stream() => Stream.value(0);

수정 불가 vs 수정 가능:
기본적으로 provider는 위젯이 수정할 수 없습니다. provider의 "Notifier" 변형을 사용하면 외부에서 수정할 수 있습니다.
이는 private setter("수정 불가" provider)와

// _state는 내부에서는 수정할 수 있지만
// 외부에서는 수정할 수 없습니다
var _state = 0;
int get state => _state;

public setter("수정 가능" provider)의 차이와 비슷합니다.

// 누구나 "state"를 수정할 수 있습니다
var state = 0;
정보

원리상 수정 불가와 수정 가능은 각각 StatelessWidget과 StatefulWidget에 대응한다고 볼 수도 있습니다.

provider는 위젯이 아니고 두 종류 모두 "상태"를 저장하므로 정확한 비유는 아닙니다. 하지만 "하나의 불변 객체" vs "두 개의 가변 객체"라는 원리는 비슷합니다.

provider 만들기​

provider는 "최상위(top-level)"에 선언해야 합니다.
즉, 어떤 클래스나 함수에도 속하지 않은 바깥에 선언해야 합니다.

provider를 만드는 문법은 위 표에서 본 것처럼 "수정 가능"인지 "수정 불가"인지에 따라 달라집니다.

final name = SomeProvider.someModifier<Result>((ref) {
  <your logic here>
});
provider 변수

이 변수를 통해 provider와 상호작용합니다.
변수는 final이어야 하며 "최상위"(전역)에 선언해야 합니다.

참고

provider가 전역이라고 걱정할 필요는 없습니다. provider는 완전히 불변입니다. provider를 선언하는 것은 함수를 선언하는 것과 다르지 않으며, 테스트와 유지보수도 쉽습니다.

provider 타입

보통 Provider, FutureProvider, StreamProvider 중 하나입니다.
어떤 provider 타입을 사용할지는 함수의 반환값에 따라 결정됩니다. 예를 들어 Future<Activity>를 만들려면 FutureProvider<Activity>를 사용합니다.

가장 많이 쓰게 될 것은 FutureProvider입니다.

팁

"어떤 provider를 골라야 할까"라고 고민하지 마세요. 대신 "무엇을 반환하고 싶은가"를 생각하세요. provider 타입은 자연스럽게 정해집니다.

수식자 (선택 사항)

provider 타입 뒤에 "수식자(modifier)"가 붙는 경우가 많습니다.
수식자는 선택 사항이며, 타입 안전한 방식으로 provider의 동작을 조정하는 데 사용합니다.

현재 사용할 수 있는 수식자는 두 가지입니다.

  • autoDispose: provider가 더 이상 사용되지 않으면 캐시를 자동으로 비웁니다. 자동 폐기도 참고하세요.
  • family: provider에 인자를 전달할 수 있게 합니다. Family도 참고하세요.
Ref

다른 provider와 상호작용할 때 사용하는 객체입니다.
모든 provider가 ref를 가지고 있으며, provider 함수의 매개변수로 받거나 Notifier의 프로퍼티로 접근합니다.

provider 함수

provider의 로직을 작성하는 곳입니다. 이 함수는 provider를 처음 읽을 때 호출됩니다.
이후에 다시 읽을 때는 함수를 호출하지 않고 캐시된 값을 반환합니다.

정보

provider는 제한 없이 원하는 만큼 선언할 수 있습니다. package:provider와 달리, Riverpod에서는 같은 "타입"의 상태를 노출하는 provider를 여러 개 만들 수 있습니다.

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

두 provider가 모두 String을 만든다는 점은 아무 문제가 되지 않습니다.

provider 사용하기​

provider는 그 자체로는 아무 일도 하지 않는다는 점에서 위젯과 비슷합니다.
위젯이 UI를 기술하듯, provider는 상태를 기술합니다.
놀랍게도 provider는 완전히 stateless이며, 문법이 조금 더 장황해지는 문제만 없었다면 const로 인스턴스화할 수도 있었을 것입니다.

provider를 사용하려면 별도의 객체인 ProviderContainer가 필요합니다. 자세한 내용은 ProviderContainers/ProviderScopes를 참고하세요.

간단히 말해, provider를 사용하기 전에 Flutter 앱을 ProviderScope로 감싸야 합니다.

void main() {
runApp(ProviderScope(child: MyApp()));
}

그다음 provider와 상호작용하려면 Ref를 얻어야 합니다. Ref에 대한 자세한 내용은 Ref를 참고하세요.

요약하면 Ref를 얻는 방법은 두 가지입니다.

  • provider는 자연스럽게 ref에 접근할 수 있습니다.
    provider 함수의 첫 번째 매개변수나 Notifier의 ref 프로퍼티가 바로 그것입니다. 이를 통해 provider끼리 서로 통신할 수 있습니다.
  • 위젯 트리에서는 Consumer라고 부르는 특별한 위젯이 필요합니다. 이 위젯은 WidgetRef를 제공하여 위젯 트리와 provider 트리를 연결해 줍니다.

예를 들어 간단한 문자열을 반환하는 helloWorldProvider가 있다고 해 봅시다. 위젯 안에서는 다음과 같이 사용할 수 있습니다.

class Example extends StatelessWidget {

Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, _) {
// provider의 값을 가져옵니다
final helloWorld = ref.watch(helloWorldProvider);

// 가져온 값을 UI에 사용합니다
return Text(helloWorld);
},
);
}
}