Flutter 애플리케이션에서 상태 관리를 쉽게 관리할 수 있도록 도와주는 반응형 캐싱 프레임워크이다.
Riverpod은 상태관리를 위해 사용하는 주요 라이브러리인 Provider의 한계를 극복하고 개선된 기능을 제공하기 위한 목적으로 개발되었다.
Riverpod의 주요 특징은 다음과 같다.
Riverpod은 모든 provider를 선언적으로 정의하고, 타입 안정성을 제공하기 때문에 컴파일 타임에 오류를 발견할 수 있다.
provider의 종류로는 다음과 같다.
Future
객체를 반환하는 provider이다. API 호출이나, 데이터베이스 쿼리와 같은 비동기 작업에 사용된다.StateNotifier
를 통해 상태를 관리하고, 변경사항을 전파할 수 있다.pubspec.yaml에 다음과 같이 의존성을 추가한 후 flutter pub get
으로 다운로드한다.
name: my_app_name
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
build_runner:
custom_lint:
riverpod_generator: ^2.4.2
riverpod_lint: ^2.3.12
Riverpod은 riverpod_lint 패키지를 제공한다. lint를 통해 코드 작성에 린트 규칙을 적용할 수 있다.
프로젝트에 analysis_options.yaml
파일을 생성하고 다음의 내용을 추가한다.
analyzer:
plugins:
- custom_lint
위 내용을 추가하고 나면 코드 작성 시 Riverpod 관련 코드에서 lint가 경고를 안내한다.
lint에 대한 규칙은 아래의 riverpod_lint pub dev 페이지를 참고하자.
https://pub.dev/packages/riverpod_lint
Flutter Riverpod Snippets를 사용하여 간단한 스니펫으로 Riverpod 기능을 사용할 수 있다.
Riverpod을 통해 상태 데이터를 사용하는 기본적인 방법은 다음과 같다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
void main() {
runApp(
// 1. root 위젯에서 providers를 관리할 수 있게 전체 앱을 ProviderScope으로 감싼다.
// 이렇게하면 애플리케이션 전체에 Riverpod이 활성화된다.
const ProviderScope(
child: MyApp(),
),
);
}
// 2. provider를 생성한다.
String helloWorld(HelloWorldRef ref) {
return "Hello, World!";
}
// 3. Riverpod에서 관리되는 ConsumerWidget을 확장한다.
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final String word = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("example"),
),
body: Center(
child: Text(word),
),
),
);
}
}
위 코드를 작성하면 빨간 밑줄이 나타나는데, 터미널에 다음의 명령어를 입력하여 Code generator를 통해 main.g.dart
파일을 생성한다.
$ fvm flutter pub run build_runner watch
Deprecated. Use `dart run` instead.
Building package executable...
Built build_runner:build_runner.
[INFO] Generating build script completed, took 159ms
[INFO] Setting up file watchers completed, took 6ms
[INFO] Waiting for all file watchers to be ready completed, took 201ms
[INFO] Reading cached asset graph completed, took 106ms
[INFO] Checking for updates since last build completed, took 607ms
[INFO] Running build completed, took 6ms
[INFO] Caching finalized dependency graph completed, took 37ms
[INFO] Succeeded after 46ms with 0 outputs (0 actions)
[INFO] ------------------------------------------------------------------------
[INFO] Starting Build
[INFO] Updating asset graph completed, took 3ms
[WARNING] source_gen:combining_builder on lib/main.dart:
main.g.dart must be included as a part directive in the input library with:
part 'main.g.dart';
[INFO] Running build completed, took 8.3s
[INFO] Caching finalized dependency graph completed, took 59ms
[INFO] Succeeded after 8.4s with 519 outputs (1038 actions)
[INFO] ------------------------------------------------------------------------
[INFO] Starting Build
[INFO] Updating asset graph completed, took 2ms
[INFO] Running build completed, took 36ms
[INFO] Caching finalized dependency graph completed, took 67ms
[INFO] Succeeded after 107ms with 2 outputs (2 actions)
이후 프로젝트를 에뮬레이터에 실행할 수 있고, lib
디렉토리 하위에 다음과 같이 main.g.dart
파일이 생성된다.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$helloWorldHash() => r'2ed8016b6f9c8762482dd2701393dd3d69c48550';
/// See also [helloWorld].
(helloWorld)
final helloWorldProvider = AutoDisposeProvider<String>.internal(
helloWorld,
name: r'helloWorldProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$helloWorldHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef HelloWorldRef = AutoDisposeProviderRef<String>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@riverpod
: @riverpod
, @Riverpod()
로 어노테이션을 부착해야 한다. String helloWorld(...) {}
: helloWorld
함수의 이름에 따라 Code generator를 통해 helloWorldProvider
변수가 생성된다.Future<User> fetchUser(FetchUserRef ref) {}
Riverpod을 사용하여 네트워크 통신을 수행하는 간단한 앱을 만들어보며 각 요소들을 알아보자.
가장먼저 내 앱에 Riverpod을 구성하기 위해서 root 위젯을 ProviderScope
위젯으로 감싸야 한다.
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
ProviderScope 위젯으로 감싸면 모든 영역에서 provider에 접근할 수 있게 된다.
Riverpod을 사용하는 주요 목적 중 하나는 복잡해져가는 앱의 비즈니스 로직과 UI로직의 분리와 간편한 상태 관리라 할 수 있다.
비즈니스 로직과 UI 로직을 분리하기 위해 다음과 같이 디렉토리를 생성하자.
libs
/providers
/models
/views
이제 각각 디렉토리에 해당하는 파일들을 생성하며 앱을 만들어보자.
provider는 네트워크 통신과 같은 비즈니스 로직을 담당한다.
회원들의 프로필 목록을 불러오는 API가 있다고 가정하고 fetch 기능을 구현한다.
/// user_provider.dart
'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_provider.g.dart';
// provider임을 명시하기 위해 @riverpod 어노테이션을 부착한다.
Future<List<Profile>> fetchProfiles(FetchProfilesRef ref) async {
// 1. API를 통해 회원들의 프로필 목록을 불러온다.
final response = await http.get(
Uri.parse(
'https://gist.githubusercontent.com/'
'cafefarm-johnny/'
'ad6ca270049e3fbd24be0cae2e20fd6d/'
'raw/'
'e20114cc476ddb54cdc33bf2223dfc55c74074a3/'
'chat_app_peoples_data.json',
),
);
if (kDebugMode) {
print('status code: ${response.statusCode}');
print('response body: ${response.body}');
}
// 2. 불러온 데이터를 데이터 형식에 맞게 적절한 자료구조로 파싱한다.
final json = jsonDecode(response.body) as Map<String, dynamic>;
final rawProfiles = json['Johnny'] as List<dynamic>;
if (rawProfiles.isEmpty) {
return List.empty();
}
// 3. raw 데이터를 모델 데이터로 변환하여 반환한다.
return rawProfiles.map((rawProfile) => Profile.fromJson(rawProfile)).toList();
}
@riverpod
어노테이션을 부착한 함수에서 반환되는 값에 대해 상태 관리를 시작한다.
@riverpod
어노테이션은 top-level 함수에만 부착이 가능하다. 따라서 top-level이 클래스인 경우 클래스 내부의 메서드에는 부착할 경우 오류가 발생한다.
네트워크로 조회해온 데이터를 바인딩할 모델을 정의한다.
class Profile {
const Profile({
required this.url,
required this.nickname,
required this.statusMessage,
});
final String url;
final String nickname;
final String statusMessage;
factory Profile.fromJson(Map<String, dynamic> json) {
return Profile(
url: json['url'] as String,
nickname: json['nickname'] as String,
statusMessage: json['statusMessage'] as String,
);
}
}
불러온 정보를 ListView로 간단하게 보여주는 UI를 작성하자.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class UserList extends StatelessWidget {
const UserList({super.key});
Widget build(BuildContext context) {
// 1. Consumer 위젯을 통해 Ref에 접근하고
return Consumer(
builder: (context, ref, child) {
// 2. 필요로 하는 provider를 호출한다.
final AsyncValue<List<Profile>> profiles = ref.watch(
fetchProfilesProvider,
);
return Center(
// 3. 패턴매칭을 사용하여 각 상태에 따른 위젯을 반환한다.
child: switch (profiles) {
// 3-1. API 통신에 성공했다면 ListView를 반환한다.
AsyncData(:final value) => ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(value[index].url),
),
title: Text(value[index].nickname),
subtitle: Text(value[index].statusMessage),
);
},
),
// 3-2. API 통신에 실패했다면 에러 메시지를 반환한다.
AsyncError() => const Text('Error occur!'),
// 3-3. 통신 과정 중이라면 로딩 인디케이터를 반환한다.
_ => const CircularProgressIndicator(),
},
);
},
);
}
}
이제 앱을 실행해보면 다음과 같이 확인할 수 있다.
위 과정을 수행하여 앱을 실행해보며 테스트해보면 몇 가지 특징을 알 수 있다.
UI로직에서 provider를 통해 비즈니스 로직을 호출하기 위해서는 WidgetRef
라는 객체를 필요로 한다. provider를 정의할 때는 ref 객체를 매개변수로 정의하기 때문에 접근할 수 있다. UI에서는 ref 객체에 어떻게 접근할 수 있을까?
Riverpod은 WidgetRef
객체에 접근하기 위해 Consumer
라는 위젯을 제공한다. Consumer
위젯으로 렌더링할 위젯을 감싸면 ref 객체에 접근할 수 있게되고, 이를 통해 provider에 접근할 수 있게 된다.
class Consumer extends ConsumerWidget {
/// {@macro riverpod.consumer}
const Consumer({super.key, required ConsumerBuilder builder, Widget? child})
: _child = child,
_builder = builder;
final ConsumerBuilder _builder;
final Widget? _child;
Widget build(BuildContext context, WidgetRef ref) {
return _builder(context, ref, _child);
}
}
위의 View에서 작성했던 코드를 다시 보자.
build(BuildContext context) {
// 1. Consumer 위젯을 통해 Ref에 접근하고
return Consumer(
builder: (context, ref, child) {
// 2. 필요로 하는 Provider를 호출한다.
final AsyncValue<List<Profile>> profiles = ref.watch(
fetchProfilesProvider,
);
return Center(...);
},
);
}
Widget
정리하면
WidgetRef
객체에 접근해야 한다.WidgetRef
객체에 접근할 수 있게 Consumer
위젯을 제공한다.Consumer
위젯은 WidgetRef
에 접근하기 위해 제공되는 위젯이지만 다른 위젯을 감싸서 사용하기 때문에 들여쓰기가 깊어진다는 문제가 발생한다.
이를 해결하기 위해 ConsumerWidget
, ConsumerStatefulWidget
을 제공한다. 이 위젯들은 StatelessWidget
, StatefulWidget
과 Consumer
위젯을 결합한 위젯이다.
사용 방법은 다음과 같다.
// 1. StatelessWidget 대신 ConsumerWidget을 확장한다.
class UserList extends ConsumerWidget {
const UserList({super.key});
// 2. Consumer를 통해 Ref에 접근했던 방식에서
// build 메서드를 오버라이딩하며 접근할 수 있게 되었다.
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<Profile>> profiles = ref.watch(
fetchProfilesProvider,
);
// UI 로직은 변경 없이 Consumer만 제거되었다.
return Center(...);
}
}
이전과 다르게 extends StatelessWidget
이 extends ConsumerWidget
으로 변경되었고, build()
메서드 내부에 Consumer
위젯이 제거되어 들여쓰기가 줄어들었다.
마찬가지로 StatefulWidget
에 해당하는 위젯은 ConsumerStatefulWidget
을 사용하면 된다.
// 1. StatefulWidget 대신 ConsumerStatefulWidget을 확장한다.
class UserList extends ConsumerStatefulWidget {
const UserList({super.key});
ConsumerState createState() => _UserListState();
}
// 2. State 대신 ConsumerState를 확장한다.
class _UserListState extends ConsumerState<UserList> {
void initState() {
super.initState();
// 3. 라이프사이클 과정에서 ConsumerState의 프로퍼티로써 WidgetRef에 접근할 수 있다.
ref.listenManual(fetchProfilesProvider, (previous, next) {
// provider에 리스너를 등록하여 스낵바 등을 구현할 수 있다.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('최신 친구 목록을 조회했습니다.'),
),
);
});
}
// 3. ConsumerStatefulWidget에서는 Ref가 build 메서드의 매개변수로 제공되지 않고,
// ConsumerState의 프로퍼티로써 접근할 수 있다.
Widget build(BuildContext context) {
final AsyncValue<List<Profile>> profiles = ref.watch(
fetchProfilesProvider,
);
return Center(...);
}
}
지금까지는 외부에서 데이터를 가져오고 상태 데이터를 생성하는 과정이었다. 하지만 앱은 데이터를 가져오기만 할 뿐 만 아니라 수정, 삭제등의 행위를 통해 데이터를 변경하며 UI를 갱신해야 한다.
UI 정보를 갱신하기 위해서는 Consumer위젯이 상태 데이터를 바라보며 상태 데이터의 변경을 감지할 수 있어야 한다.
provider는 관심사 분리를 보장하기 위해 자신의 상태 데이터를 변경할 수 있는 방법을 Consumer로 제공하지 않는다. 대신 Notifier
를 통해 상태 데이터를 변경할 수 있는 방법을 제공한다.
이전에 작성한 top-level 함수가 정의되어 있던 provider를 top-level 클래스로 변경하여 Notifier
를 확장해야 한다.
part 'user_provider.g.dart';
/// 네트워크 처리는 비즈니스 로직에 해당한다.
/// Riverpod에서 비즈니스 로직은 provider 내부에 작성한다.
// 1. provider를 class로 승격하여 Notifier로 변경한다.
class UserNotifier extends _$UserNotifier {
// 2. 이전에 있었던 fetchProfilesProvider 데이터 fetch 로직을 build 메서드로 이동한다.
Future<List<Profile>> build() async {
return await _fetchProfiles();
}
Future<List<Profile>> _fetchProfiles() async {
...
}
}
Notifier
: @riverpod
어노테이션을 클래스 레벨에 부착하면 클래스를 Notifier로 승격된다. _$Notifier
를 확장해야 한다.WidgetRef.read(provider.notifier).methodName()
를 사용하여 액세스 할 수 있다.build()
: build
메서드를 override 해야 한다.build
메서드는 절대 직접 호출하면 안된다.⚠️ 주의사항
notifier 생성자에서는ref
및 기타 프로퍼티에 접근할 수 없기 때문에 생성자 내부에 로직을 작성하면 예외가 발생한다.class MyNotifier extends $_MyNotifier { MyNotifier() { // ❌ 이렇게 하지 마세요. // 이 경우 예외가 발생합니다. state = AsyncValue.data(42); } Result build() { // ✅ 대신 이렇게 하세요. state = AsyncValue.data(42); } }
위의 앱에서 floating 버튼을 하나 생성하고 클릭하면 새로운 사용자가 목록에 추가되도록 사용자를 추가하는 기능을 구현하며 상태 변수를 갱신하는 방법을 알아보자.
가장 먼저 Notifier에 프로필 추가 기능을 구현하자.
part 'user_provider.g.dart';
class UserNotifier extends _$UserNotifier {
// 1. 외부 데이터 베이스를 흉내내기 위해 데이터 저장 변수를 선언한다.
final List<Profile> _db = [];
Future<List<Profile>> build() async {
return await _fetch();
}
Future<List<Profile>> _fetch() async {
...
if (rawProfiles.isEmpty) {
return List.empty();
}
// 3. build 메서드가 re-build될 때 데이터가 갱신될 수 있도록
// addAll() 메서드로 신규 데이터를 추가하여 반환한다.
return rawProfiles
.map((rawProfile) => Profile.fromJson(rawProfile))
.toList()
..addAll(_db);
}
// 2. Create 로직을 추가한다.
Future<void> addUser(Profile profile) async {
// (저장) like 네트워크 통신 처리
Future.delayed(const Duration(seconds: 3));
_db.add(profile);
// POST 요청이 완료되면 로컬 캐시를 Dirty 상태로 변경한다.
// 로컬 캐시가 Dirty 상태로 변경되면 notifier의 build 메서드가 비동기적으로 재호출되며,
// 리스너들에게 변경사항을 전달한다.
ref.invalidateSelf();
// 상태가 새로 갱신될 때 까지 대기한다.
await future;
}
}
ref.invalidateSelf()
:신규 프로필 정보를 추가하는 Floating 버튼을 구현한다.
class AddUserFloatingButton extends ConsumerStatefulWidget {
const AddUserFloatingButton({super.key});
ConsumerState<AddUserFloatingButton> createState() =>
_AddUserFloatingButtonState();
}
class _AddUserFloatingButtonState extends ConsumerState<AddUserFloatingButton> {
int _count = 0;
void addUserRandomly(WidgetRef ref) {
_count++;
Profile newProfile = Profile(
nickname: 'Jay$_count',
statusMessage: '반가워요!!',
url: 'https://via.placeholder.com/200/000000/FFFFFF/?text=Jay$_count',
);
// ref.read를 사용하여 notifier에 접근한다.
// notifier에 접근하여 addUser 메서드를 호출한다.
ref.read(userNotifierProvider.notifier).addUser(newProfile);
}
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () => addUserRandomly(ref),
child: const Icon(Icons.add),
);
}
}
Notifier의
addUser()
를 호출할 때ref.watch
가 아닌ref.read
를 호출한다.
ref.watch
도 기술적으로는 동작할 수 있지만, onPressed와 같은 이벤트 핸들러에서 로직이 수행되는 경우에는ref.read
를 사용하는 것이 좋다.
데이터를 조회해오는 API는 흔히 페이지네이션 매개변수를 가진다.
Consumer에서 Notifier 또는 provider로 매개변수를 전달해야 페이지네이션 처리가 가능해지는데 이를 어떻게 구현하는지 알아보자.
part 'user_provider.g.dart';
class UserNotifier extends _$UserNotifier {
final List<Profile> _db = [];
// 매개변수를 받고자 하면 그냥 build 메서드 시그니처에 매개변수를 선언하면 된다.
Future<List<Profile>> build([int size = 0]) async {
return await _fetch(size);
}
Future<List<Profile>> _fetch(int size) async {
...
}
}
Notifier 또는 provider에서는 매개변수를 받고자하는 경우 해당하는 build 메서드나 함수에 매개변수를 정의하면 된다.
이후 UI에서 Notifier 또는 provider를 호출하는 로직에서 매개변수를 전달하면 된다.
build(BuildContext context) {
final AsyncValue<List<Profile>> profiles = ref.watch(
userNotifierProvider(5), // 매개변수 전달
);
}
Widget
여기서 캐싱과 관련된 몇가지 특징이 나타나는데 그것은 다음과 같다.
size = 5
매개변수를 전달하면 네트워크 요청이 캐싱되어 이후 size = 5
로 매개변수가 전달되면 캐싱된 응답을 전달한다.Code Generator는 내가 작성한 코드를 기반으로 Riverpod에 필요한 부가적인 코드들을 생산하는 도구이다.
예를 들어 아래와 같이 Notifier 구현 코드를 작성하면 Code Generator가 이를 읽고 해당하는 Dart 코드를 생성해준다.
part 'user_provider.g.dart'; // 반드시 Code Generator가 생성한 파일을 part 문법으로 선언해야지만 Riverpod 코드에 대해 컴파일러가 인식한다.
class UserNotifier extends _$UserNotifier {
final List<Profile> _db = [];
Future<List<Profile>> build([int size = 0]) async {
return await _fetch(size);
}
Future<List<Profile>> _fetch(int size) async {
...
}
Future<void> addUser(Profile profile) async {
...
}
}
위와 같이 짧게 작성한 코드가 실은 Dart 언어로 작성 시 아래와 같이 매우 긴 코드가 되어야 하는 것이다.
// user_provider.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$userNotifierHash() => r'99ab39ac7d8c07e6876affb49c4ed9510089e9cc';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$UserNotifier
extends BuildlessAutoDisposeAsyncNotifier<List<Profile>> {
late final int size;
FutureOr<List<Profile>> build([
int size = 0,
]);
}
/// Copied from [UserNotifier].
(UserNotifier)
const userNotifierProvider = UserNotifierFamily();
/// Copied from [UserNotifier].
class UserNotifierFamily extends Family<AsyncValue<List<Profile>>> {
/// Copied from [UserNotifier].
const UserNotifierFamily();
/// Copied from [UserNotifier].
UserNotifierProvider call([
int size = 0,
]) {
return UserNotifierProvider(
size,
);
}
UserNotifierProvider getProviderOverride(
covariant UserNotifierProvider provider,
) {
return call(
provider.size,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
String? get name => r'userNotifierProvider';
}
/// Copied from [UserNotifier].
class UserNotifierProvider
extends AutoDisposeAsyncNotifierProviderImpl<UserNotifier, List<Profile>> {
/// Copied from [UserNotifier].
UserNotifierProvider([
int size = 0,
]) : this._internal(
() => UserNotifier()..size = size,
from: userNotifierProvider,
name: r'userNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$userNotifierHash,
dependencies: UserNotifierFamily._dependencies,
allTransitiveDependencies:
UserNotifierFamily._allTransitiveDependencies,
size: size,
);
UserNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.size,
}) : super.internal();
final int size;
FutureOr<List<Profile>> runNotifierBuild(
covariant UserNotifier notifier,
) {
return notifier.build(
size,
);
}
Override overrideWith(UserNotifier Function() create) {
return ProviderOverride(
origin: this,
override: UserNotifierProvider._internal(
() => create()..size = size,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
size: size,
),
);
}
AutoDisposeAsyncNotifierProviderElement<UserNotifier, List<Profile>>
createElement() {
return _UserNotifierProviderElement(this);
}
bool operator ==(Object other) {
return other is UserNotifierProvider && other.size == size;
}
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, size.hashCode);
return _SystemHash.finish(hash);
}
}
mixin UserNotifierRef on AutoDisposeAsyncNotifierProviderRef<List<Profile>> {
/// The parameter `size` of this provider.
int get size;
}
class _UserNotifierProviderElement
extends AutoDisposeAsyncNotifierProviderElement<UserNotifier, List<Profile>>
with UserNotifierRef {
_UserNotifierProviderElement(super.provider);
int get size => (origin as UserNotifierProvider).size;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
Code Generator는 이러한 불편한 코드 작성을 편리하게 도와주어 코드 작성 시 개발자가 보일러플레이트를 작성하지 않게 도와준다. 뿐 아니라 아래와 같은 장점도 있다.
example(ExampleRef ref) {
return 'foo';
}
String
class Example extends _$ExampleNotifier {
String build() {
return 'foo';
}
}
<String> example(ExampleRef ref) async {
return Future.value('foo');
}
Future
class Example extends _$ExampleNotifier {
Future<String> build() async {
return Future.value('foo');
}
}
<String> example(ExampleRef ref) async* {
yield 'foo';
}
Stream
class Example extends _$ExampleNotifier {
Stream<String> build() async* {
yield 'foo';
}
}
pkg:Provider
에서는 provider가 위젯이기 때문에 위젯 트리 안에 코드가 배치된다.
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
Riverpod
에서 provider는 위젯이 아닌 Dart 객체이다. 그래서 위젯 트리 외부에서 정의된다.
// Provider는 최상위 변수
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
pkg:Provider
에서는 provider에 접근하기 위해 BuildContext
를 사용한다.
class Example extends StatelessWidget {
Widget build(BuildContext context) {
Model model = context.watch<Model>();
...
}
}
Riverpod
에서는 StatelessWidget 대신 ConsumerWidget
을 확장하며, BuildContext가 아닌 WidgetRef
라는 별도의 객체를 통해 provider에 접근한다.
class Example extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
Model model = ref.watch(modelProvider);
...
}
}
Riverpod은 제네릭 타입에 의존하지 않고, 정의된 provider를 통해 생성된 변수에 의존한다.