Riverpod
How To Install
flutter_riverpod (opens in a new tab)
riverpod_generator (opens in a new tab)
riverpod_annotation (opens in a new tab)
build_runner (opens in a new tab)
dependencies:
flutter:
sdk: flutter
...
riverpod: ^2.3.6
flutter_riverpod: ^2.3.6
riverpod_annotation: ^2.1.1
dev_dependencies:
...
riverpod_generator: ^2.2.3
build_runner: ^2.4.4
Provier의 종류
각각 다른 타입을 반환해주고 사용 목적이 다름, 모든 Provider는 글로벌하게 선언됨.
Provider
- 가장 기본 베이스가 되는 Provider
- 아무 타입이나 반환 가능
- Service, 계산한 값등을 반환할 때 사용
- 반환값을 캐싱할 때 유용하게 사용 (빌드 횟수 최소화 가능)
- 여러 Provider의 값들을 묶어서 한 번에 반환 값을 만들어낼 수 있음
Provider 종류 | 반환 값 | 사용 예제 |
---|---|---|
Provider | 아무 타입 | 데이터 캐싱 |
StateProvider | 아무 타입 | 간단한 상태값 관리 |
StateNotifierProvider | StateNotifier를 상속한 값 반환 | 복잡한 상태값 관리 |
FutureProvider | Future 타입 | API 요청의 Future 결과값 |
StreamProvider | Stream 타입 | API 요청의 Stream 결과값 |
Notifier / AsyncNotifier Provider
riverpod이 2점대로 올라오면서 생긴것 같다. 공식 문서에는
NotifierProvider 는 Notifier 를 수신하고 노출하는 데 사용되는 공급자입니다.
와 같이소개가 되어있다. 이 때, 공식 문서에서 말하는 notifier
란 프로그래밍에서 흔히 상태가 변할 때마다 모든 Observer에 알리는 객체라고 한다.
Notifier Code Generation
아래와 같이 \_$클래스명
을 extends 해주고 @riverpod
Annotation을 붙이고 Code Gen을 하면,
@riverpod
class Todos extends _$Todos {
@override
List<TodoModel> build() {
return [];
}
}
다음과 같이 Code가 생성된다.
Code Gen 결과 👇
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'todo_test.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$todosHash() => r'02f02edc471b5c32f00c34fef375c727690da757';
/// See also [Todos].
@ProviderFor(Todos)
final todosProvider =
AutoDisposeNotifierProvider<Todos, List<TodoModel>>.internal(
Todos.new,
name: r'todosProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$todosHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Todos = AutoDisposeNotifier<List<TodoModel>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member
내가 궁금했던 부분은 이 부분이었다. 공식 문서에서 freezed를 사용하고 있길래 freezed에 의존성을 갖는건가? 하고 생각했는데 그건 아니었다.
간단한 사용법을 보자면 Todos class에 add 함수가 있다고 가정할 때,
@riverpod
class Todos extends _$Todos{
...
void addTodo(Todo todo) {
state = [...state, todo];
}
...
}
Todo를 생성하는 로직을 만들고,
여기서 중요한 것은 .notifier로 provider를 통해서 class 내부에 있는 메서드를 동작시켜줘야 한다는 것이다.
그래야 변경사항이 있는 것에 대해서 리빌드 작업이 일어나기 때문이다.
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(todosProvider.notifier).addTodo(
TodoModel(title: "Some Todo", description: "Hello Todo"),
);
},
child: const Icon(Icons.plus_one),
),
해당 provider를 build 내에서 watch 해줘야 한다.
@override
Widget build(BuildContext context, WidgetRef ref) {
List<TodoModel> todos = ref.watch(todosProvider);
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
...add logic
},
child: const Icon(Icons.plus_one),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
subtitle: Text(todos[index].description),
);
}),
);
}
전체 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'todo_test.g.dart';
class TodoTest extends ConsumerWidget {
static final String routeName = "todo_test";
const TodoTest({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
List<TodoModel> todos = ref.watch(todosProvider);
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(todosProvider.notifier).addTodo(
TodoModel(title: "Some Todo", description: "Hello Todo"),
);
},
child: const Icon(Icons.plus_one),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
subtitle: Text(todos[index].description),
);
}),
);
}
}
@riverpod
class Todos extends _$Todos {
@override
List<TodoModel> build() {
return [];
}
void addTodo(TodoModel todo) {
state = [...state, todo];
}
}
class TodoModel {
final String title;
final String description;
final bool isDone;
TodoModel({
required this.title,
required this.description,
this.isDone = false,
});
TodoModel copyWith({
String? title,
String? description,
bool? isDone,
}) =>
TodoModel(
title: title ?? this.title,
description: description ?? this.description,
isDone: isDone ?? this.isDone,
);
}
StateProvider
- UI에서 "직접적으로" 데이터를 변경할 수 있도록 하고 싶을 때 사용
- 단순한 형태의 데이터만 관리 (int, double, String 등)
- Map, List 등 복잡한 형태의 데이터는 다루지 않음
- 복잡한 로직이 필요한 경우 사용하지 않음 (number++ 정도의 간단한 로직으로만 한정)
final numberProvider = StateProvider<int>((ref) => 0);
사용 방법
예제 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_sample/layout/default_layout.dart';
import 'package:riverpod_sample/riverpod/state_provider.dart';
class StateRiverpodScreen extends ConsumerWidget {
const StateRiverpodScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch, read, listen만 씀
final provider = ref.watch(numberProvider);
return DefaultLayout(
title: 'State Riverpod Screen',
body: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
provider.toString(),
),
ElevatedButton(
onPressed: () {
ref.read(numberProvider.notifier).update((state) => state + 1);
},
child: Text('Up')),
ElevatedButton(
onPressed: () {
ref.read(numberProvider.notifier).state = ref.read(numberProvider.notifier).state - 1;
},
child: Text('Down')),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => _NextScreen()));
},
child: Text('Push'))
],
),
),
);
}
}
class _NextScreen extends ConsumerWidget{
const _NextScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(numberProvider);
return DefaultLayout(
title: 'Next Screen',
body: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
provider.toString(),
),
ElevatedButton(
onPressed: () {
ref.read(numberProvider.notifier).update((state) => state + 1);
},
child: Text('Up'))
],
),
),
);
}
}
StateNotifierProvider
참조
- StateProvider와 마찬가지로 UI에서 직접적으로 데이터를 변경할 수 있도록 하고 싶을 때 사용
- 복잡한 형태의 데이터 관리 가능 (클래스의 메소드를 이용한 상태 관리)
- StateNotifier를 상속한 클래스를 반환
class 형태로 선언해서 사용 함
model 선언
class ShoppingItemModel {
final String name; // 이름
final int quantity; // 갯수
final bool hasBought; // 구매 했는지
final bool isSpicy; // 매운지;
ShoppingItemModel(
{required this.name,
required this.quantity,
required this.hasBought,
required this.isSpicy});
}
StateNotifier 상속받은 class 선언
class ShoppingListNotifier extends StateNotifier<List<ShoppingItemModel>> {
ShoppingListNotifier()
: super([
// ShoppingListProvider 초기화
// ShoppingListProvider 선언시 해당하는 값들을 처음에 사용할 수 잇음
ShoppingItemModel(
name: '김치', quantity: 3, hasBought: false, isSpicy: true),
ShoppingItemModel(
name: '라면', quantity: 5, hasBought: false, isSpicy: true),
ShoppingItemModel(
name: '삼겹살', quantity: 10, hasBought: false, isSpicy: false),
ShoppingItemModel(
name: '수박', quantity: 2, hasBought: false, isSpicy: false),
ShoppingItemModel(
name: '카스테라', quantity: 7, hasBought: false, isSpicy: false),
]);
void toggleHasBought({required String name, }) {
// state는 StateNotifier에 자동으로 제공됨
state = state
.map((e) => e.name == name
? ShoppingItemModel(
name: e.name,
quantity: e.quantity,
hasBought: !e.hasBought,
isSpicy: e.isSpicy)
: e)
.toList();
}
}
Provider로 만들어 주기
// Generic에는 어떤 StateNotifier를 상속한 클래스를 쓸건지넣어주고 해당 Class가 관리하는 상태의 type을 두 번째 Generic으로 넣어줌
final shoppingListNotifier =
StateNotifierProvider<ShoppingListNotifier, List<ShoppingItemModel>>((ref) =>
ShoppingListNotifier());
사용방법
stateless 위젯이 상속받고 있는 위젯을 ConsumerWidget으로 변경해주고 BuildContext 옆에 WidgetRef 추가해줘야 함.
class Test extends ConsumerWidget{ // 여기 변경
const Test({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) { // 여기 변경
return Container();
}
}
사용 방법은 stateProvider랑 크게 다를바는 없음.
// 참조하기
final List<ShoppingItemModel> state = ref.watch(shoppingListNotifier);
// StateNotifier 안에 있는 함수 사용 ex) onPressed 함수 안에 사용
ref.read(shoppingListNotifier.notifier).toggleHasBought(name: e.name);
FutureProvider
- Future 타입만 반환가능
- API 요청의 결과를 반환할 때 자주 사용
- 복잡한 로직 또는 사용자의 특정 행동뒤에 Future를 재실행하는 기능이 없음
사용 방법
선언 :
final multipleFutureProvider = FutureProvider((ref) async {
await Future.delayed(Duration(seconds: 2));
//throw Exception('Error 입니다.');
return [1,2,3,4,5];
});
maybeWhen()
AsyncValue().maybeWhen(
data: (data) {
데이터가 들어왓을 때 실행될 코드
여기서 data는 Future가 벗겨진 값
}, onElse: () {
데이터가 아직 들어오지 않았을 때 실행될 코드
}
)
AsyncValue()는 FutureProviderFamily에 extends 되어 있는 추상 클래스이며, FutureProvider를 watch하거나 read 했을 때 return 되는 타입이다.
사용 :
Provider를 불러오고 .when을 통해서 사용해주는게 특이한데 이는 StreamProvider에서도 똑같음.
class FutureProviderScreen extends ConsumerWidget{
const FutureProviderScreen({super.key}); // (1)
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(multipleFutureProvider);
return DefaultLayout(
title: 'FutureProviderScreen',
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// date - 로딩이 끝나서 데이터가 있을 때 / error - 에러가 있을 때 / loading - 로딩 중일 때 실행
state.when(data: (date) {
return Text(
date.toString(),
textAlign: TextAlign.center,
);
},
error: (err, Stack) => Text(err.toString()),
loading: (() => const Center(child: CircularProgressIndicator())))
],
)
);
}
}
StreamProvider
- Stream 타입만 반환가능
- API 요청의 결과를 Stream으로 반환할 때 자주 사용 (Socker 등)
final multipleStreamProvider = StreamProvider<List<int>>((ref) async* {
for (int i = 0; i < 10; i++) {
await Future.delayed(Duration(seconds: 2));
yield List.generate(3, (index) => index * i);
}
});
class StreamProviderScreen extends ConsumerWidget{
const StreamProviderScreen ({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(multipleStreamProvider);
return DefaultLayout(
title: 'Stream Provier Screen',
body: Center(
child: state.when(
data: (data) {
return Text(data.toString());
},
error: (err, Stack) {
return Text(err.toString());
},
loading: (() => CircularProgressIndicator()),
)),
);
}
}
Family: 인자값 입력받기
선언하는 Provider 뒤에 fmaily 옵션을 붙여서 사용 한다. 이 때 Generic을 원래 선언하던 return type 외에 입력 받을 인자값의 type도 함께 선언함.
final familyModifierProvider = FutureProvider.family<List<int>, int>(
(ref, data) async { // data는 인자값
await Future.delayed(Duration(seconds: 1));
return List.generate(5, (index) => data * index).toList();
},
);
원래는 **watch / read(프로바이더이름)**과 같은 방법으로 선언을 했다면 familyModifier는 **watch / read(프로바이더이름(인자값))**과 같은 형태로 사용함.
final state = ref.watch(familyModifierProvider(5));
.autodispose : 캐시 삭제
provider는 최초 실행되고 나면 실행된 값을 가지고 있는데 autodispose는 메모리에서 실행 결과를 자동으로 dispose 해줘서 실행할 때마다 매번 실행되게 해주는 방법임.
사용방법은 다른 provider와 같음.
final autoDisposeModifierProvider = FutureProvider.autoDispose
<List<int>>((ref) async {
await Future.delayed(Duration(seconds: 1));
return [1,2,3,4,5];
},
);
ChangeNotifierProvider
Provider 패키지에서 마이그레이션 용도
Riverpod Observer
리버팟 옵저버는 Dio의 interceptor와 같이 Provider가 생성, 변경, 삭제 될 때마다 실행되는 함수를 지정할 수 있음.
사용하기 위해서는 ProviderObserver
를 상속받아야 한다.
class Logger extends ProviderObserver {
@override
// update 되었을 때 호출되는 Provider
void didUpdateProvider(ProviderBase provider, Object? previousValue, Object? newValue, ProviderContainer container) {
debugPrint('[Provider Updated] provider: $provider, previousValue: $previousValue, newValue: $newValue, container: $container');
}
@override
// 프로바이더를 추가하면 불리는 함수
void didAddProvider(ProviderBase provider, Object? value, ProviderContainer container) {
debugPrint("[Provider Added] provider: $provider, value: $value, container: $container");
}
@override
// 프로바이더가 삭제되면 불리는 함수
void didDisposeProvider(ProviderBase provider, ProviderContainer container) {
debugPrint("[Provider Disposed] provider: $provider, container: $container");
}
}
Reference
Riverpod Observer Sample Commit (opens in a new tab)
Riverpod Debugger
Riverpod Debugger (opens in a new tab)
Code Generation
riverpod 공식 문서에는 code generation을 만든 이유를 아래 2가지로 설명한다.
- 어떤 프로바이더를 사용할지 고민할 필요가 없도록 하기 위해서 만들어짐
- Parameter를 그대로 일반 함수처럼 사용할 수 있도록하기 위해서 만들어짐
pubspec.yaml
Code Generation 기능이 추가 되었기 때문에 build_runner
와 riverpod_generator
, riverpod_annotation
을 추가해줘야 한다.
flutter_riverpod (opens in a new tab)
riverpod_generator (opens in a new tab)
riverpod_annotation (opens in a new tab)
build_runner (opens in a new tab)
dependencies:
flutter:
sdk: flutter
...
riverpod: ^2.3.6
flutter_riverpod: ^2.3.6
riverpod_annotation: ^2.1.1
dev_dependencies:
...
riverpod_generator: ^2.2.3
build_runner: ^2.4.4
사용방법
사용 방법은 간단하다. 다음과 같이 일반 함수 위에 @riverpod
을 붙여주고 인자에 생성 될(아직 생성 안됨) 예정인 Ref를 인자로 넣어주면 되는데 인자로 넣어줄 Ref의 명은 함수명의 첫 글자 대문자 + Ref
으로 결정된다. 이러한 함수를 생성해준 후 code generation을 해주면 된다.
@riverpod
String gState(GStateRef ref) {
return "Hello Code Generation";
}
그러면 아래와 같이 code generation 된 파일에 provider가 생성되며 해당하는 타입은 우리가 제공한 함수에 따라서 결정된다.
@ProviderFor(gState)
final gStateProvider = AutoDisposeProvider<String>.internal(
gState,
name: r'gStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$gStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef GStateRef = AutoDisposeProviderRef<String>;
With Go Router
part 'router.g.dart';
@riverpod
GoRouter goRouter(GoRouterRef ref) {
return GoRouter(initialLocation: '/',
routes: [
ShellRoute(
// Scaffold 없으면 에러 발생
builder: (context, state, child) => Scaffold(body: child,),
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeView(),
routes: [
GoRoute(
path: 'detail',
builder: (context, state) => const HomeDetailView(),
),
],
),
],
),
]);
}
Provider 사용방법
방법 1. update 사용
ref.read([specificProvider].notifier).update((state) => state + 1);
방법 2. state 값 직접 가져와서 변경
ref.read([specificProvider].notifier).state
= ref.read([specificProvider].notifier).state - 1;
.listen
Listen Provider는 선언시에 뭔가를 해주는게 아니라 값을 받을 때 .listen을 붙이는데 내가 이해한 바로는 프로바이더가 어떠한 동작이 있을 때 다음에 나올 값을 next에 넣어주고 현재의 값을 previous에 넣어주는 형식임.
final listenProvider = StateProvider<int>((ref) => 0,); // 선언
// previous는 현재값, next는 다음에 오는 값임
ref.listen<int>(listenProvider, (previous, next) {
if(previous != next) {
controller.animateTo(next,);
}
});
전체 예제 코드
class _ListProviederScreenState extends ConsumerState<ListenProviederScreen>
with TickerProviderStateMixin {
late final TabController controller;
@override
void initState() {
super.initState();
controller = TabController(
length: 10, vsync: this,
initialIndex: ref.read(listenProvider));
}
@override
Widget build(BuildContext context) {
ref.listen<int>(listenProvider, (previous, next) {
if(previous != next) {
controller.animateTo(next,);
}
});
return DefaultLayout(
title: 'Listen Provieder Screen',
body: TabBarView(
physics: NeverScrollableScrollPhysics(), // scroll로 이동이 안되게 설정
controller: controller,
children: List.generate(
10,
(index) => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(index.toString(),textAlign: TextAlign.center,),
ElevatedButton(onPressed: (){
ref.read(listenProvider.notifier).update((state) => state == 10 ? 10 : state + 1);
}, child: Text('Next'),),
ElevatedButton(onPressed: (){
ref.read(listenProvider.notifier).update((state) => state == 0 ? 0 : state - 1);
}, child: Text('Previous'),)
],
),
),
),
);
}
}
.select
final selectProvider = StateNotifierProvider<SelectNotifier, ShoppingItemModel>(
(ref) => SelectNotifier(),
);
class SelectNotifier extends StateNotifier<ShoppingItemModel> {
SelectNotifier() : super(
ShoppingItemModel(name: '김치', quantity: 3, hasBought: false, isSpicy: true)
);
toggleHasBought() {
state = state.copyWith(hasBought: !state.hasBought);
}
toggleIsSpicy() {
state = state.copyWith(isSpicy: !state.isSpicy);
}
}
provider 뒤에 .select를 붙여서 사용, 특정한 메서드의 값만 관측하고 싶을 때 사용하는 옵션.
// watch에 활용
final state = ref.watch(selectProvider.select((value) => value.isSpicy));
// listen과 함께 활용
ref.listen(selectProvider.select((value) => value.hasBought), (previous, next) {
print("previoud : $previous, next : $next");
});
Provider 안에 Provider
Provider 안에서 Provider를 호출해서 사용할 수도 있음.
stf widget > consumer widget
State 앞에 Consumer 붙이면 됨.
class Test extends ConsumerStatefulWidget { // 여기 변경
const Test({Key? key}) : super(key: key);
@override
//여기 변경
ConsumerState<Test> createState() => _TestState();
}
class _TestState extends ConsumerState<Test> { // 여기 변경
@override
Widget build(BuildContext context) {
return Container();
}
}
Consumer Widget
Consumer Widget은 기본적으로 builder이다. 이지만 builder의 callback 함수에 context외에 ref
와 child
를 추가적으로 받는다는 점이 다르다.
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
builder: (context) => Consumer(
builder: (context, _consumerRef, child) {
return TransactionDetailView(
stream: _consumerRef.watch(transactionRepositoryProvider.notifier).getTransactionByPeriod(widget.date, widget.date),
title: "",
);
}
),
);
위와 같은 방식으로 사용할 수 있다. ref가 없는 상위 위젯에서 하위 위젯에 ref를 주입시켜야 할 때 사용할 수 있지만 그다지 권고되는 방법은 아니다. (코드 재사용성 측면에서 ref를 사용하는 위젯을 하나의 ConsumerState나 ConsumerWidget으로 만들어서 사용하는 것이 좋다.)
.notifier의 의미
값을 참조할 때 provider 뒤에 .notifier를 붙이면 해당 class가 그대로 옴, 이를 통해서 해당 class 내부에 선언 된 함수에 바로 접근이 가능하게 됨.
Reference
riverpod sample code (opens in a new tab)
riverpod 공식문서 (opens in a new tab)
pvscode riverpod snippet (opens in a new tab)