Provider
Provider
는 프로바이더 중에서 가장 기본적인 프로바이더이며, 값을 동기적으로 생성해 줍니다.
Provider
는 일반적으로 다음과 같은 용도로 사용됩니다.
- 계산 결과를 캐싱
- 다른 공급자(
Repository
또는HttpClient
)에 값 노출 - 테스트 또는 위젯이 값을 재정의할 수 있는 방법을 제공
select
를 사용하지 않고도 프로바이더 및 위젯 리빌드를 줄 일 수 있습니다.
Provider
를 사용하여 계산 결과를 캐시하기
Provider
는 ref.watch와 함께 사용하면 동기식 작업을 캐싱할 수 있습니다.
예를 들면 Todo 목록에 필터를 적용하는 경우입니다. 목록을 필터링하는 데에는 약간의 비용이 발생 할 수 있습니다. 그렇기 때문에 화면을 다시 렌더링될 할마다 Todo 목록은 다시 필터링하지 않는 것이 이상적입니다. 이 상황에서 Provider
를 사용하여 필터링 결과값을 캐싱할 수 있습니다.
이를 위해 Todo 목록을 관리하는 다음과 같은 NotifierProvider가 있다고 가정합니다.
class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}
class TodosNotifier extends Notifier<List<Todo>> {
@override
List<Todo> build() {
return [];
}
void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO: "removeTodo"와 같은 다른 메서드 추가하기, ...
}
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(() {
return TodosNotifier();
});
여기서 Provider
를 사용하여 Todo 목록을 필터링하고 완료된 할일만 표시할 수 있습니다.
final completedTodosProvider = Provider<List<Todo>>((ref) {
// todosProvider로부터 모든 todo 목록을 가져옵니다.
final todos = ref.watch(todosProvider);
// 완료된 할일 목록만 반환합니다.
return todos.where((todo) => todo.isCompleted).toList();
});
이 코드를 사용하면 이제 completedTodosProvider
를 수신하여 완료된 할일 목록만 UI에 표시할 수 있습니다:
Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO show the todos using a ListView/GridView/...
// TODO: ListView/GridView/...를 사용하여 할일 목록을 표시합니다.
});
여기서 흥미로운 부분은 할일 목록 필터링이 캐시된다는 것입니다.
즉, 완료된 할일 목록을 여러 번 읽더라도 할 일이 추가/제거/업데이트될 때까지는 완료된 할일 목록이 다시 계산되지 않습니다.
할 일 목록이 변경될 때 캐시를 수동으로 무효화할 필요가 없다는 점에 유의하십시오. Provider
는 ref.watch 덕분에 결과를 다시 계산해야 하는 시기를 자동으로 알 수 있습니다 .
Provider
를 사용하여 프로바이더/위젯 리빌드 줄이기
Provider
가 다른 프로바이더와 다른 점은 프로바이더가 다시 계산되더라도(일반적으로 ref.watch를 사용할때) 값이 변경되지 않는 경우, 이 프로바이더의 값을 감시하는 위젯/프로바이더는 업데이트하지 않는 점입니다.
이 특성을 활용할 수 있는 예시는, 페이지네이션의 이전/다음 버튼을 활성화/비활성화하는 경우를 들 수 있습니다.
여기서는 “Previous” 버튼에 집중하겠습니다.
이러한 버튼을 순수하게 구현하면 현재 페이지 인덱스를 가져오는 위젯이 될 것이고, 해당 인덱스가 0이면 버튼을 비활성화할 것입니다.
이 코드는 다음과 같습니다.
final pageIndexProvider = StateProvider<int>((ref) => 0);
class PreviousButton extends ConsumerWidget {
const PreviousButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 현재 페이지가 첫 페이지(0)가 아닌 경우, previous 버튼 활성화
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;
void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}
return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}
이 코드의 문제점은 현재 페이지를 변경할 때마다 “Previous” 버튼이 다시 빌드된다는 것입니다.
활성화와 비활성화 사이를 전환할 때만 버튼이 다시 빌드되는 것이 가장 이상적입니다.
이 문제의 원인은 이전 페이지로 이동할 수 있는지 여부를 ‘Previous’ 버튼 내부에서 계산하고 있기 때문입니다.
따라서 이 문제를 해결하는 방법은 이 로직을 위젯 외부로 추철하여 Provider
에 맡기는 것입니다.
final pageIndexProvider = StateProvider<int>((ref) => 0);
// 사용자가 이전 페이지로 이동할 수 있는지 여부를 계산하는 Provider
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) != 0;
});
class PreviousButton extends ConsumerWidget {
const PreviousButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 이제 새로운 Provider를 감시합니다.
// 위젯 내부에서는 더 이상 이전 페이지로 이동할 수 있는지 여부를 계산하지 않습니다.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);
void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}
return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}
이렇게 하면 페이지 인덱스가 변경될 때마다 PreviousButton
위젯은 더 이상 리빌드되지 않습니다 .
지금부터 페이지 인덱스가 변경되면 canGoToPreviousPageProvider
는 다시 계산됩니다. 그러나 canGoToPreviousPageProvider
가 노출한 값이 변경되지 않는다면 PreviousButton
위젯은 다시 빌드되지 않습니다.
이 작은 리팩토링 덕분에 버튼의 성능을 개선하고 위젯과 로직을 분리하여 관리할 수 있습니다.
(Async)NotifierProvider
NotifierProvider는 Notifier를 수신하고 노출하는 데 사용되는 프로바이더입니다 .
AsyncNotifierProvider는 AsyncNotifier를 수신하고 노출하는 데 사용되는 프로바이더입니다 . AsyncNotifier는 비동기적으로 초기화할 수 있는 Notifier 입니다 .
(Async)NotifierProvider
는 (Async)Notifier
와 함께 사용자 상호 작용에 반응하여 변경될 수 있는 상태를 관리하기 위한 Riverpod의 권장 솔루션입니다.
일반적으로 다음 용도로 사용됩니다.
- 사용자 정의 이벤트에 반응한 후 시간이 지남에 따라 변경될 수 있는 상태를 노출합니다.
- 일부 상태를 수정하기 위한 로직(“비즈니스 로직”)을 한 곳에서 중앙 집중화하여 시간이 지남에 따라 유지 관리성을 향상시킵니다.
사용 예로, NotifierProvider를 사용하여 할 일 목록을 구현할 수 있습니다. 이렇게 하면 UI가 사용자 상호 작용에서 할 일 목록을 수정하도록 하는 addTodo
것과 같은 메서드를 노출할 수 있습니다 .
// 불변 상태를 선언합니다.
// 구현을 돕기 위해 Freezed와 같은 패키지를 사용할 수도 있습니다.
@immutable
class Todo {
const Todo({
required this.id,
required this.description,
required this.completed,
});
// 모든 프로퍼티는 클래스에서 'final'이어야 합니다.
final String id;
final String description;
final bool completed;
// Todo는 불변 객체라서 변경할 수 없으므로
// 약간 다른 내용으로 할일을 복제할 수 있는 메서드를 구현했습니다.
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
// NotifierProvider에 전달할 Notifier 클래스입니다.
// 이 클래스는 "state" 프로퍼티 외부에 상태를 노출해서는 안되며, 이는 퍼블릭 게터/프로퍼티가 없음을 의미합니다!
// 이 클래스의 퍼블릭 메서드는 UI가 상태를 수정할 수 있게 해줍니다.
class TodosNotifier extends Notifier<List<Todo>> {
// We initialize the list of todos to an empty list
@override
List<Todo> build() {
return [];
}
// todo를 추가할 수 있습니다.
void addTodo(Todo todo) {
// 상태는 불변이므로 `state.add(todo)`를 수행할 수 없습니다.
// 대신 이전 항목과 새 항목이 포함된 새로운 목록을 만들어야 합니다.
// 스프레드 연산자를 사용하면 도움이 됩니다!
state = [...state, todo];
// 여기서는 "notifyListeners" 또는 이와 유사한 것을 호출할 필요가 없습니다.
// "state ="를 호출하면 필요할 때 자동으로 UI를 다시 빌드합니다.
}
// todo를 제거할 수 있습니다.
void removeTodo(String todoId) {
// 다시 말하지만, 현재 프로바이더의 상태는 불변 객체입니다.
// 따라서 기존 목록을 변경하는 대신 새로운 목록을 만들어야 합니다.
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
// todo를 완료로 표시할 수 있습니다.
void toggle(String todoId) {
state = [
for (final todo in state)
// 일치하는 todo만 완료로 표시합니다.
if (todo.id == todoId)
// 다시 한 번 말하지만,
// 상태는 불변 객체이므로 복사본을 만들어야 합니다.
todo.copyWith(completed: !todo.completed)
else
// 일치하지 않는 다른 todo는 수정하지 않습니다.
todo,
];
}
}
// 마지막으로 NotifierProvider를 사용하여 UI가 TodosNotifier 클래스와 상호 작용할 수 있도록 합니다.
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(() {
return TodosNotifier();
});
이제 NotifierProvider를 정의했으므로, 이를 사용하여 UI의 할일 목록과 상호작용할 수 있습니다.
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// todo 목록이 변경되면 위젯을 다시 빌드합니다.
List<Todo> todos = ref.watch(todosProvider);
// todo을 스크롤 가능한 ListView로 렌더링합니다.
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// todo를 탭하면 완료 상태를 변경합니다.
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
사용 예로 AsyncNotifierProvider를 사용하여 원격 할 일 목록을 구현할 수 있습니다. 이렇게 하면 addTodo
와 같은 메서드를 노출하여 UI가 사용자 상호작용에 따라 할 일 목록을 수정할 수 있습니다.
@immutable
class Todo {
const Todo({
required this.id,
required this.description,
required this.completed,
});
factory Todo.fromJson(Map<String, dynamic> map) {
return Todo(
id: map['id'] as String,
description: map['description'] as String,
completed: map['completed'] as bool,
);
}
final String id;
final String description;
final bool completed;
Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'description': description,
'completed': completed,
};
}
// AsyncNotifierProvider에 전달할 AsyncNotifier 클래스입니다.
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
Future<List<Todo>> _fetchTodo() async {
final json = await http.get('api/todos');
final todos = jsonDecode(json) as List<Map<String, dynamic>>;
return todos.map(Todo.fromJson).toList();
}
@override
Future<List<Todo>> build() async {
// 원격 리포지토리에서 초기 todo 목록 불러오기
return _fetchTodo();
}
Future<void> addTodo(Todo todo) async {
// 상태를 로딩 중으로 설정
state = const AsyncValue.loading();
// 새로운 todo를 추가하고 원격 저장소에서 todo 목록을 다시 로드합니다.
state = await AsyncValue.guard(() async {
await http.post('api/todos', todo.toJson());
return _fetchTodo();
});
}
Future<void> removeTodo(String todoId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.delete('api/todos/$todoId');
return _fetchTodo();
});
}
Future<void> toggle(String todoId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.patch(
'api/todos/$todoId',
<String, dynamic>{'completed': true},
);
return _fetchTodo();
});
}
}
// 마지막으로 AsyncNotifierProvider를 사용하여 UI가 AsyncTodosNotifier 클래스와 상호 작용할 수 있도록 합니다.
final asyncTodosProvider =
AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(() {
return AsyncTodosNotifier();
});
이제 AsyncNotifierProvider를 정의했으므로 이를 사용하여 UI의 할 일 목록과 상호 작용할 수 있습니다.
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// todo 목록이 변경되면 위젯을 다시 빌드합니다.
final asyncTodos = ref.watch(asyncTodosProvider);
return asyncTodos.when(
data: (todos) => ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
onChanged: (value) => ref.read(asyncTodosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (err, stack) => Text('Error: $err'),
);
}
}
StateNotifierProvider
StateNotifierProvider
는 StateNotifier를 수신하고 노출하는 데 사용되는 프로바이더입니다 .
일반적으로 다음 용도로 사용됩니다.
- 사용자 정의 이벤트에 반응한 후 시간이 지남에 따라 변경될 수 있는 불변 상태를 노출합니다 .
- 일부 상태를 수정하기 위한 로직( “비즈니스 로직”)을 한 곳에서 중앙 집중화하여 시간이 지남에 따라 유지 관리성을 향상시킵니다.
[!info] 정보
StateNotifierProvider
대신 NotifierProvider를 사용하는 것이 좋습니다 .
사용 예로 StateNotifierProvider
는 todo-list를 구현하는 데 사용할 수 있습니다.
@immutable
class Todo {
const Todo({required this.id, required this.description, required this.completed});
final String id;
final String description;
final bool completed;
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier(): super([]);
void addTodo(Todo todo) {
state = [...state, todo];
}
void removeTodo(String todoId) {
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
void toggle(String todoId) {
state = [
for (final todo in state)
if (todo.id == todoId)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
}
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
이제 StateNotifierProvider
를 정의했으므로, 이를 사용하여 UI에서 todo 목록과 상호작용할 수 있습니다.
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
List<Todo> todos = ref.watch(todosProvider);
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
FutureProvider
FutureProvider
는 비동기 Provider라고 말할 수 있습니다.
FutureProvider
는일반적으로 다음 용도로 사용됩니다.
- 비동기 작업(예: 네트워크 요청) 수행 및 캐싱
- 비동기 작업의 오류/로드 상태 처리
- 여러 비동기 값을 하나의 값을 조합하기
FutureProvider
는ref.watch와 결합하여 그 효과를 발휘합니다. 예를 들어, 일부 값이 변경될 때 일부 데이터를 자동으로 다시 가져올 수 있으므로 프로바이더는 항상 최신 값을 유지할 수 있습니다.
[!info] 정보
FutureProvider
는 사용자 상호 작용 후 계산을 직접 변경할 수 있는 방법이 없습니다.FutureProvider
는 간단한 사용 사례를 해결하기 위해 설계되었습니다.
보다 고급 시나리오의 경우에는 AsyncNotifierProvider 사용하는 것이 좋습니다.
사용 예: 구성 파일 읽기
FutureProvider
는 JSON 파일을 읽어 생성된 Configuration
객체를 노출하는 방법으로 사용할 수 있습니다.
config 생성은 async/await 구문을 사용하여 프로바이더 내부에서 수행하면 됩니다. Flutter의 에셋(asset) 시스템을 사용하는 예제는 다음과 같습니다.
final configProvider = FutureProvider<Configuration>((ref) async {
final content = json.decode(
await rootBundle.loadString('assets/configurations.json'),
) as Map<String, Object?>;
return Configuration.fromJson(content);
});
그런 다음 UI에서는 다음과 같이 configurations을 감시(watch)합니다.
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<Configuration> config = ref.watch(configProvider);
return config.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (config) {
return Text(config.host);
},
);
}
이렇게 하면 Future가 완료되면 UI가 자동으로 리빌드됩니다 . 게다가 캐시 기능이 동작하기 때문에 여러 위젯이 동시에 config를 원하는 경우에도 에셋은 한 번만 로드됩니다.
보시다시피 위젯 내부의 FutureProvider
를 감시하면 오류/로딩 상태를 처리할 수 있는 AsyncValue 가 반환됩니다. AsyncValue 덕분에 error / loading과 같은 상태를 적절하게 위젯으로 변환 할 수 있습니다.
StreamProvider
StreamProvider
는 FutureProvider 의 Stream 버전입니다.
일반적으로는 다음과 같은 용도로 사용됩니다.
- Firebase 및 WebSocket을 모니터링.
- 일정 시간마다 다른 프로바이더를 업데이트.
Stream 자체가 값의 갱신을 감시하는 기능을 가지고 있므로, StreamProvider
사용하는 것이 별로라고 생각할 수도 있습니다. 특히 스트림을 수신하는데 Flutter의 StreamBuilder 로 충분하다고 생각할 수 있지만, 이는 잘못된 생각입니다.
그러나 StreamBuilder 대신 StreamProvider
를사용하면 다음과 같은 많은 이점이 있습니다.
- 다른 프로바이더가 ref.watch 를 사용하여 스트림을 감시할 수 있습니다.
- AsyncValue를 사용하면 loading/error 상태를 적절하게 처리 할 수 있습니다.
- 일반 스트림과 브로드캐스트(broadcast) 스트림을 구별할 필요가 없습니다.
- 스트림으로부터 출력된 최신 값을 캐시합니다.(도중에 감시를 개시해도 최신의 값을 얻을 수 있습니다).
StreamProvider
을 오버라이드(override)하여 테스트에서 스트림을 간단하게 모킹할 수 있습니다.
사용 예: 소켓을 사용한 실시간 채팅
StreamProvider
는 비디오 스트리밍, 일기 예보 API 또는 라이브 채팅과 같은 비동기 데이터 스트림을 처리할 때 사용됩니다.
final chatProvider = StreamProvider<List<String>>((ref) async* {
// 소켓을 사용하여 API에 연결하고 출력을 디코딩합니다.
final socket = await Socket.connect('my-api', 4242);
ref.onDispose(socket.close);
var allMessages = const <String>[];
await for (final message in socket.map(utf8.decode)) {
// 새로운 메시지가 수신되면, 전체 메시지 목록에 추가합니다.
allMessages = [...allMessages, message];
yield allMessages;
}
});
그러면 UI에서 다음과 같이 라이브 스트리밍 채팅을 모니터링 할 수 있습니다.
Widget build(BuildContext context, WidgetRef ref) {
final liveChats = ref.watch(chatProvider);
// FutureProvider와 마찬가지로, loading/error 상태를 처리하기 위해 AsyncValue.when을 사용할 수 있습니다.
return liveChats.when(
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text(error.toString()),
data: (messages) {
return ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return Text(message);
},
);
},
);
}
StateProvider
StateProvider
는 외부에서 변경이 가능한 상태를 노출하는 프로바이더입니다. 매우 간단한 시나리오에서 StateNotifier 클래스를 작성할 필요가 없도록 설계된 StateNotifierProvider의 단순화 버전입니다.
따라서 StateProvider
는 UI에서 사용되는 간단한 상태를 관리하는 데 적합합니다. 간단한 상태는 다음과 같은 유형의 상태를 의미합니다.
- enum, 예를 들면 필터 타입
- String, 예를 들면 텍스트 필드의 입력 내용 등
- boolean, 예를 들면 체크 박스의 값 등
- number, 예를 들면 페이지 네이션의 페이지 번호 또는 입력폼 필드
반대로, 다음과 같은 경우에는 StateProvider
를사용해서는 안됩니다.
- 상태에 유효성 검증 로직 필요한 경우
- 상태 자체가 복잡한 객체인 경우(사용자 정의 클래스 및
List
/Map
등) - 상태를 변경하기 위한 로직이 단순하지 않은 경우
좀 더 고급 상태 관리가 필요한 경우에는 StateNotifier 클래스를 작성한 다음 StateNotifierProvider를 사용하는 것이 좋습니다. 이 경우 약간의 보일러 플레이트 코드 설정이 필요합니다. 하지만 비즈니스 로직을 한 곳에서 집중 관리 할 수 있기 때문에 프로젝트의 장기적인 유지 보수 관리에 최선이라고 할 수 있습니다.
사용 예시: 드롭다운을 사용하여 필터 타입 변경하기
StateProvider
의 대표적인 사용 사례는 드롭다운 메뉴, 텍스트 필드, 체크 박스 등 입력폼에 사용되는 컴퍼넌트의 상태를 관리하는 것입니다.
StateProvider
를 사용하여 product 목록의 정렬 방식을 변경할 수 있는 드롭다운 메뉴를 구현하는 방법을 살펴보겠습니다.
Product
의 정의와 product 목록의 내용은 다음과 같습니다.
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),
];
final productsProvider = Provider<List<Product>>((ref) {
return _products;
});
실제 앱에서는 FutureProvider를 사용하여 네트워크 요청을 통해 목록을 얻을 수 있습니다. 하지만 여기에서는 간략화를 위해 하드 코딩하고 있습니다.
그리고 UI에는 다음과 같이 상품 목록을 표시합니다.
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}
이제 가격이나 이름으로 제품을 필터링할 수 있는 드롭다운 메뉴를 추가합니다.
이를 위해 Flutter의 DropDownButton을 사용합니다.
// 필터의 종류를 나타내는 enum
enum ProductSortType {
name,
price,
}
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ...
),
);
}
드롭다운 메뉴가 완성되면 StateProvider
을 생성하고 프로바이더와 드롭다운 메뉴의 상태를 동기화합니다.
먼저 StateProvider
를 만들어 봅시다.
final productSortTypeProvider = StateProvider<ProductSortType>(
// 여기서는 기본 정렬 타입인 name을 반환합니다.
(ref) => ProductSortType.name,
);
그런 다음 다음과 같이 공급자와 드롭다운 메뉴를 연결합니다.
DropdownButton<ProductSortType>(
// 정렬 타입이 바뀌면 드롭다운 메뉴가 리빌드되어 아이콘이 업데이트됩니다.
value: ref.watch(productSortTypeProvider),
// 사용자가 드롭다운 메뉴를 조작하면 프로바이더 상태가 업데이트됩니다.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),
이렇게하면 드롭다운 메뉴에서 정렬 타입을 변경할 수 있습니다. 다만, 이것만으로는 상품 목록은 정렬되지 않습니다! 이제 마지막 단계입니다. 상품 목록을 정렬하도록 productsProvider
를 업데이트 합니다.
이 기능을 구현하는 핵심은 ref.watch입니다. ref.watch를 사용하여 productsProvider
가 현재 정렬 타입을 모니터링하고, 정렬 타입이 변경 될 때마다 상품 목록을 다시 계산하도록 하는 것입니다.
구체적인 구현은 다음과 같습니다.
final productsProvider = Provider<List<Product>>((ref) {
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));
}
});
이것뿐입니다! 이렇게 하면 UI는 정렬 타입이 변경되었음을 감지하고 제품 목록을 자동으로 업데이트합니다.
다음은 다트패드에서 전체 예제입니다.
프로바이더를 두 번 읽지 않고 이전 값을 기반으로 상태를 업데이트하는 방법
가끔은 이전 값을 기반으로 StateProvider
의 상태를 업데이트하고 싶을 때가 있습니다.
다음과 같이 작성할 수 있습니다.
final counterProvider = StateProvider<int>((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// 최근 상태에서 새로운 상태를 계산하려고 하면, 이렇게 공급자를 두 번 이용하게 되는 경우가 많다.
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}
이 코드에 특별히 잘못된 것은 없지만 구문이 약간 불편합니다.
코드를 조금 더 개선하기 위해 update
함수를 사용할 수 있습니다. 이 함수는 현재 상태를 수신하고 새로운 상태를 반환하는 콜백 함수를 건내줍니다. 이 콜백 함수는 최근 상태의 값을 파라미터로 사용할 수 있습니다.
update
메소드를 사용하여 코드를 다음과 같이 리팩토링할 수 있습니다.
final counterProvider = StateProvider<int>((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}
이 변경은 동일한 효과를 달성하는 동시에 간결하고 보기 쉬운 코드로 작성 할 수 있었습니다.
ChangeNotifierProvider
ChangeNotifierProvider
는 ChangeNotifier를 Flutter에서 사용하기 위한 프로바이더입니다.
Riverpod에서는 ChangeNotifierProvider
사용을 권장하지 않으며, 주로 다음과 같은 용도로 존재합니다.
package:provider
에서ChangeNotifierProvider
사용했을 때의 마이그레이션 작업을 용이하게하기 할 수 있습니다.- 불변(immutable) 상태가 선호되지만 변경 가능한(mutable) 상태를 지원합니다.
[!info] 정보
ChangeNotifierProvider
대신 StateNotifierProvider를 사용하는 것이 좋습니다. 변경 가능한(mutable) 상태를 원하는 것이 확실할 경우에만ChangeNotifierProvider
사용하는 것이 좋습니다.
때로는 불변 상태 대신 변경 가능한 상태를 사용하는 것이 더 효율적일 수 있습니다. 그러나 단점은 유지 관리가 어렵고 다양한 기능이 예상외의 동작을 할 우려가 있다는 것입니다.
예를 들어, 상태가 변경 가능한 상태인 경우 select
가 값이 변경되지 않는 것으로 간주하므로 provider.select
를 사용하여 위젯의 리빌드를 최적화하는 기능이 작동하지 않을 수 있습니다.
결과적으로 변경 불가능한 데이터 구조를 사용하는 것이 확실하고 효율이 더 좋습니다. 따라서 ChangeNotifierProvider
도입을 검토할 때에는, 사용 사례에 맞는 벤치마크를 실시해 실제로 전체적인 퍼포먼스에 어떻게 영향을 주는지를 신중하게 파악하는 것이 중요합니다.
다음 코드는 ChangeNotifierProvider
를 Todo 목록에서 사용하는 예시입니다. 먼저 Todo의 모델과 ChangeNotifier를 정의하고 ChangeNotifierProvider
생성합니다.
class Todo {
Todo({
required this.id,
required this.description,
required this.completed,
});
String id;
String description;
bool completed;
}
class TodosNotifier extends ChangeNotifier {
final todos = <Todo>[];
// Let's allow the UI to add todos.
void addTodo(Todo todo) {
todos.add(todo);
notifyListeners();
}
// Let's allow removing todos
void removeTodo(String todoId) {
todos.remove(todos.firstWhere((element) => element.id == todoId));
notifyListeners();
}
// Let's mark a todo as completed
void toggle(String todoId) {
for (final todo in todos) {
if (todo.id == todoId) {
todo.completed = !todo.completed;
notifyListeners();
}
}
}
}
// Finally, we are using ChangeNotifierProvider to allow the UI to interact with
// our TodosNotifier class.
final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
return TodosNotifier();
});
이제 ChangeNotifierProvider
를 정의했으므로, 이를 사용하여 UI의 할 일 목록과 상호 작용할 수 있습니다:
class TodoListView extends ConsumerWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// rebuild the widget when the todo list changes
List<Todo> todos = ref.watch(todosProvider).todos;
// Let's render the todos in a scrollable list view
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// When tapping on the todo, change its completed status
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
or
'개발 > 플러터(Flutter)' 카테고리의 다른 글
Riverpod와 Hive를 사용하여 Flutter 앱 테마 관리하기 (0) | 2023.10.05 |
---|---|
(Flutter) 플러터에서 Openai API 사용하기 (0) | 2023.04.13 |
(Flutter) Riverpod 상태 불변성(Immutability) (0) | 2023.03.30 |
(Flutter) Riverpod의 Provider Lifecycles (0) | 2023.03.29 |
(Flutter) Riverpod의 ProviderObserver (0) | 2023.03.29 |