티스토리 뷰
Provider의 확장판. flutter_hooks와 결합하여 사용이 가능하고 dart, flutter 둘 다에서 사용이 가능함.
어디서든 변경을 감지할 수 있는 상태 관리 객체
사용 하는 이유
- 어디서든 상태 값에 접근 가능
- 다른 상태값(Provider)과 결합하여 사용이 용이함
- 상태 변화에 영향을 받는 부분에 대해서만 부분 렌더링이 가능.
- 로깅 등 다른 feature와 결합하여 사용 가능.
// weatherProvider가 city를 참조함.
final cityProvider = Provider((ref) => 'London');
final weatherProvider = FutureProvider((ref) async {
final city = ref.watch(cityProvider);
return fetchWeather(city: city);
});
// 이런식으로 repository를 만들어서 사용하기에 유용
final repositoryProvider = Provider((ref) => UserRepositoty());
final userProvider = FutureProvider((ref){
final repository = ref.watch(repositoryProvider);
return repository.getUser();
});
read를 넘겨주는 방식.
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider).state;
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
사용하기 전 추가해야할 패키지
riverpod => dart(flutter 사용 안 함)
flutter_riverpod => flutter 앱
flutter_hooks => flutter + flutter_hooks
hooks_riverpod
riverpod 구성요소
- Provider 정의하는 부분
final stateName =
ChangeNotifierProvider<DataType>((ref) => DataType());
- 상태 관리 방법을 정의하는 ChangeNotifier 부분
class Person {
final name;
final socialNumber;
Person({this.name, this.socialNumber});
}
class PersonProvider with ChangeNotifier {
final _personList = <Person>[];
final _dropDownList = [];
//bool type의 Fetch를 이용하여 데이터를 가져오는 중인지 체크한다.
bool _isFetch = false;
get isFetching => _isFetch;
get getData => _personList;
fetchData() async {
if (_isFetch) return;
_isFetch = true;
_personList.clear();
try {
//Realtime Database에 있는 person Key-Value형식의 데이터를 가지고 옴.
DataSnapshot dataSnapshot =
await FirebaseDatabase.instance.reference().child("person").once();
//Iterable에서 Map으로 변경한다.
Map<String, dynamic> personMap =
Map<String, dynamic>.from(dataSnapshot.value);
//Person socialNumber(key)와 name(value)를 저장한다.
personMap.forEach((keySocialNumber, valueName) {
_personList.add(
new Person(socialNumber: keySocialNumber, name: valueName));
});
//모든것이 완료되면 notifyListeners를 이용하여 연결되어있는 위젯을 업데이트 시켜준다.
//setState와 비슷하다고 생각하면 된다.
//만약 상태는 변경되었지만 notifyListeneres를 호출하지 않으면 위젯은 업데이트되지않는다.
notifyListeners();
} catch (error) {
//에러가 발생하는 경우.
print("ERROR fetchData in PersonProvider.dart : $error");
} finally {
//모든 작업이 끝나면 fetch가 false로 반환이 되게 한다.
_isFetch = false;
}
}
}
- 상태 관리와 Widget을 이어주는 ConsumerWidget 부분
class PersonWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
//watch는 Provider를 관찰하는 역할
//watch된 Provider에서 notifyListener()가 작동하는지 확인한다.
final watchProvider = watch(riverpodProvider);
if (watchProvider.isFetching) {
//Provider에서 isFetch가 true인 경우 데이터를 가져오는 중
return Center(
child: CupertinoActivityIndicator(),
);
} else {
//Provider에서 isFetch가 false인 경우 widget을 띄운다.
return Container();
}
}
}
Provider 종류
Provider
riverpod의 가장 중요한 파트 프로바이더는 하나의 상태 조각의 압축된 객체이자 상태변화를 감시하는 역할을 함.
가장 간단한 기본 형태의 Provider 읽기만 가능하며 값을 변경할 수 없다.
final valueProcider = Provider<int>((ref) {
return 0;
});
Provider를 사용하려면 먼저 전체 앱을 ProviderScope로 감싸줘야 한다.
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
StateProvider
Provider + state 프로퍼티에 직접 값 변경 => 상태 값 변경 로직이 단순한 경우 사용.
매우 간단한 use-cases에서 StateNotifier를 만들지 않아도 되도록 해줌.
StateProvider에서 주로 사용되는 state 들
- enum타입(filter type 같은)
- String(일반적으로 text field의 raw 한 데이터)
- boolean(checkbox에 사용되는 거)
- number(pagination이나 나이 입력 폼)
StateProvider를 사용하면 안 되는 경우
- 검증 로직
- 복잡한 구조의 state 객체(eg. 커스텀 클래스, List, Map,...)
- 간단한 count++ 같은 거보다 더 복잡한 state를 수정하는 로직들은 다 안된다고 생각하면 됨.
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}
class Product {
Product({required this.name, required this.price});
final String name;
final double price;
}
final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];
enum ProductSortType {
name,
price,
}
final productSortTypeProvider = StateProvider<ProductSortType>( // sortType의 name(string)만 변경
(ref) => ProductSortType.name,
);
final productsProvider = Provider<List<Product>>((ref) { // sortType이 변경되면 리스트 갱신하는 Provider
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider); // product provider watch
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ref.watch(productSortTypeProvider),
// productSortTypeProvider에서 선택된 값을 리턴하면 드롭다운 위젯의 value로 넣어줌.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
// 드롭다운 위젯에서 변경(선택)된 값을 주면 productSortTypeProvider로 state 변경.
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}
}
StateNotifierProvider with StateNotifier
StateNotifier를 리턴하는 provider => 더 복잡한 상태 값 변경 로직을 작성하고 싶은 경우
상태뿐만 아니라 일부 로직을 저장할 때 사용됨.
일반적인 사용법
- 커스텀 이벤트에 반응해서 시간이 지남에 따라 변경될 수 있는 변경 불가능한 상태를 반환함.
- 한 장소에서 몇 가지 state변경 로직을 중앙집중화할 때. (시간 지남에 따라 유지보수성이 좋아짐.)
//immutable한 상태(State)
@immutable
class Todo {
const Todo({required this.id, required this.description, required this.completed});
// 모든 속성은 final
final String id;
final String description;
final bool completed;
// immutable 한 Todo객체 이므로 clone method 만들어주기
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
// StateNotifierProvider클래스에 StateNotifier가 던져짐..
// state속성에 외부의 상태를 밖으로 내보내면 안됨.(public한 getter나 속성 없음!)
// 이 클래스에 public 메소드는 UI가 상태를 수정하게하는거 뿐임.
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier(): super([]); // empty list로 초기화
void addTodo(Todo todo) { // immutable 해서 .add()가 안됨 대신 이전 값을 포함한 새로운 리스트를 만듬.
state = [...state, todo];
// "state = ~~" 이렇게 하면 state가 변경돼서 notifyListeners가 필요 없음.
}
void removeTodo(String todoId) { // todo 리스트를 지우는 메소드
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
void toggle(String todoId) { // completed 마킹하는 메소드
state = [
for (final todo in state)
if (todo.id == todoId) // completed에 변화가 있을 때
todo.copyWith(completed: !todo.completed)
else // 없을 때
todo,
];
}
}
// UI 와 TodosNotifier클래스가 상호작용 하게 하는 StateNotifierProvider.
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
Provider 읽기
WidgetRef(ref)?
Widget과 Provider 사이에 상호 작용을 도와주는 역할을 함.(특정 Widget에서 특정 Provider에 자유롭게 접근 가능.)
모든 Provider는 ref 객체를 파라미터로 받게 됨.
// 사용할 Provider
final valueProvider = Provider<int>((ref) {
return 0;
});
// Stateless --> ComsumerWidget
class MyHomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(valueProvider);
return Scaffold(
body: Center(
child: Text(
'Value: $value',
),
),
);
}
}
WidgetRef를 얻기 위해 Consumer, ConsumerWidget, ComsumerStatefulWidget을 제공함.
riverpod에서 정의한 위젯들로 WidgetRef를 포함하고 있음. StatelessWidget, StatefulWidget을 대신하여 사용함.
- StatelessWidget => ConsumerWidget
위젯 생성시, stateless 대신 consumerWidget을 사용함.(extends 함)
build메서드에서 Widget을 리턴하는 것은 똑같으나 ref(WidgetRef)를 파라미터로 받음.
class HomeView extends ConsumerWidget {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref를 사용해 프로바이더 구독(listen)하기
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
- StatefulWidget+state => ConsumerStatefulWidget+ConsumerState
ConsumerWidget과 마찬가지로 위젯 생성 시 stateful대신 사용함.
그런데 build 메소드에서 ConsumerState 객체 속성의 파라미터를 받는다는 것이 다름.
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}) : super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> { //State 대신 ConsumerState<T>
@override
void initState() {
super.initState();
// "ref"는 StatefulWidget의 모든 생명주기 상에서 사용
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
// "ref"는 build 메소드 안에서 프로바이더를 변화 감지(watch)
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
- Consumer와 HookConsumer 위젯
위젯 안에서 ref 객체를 얻기 위한 마지막 방안. ConsumerWidget/HookConsumerWidget과 동일한 속성을 가짐
extends하지 않고 class를 별도로 정의 할 필요가 없음.
Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// HookConsumerWidget과 같이, builder안에서 hooks을 사용할 수 있습니다.
final state = useState(0);
// 프로바이더를 사용/구독(listen)하기 위해서 ref 매개변수도 사용할 수 있습니다.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);
나는 사용 안할거지만
HookWidget대신 HookConsumerWidget 사용하면 됨. 참고 링크
WidgetRef를 이용해 읽기
기능 구현 시 가급적으로 ref.watch 사용을 권장함.(reactive와 선언형에 가까워지고 애플리케이션 유지보수 용이해짐)
- ref.watch
- 반응형으로 Provider의 값이 변경되면 자체적으로 다시 build 됨.
- 비동기적으로 호출하거나, onTab, initState 등의 생명주기에서는 사용을 하면 안 됨. => 여기에서는 read 사용 권장.
- 다른 Provider와 결합할 때 아주 유용하게 쓰임.
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider =
StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// 할일(todos)목록과 필터(filter)상태 값을 취득합니다.
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// 완료된(completed) 할일 목록을 반환합니다.
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// 필터링되지 않은 목록을 반환합니다.
return todos;
}
});
- ref.listen
- Provider의 값이 변경되면 값을 구독하거나 상태 값 변화에 대응하는 함수를 호출함.
- watch와 마찬가지로 build 안이나 Provider안에서 사용되어야 함.
- 두 개의 위치 인자(positional arguments)가 필요. 프로바이더와 콜백 함수(상태변화에 대응).
- 콜백 함수에는 이전 값과 현재 갱신된 상태 값 두 가지를 넘겨줌.
- SnackBar나 Dialog를 처리하는데 유용함.
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
- ref.read
- Provider의 값을 읽어오기만 한다. 값이 변경되어도 별다른 동작을 하지 않음.(변경 감지가 아니라 그냥 읽어옴.)
- 공식문서에 따르면 특별한 경우가 아니면 사용을 하지 않는 것 같다.(영원히 값이 변경되지 않는다는 보장이 있나?라고 물어봄)
- 공식문서에서는 watch 사용을 권장하고 read를 build 내에서 사용하지 말라고 권장되어있다.
- 리빌딩되는 횟수를 줄이기 위해 read를 사용하는 경우도 watch도 똑같은 효과를 얻을 수 있기 때문.
(똑같은 효과를 얻을 수 있도록 Provider가 다양한 방식을 제공함)
final counterProvider = StateProvider((ref) => 0);
// 얘보다는
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
// 이렇게 사용하기~, 이렇게 사용하는게 .refresh를 호출해 카운터 값이 초기화 되어도 대응이 가능함.
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
- provider.select 관찰하고 싶은 상태 값을 선택하여 선택한 속성의 값이 변화했을 때만 사용할 수 있음.
- ref.refresh 상태 초기화
StateNotifierProvider 사용(두 가지)
final provider = StateNotifierProvider<MyStateNotifier, MyModel>((ref) {
return MyStateNotifier();
});
Widget build(BuildContext context, ScopedReader watch) {
// 방식1 Provider 객체에 접근
MyStateNotifier notifier = watch(provider.notifier);
// 방식2 Provider의 상태(값) 반환
MyModel state = watch(provider);
}
.family
Provider를 생성할 때, 외부 값을 참조하기 위한 용도
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
.autoDispose
자동으로 해당 Provider를 삭제. FutureProvider, StreamProvider와 함께 사용됨.
final userProvider = StreamProvider.autoDispose<User>((ref) {
// 로직 로직
});
final userFutureFamilyProvider =
FutureProvider.family.autoDispose<UserState, User>(
(ref, user) {
return UserState(user);
},
);
// family와 함께 사용
final shopDetailProvider =
FutureProvider.family.autoDispose<ShopDetailData, int>(
(ref, shopId) {
return ref.watch(shopRepositroyProvider).getShopDetailData(shopId);
},
);
final detailData = ref.watch(shopDetailProvider(widget.shopId));
return SafeArea(
child: detailData.when( // when을 사용해 데이터 상태 별 화면을 처리
// 로딩 화면
loading: (pre) => const Scaffold(
body: Center(
child: CircularProgressIndicator())),
// 에러 화면
error: (error, stack, pre) => Scaffold(
body: Center(
child: Text(
error.toString(),
style: const TextStyle(fontSize: 32)))),
data: (shopData) { // 데이터 상태별 화면 구현
// 결과 화면 구현
}
ProviderObserver
보통 Logger를 만들 때 많이 사용함. ProviderContainer의 변화를 관찰한다.
제공하는 메서드
- didAddProvider: 프로바이더 초기화 시마다 호출됨.
- didDisposeProvider: 프로바이더가 disposed 될 때마다 호출됨
- dodUpdateProvider: 프로바이더 값이 변경될 때마다 호출됨.
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('''
{
"provider": "${provider.name ?? provider.runtimeType}",
"newValue": "$newValue"
}''');
}
}
void main() {
runApp(
ProviderScope(observers: [Logger()], child: const MyApp()),
);
}
추가
Future & Stream Provider
Future Provider
FutureBuilder+Provider라고 생각하면 됨.
null 값이 넘어오지 않도록 값이 준비되지 않았을 때 그 준비기간 동안의 값을 제공함. => initial Value가 있음.
- 비동기 작업을 캐싱하고 수행함.
- 비동기 작업의 에러/로딩 상태를 관리하기 좋다.
- 다른 값에다가 다양한 비동기 값들을 합칠 수 있다.
simple use-case에 맞게 설계된 거라 유저 상호작용이 있는 복잡한 수정은 StateNotifierProvider를 사용하기를 권장함.
사용 예: configureation파일 읽기
final configProvider = FutureProvider<Configuration>((ref) async {
final content = json.decode(
await rootBundle.loadString('assets/configurations.json'),
) as Map<String, Object?>;
return Configuration.fromJson(content);
});
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<Configuration> config = ref.watch(configProvider);
return config.when(
loading: () => const CircularProgressIndicator(), // unCompleted 상태
error: (err, stack) => Text('Error: $err'), // 실패
data: (config) { // 성공
return Text(config.host);
},
);
}
Future complete가 되면 UI를 자동으로 리빌드함. 동시에 다양한 위젯들이 속성 파일을 원할 때 한 번만 읽으면 됨.
Stream Provider
StreamBuilder+Provider라고 생각하면 됨.
Stream 타입 값을 제공함. FutureProvider처럼 다른 값이 들어오면 자동으로 값을 리턴한다.
FutureProvider와 다른 점은 값이 필요한 만큼 리빌드 한다.
- Firebase나 web-sockets의 콜백을 등록
- 매 몇 초마다 다른 provider가 다시 생성됨.
예시
final futureProvider = FutureProvider<int>((ref) {
return Future.delayed(const Duration(seconds: 3), () => 5);
});
final streamProvider = StreamProvider<int>((ref) {
int count = 0;
return Stream.periodic(const Duration(seconds: 2), (_) => count++);
});
class HomePage extends ConsumerWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final streamAsyncValue = ref.watch(streamProvider);
final futureAsyncValue = ref.watch(futureProvider);
return Scaffold(
appBar: AppBar(
title: Text('HomePage'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: streamAsyncValue.when(
data: (data) => Text('Value: $data'),
loading: (_) => const CircularProgressIndicator(),
error: (e, st, _) => Text('Error: $e'),
),
),
Center(
child: futureAsyncValue.when(
data: (data) => Text('Value: $data'),
loading: (_) => const CircularProgressIndicator(),
error: (e, st, _) => Text('Error: $e'),
),
),
],
),
);
}
}