StateProvider는 상태를 수정하는 방법을 노출하는 Provider 입니다. 이는 매우 간단한 사용 사례에 대해 Notifier 클래스를 작성할 필요가 없도록 설계된 NotifierProvider의 단순화입니다.
StateProvider는 주로 사용자 인터페이스에서 간단한 변수를 수정할 수 있도록 하기 위해 존재합니다.
StateProvider의 상태는 일반적으로 다음 중 하나입니다:
- 필터 유형과 같은 열거형
- 문자열, 일반적으로 텍스트 필드의 원시 콘텐츠
- boolean, 체크박스의 경우
- 숫자(페이지 매김 또는 연령 양식 필드의 경우)
다음과 같은 경우에는 StateProvider를 사용해서는 안 됩니다:
- 상태에 유효성 검사 로직이 필요한 경우
- 상태가 복잡한 객체(예: 사용자 정의 클래스, 목록/맵 등)인 경우.
- 상태 수정을 위한 로직은 단순한 count++보다 더 복잡합니다.
좀 더 복잡한 경우에는 NotifierProvider를 대신 사용하고 Notifier 클래스를 생성하는 것을 고려해 보세요.
처음에는 상용구가 조금 더 커지지만, 커스텀 Notifier 클래스를 사용하면 상태의 비즈니스 로직을 한 곳에 중앙 집중화할 수 있으므로 프로젝트의 장기적인 유지 관리에 매우 중요합니다.
사용 예시: 드롭다운을 사용하여 필터 유형 변경하기
StateProvider의 실제 사용 사례는 드롭다운/텍스트 필드/체크박스와 같은 간단한 양식 구성 요소의 상태를 관리하는 것입니다.
특히 StateProvider를 사용하여 제품 목록의 정렬 방식을 변경할 수 있는 드롭다운을 구현하는 방법을 살펴보겠습니다.
간단하게 하기 위해 애플리케이션에서 직접 얻을 수 있는 제품 목록은 다음과 같습니다:
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를 사용하여 네트워크 요청을 통해 얻을 수 있습니다.
그러면 사용자 인터페이스에 제품 목록이 표시될 수 있습니다:
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} \$'),
);
},
),
);
}
이제 기본 작업을 마쳤으므로 가격 또는 이름별로 제품을 필터링할 수 있는 드롭다운을 추가할 수 있습니다.
이를 위해 드롭다운 버튼을 사용하겠습니다.
// An enum representing the filter type
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를 생성하고 드롭다운의 상태를 provider와 동기화해 보겠습니다.
먼저 StateProvider를 생성해 보겠습니다:
final productSortTypeProvider = StateProvider<ProductSortType>(
// 여기서는 기본 정렬 유형인 이름을 반환합니다.
(ref) => ProductSortType.name,
);
그런 다음 다음을 수행하여 이 provider를 드롭다운에 연결할 수 있습니다:
DropdownButton<ProductSortType>(
// 정렬 유형이 변경되면 드롭다운이 다시 작성되어 표시된 아이콘이 업데이트됩니다.
value: ref.watch(productSortTypeProvider),
// 사용자가 드롭다운과 상호 작용하면 provider 상태가 업데이트됩니다.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),
이제 정렬 유형을 변경할 수 있습니다.
아직 제품 목록에는 영향을 미치지 않습니다!
이제 마지막 단계입니다: 제품 목록을 정렬하도록 제품 provider를 업데이트합니다.
이를 구현하는 핵심 구성 요소는 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));
}
});
그게 다입니다! 정렬 유형이 변경되면 사용자 인터페이스에서 제품 목록을 자동으로 다시 렌더링하는 데 이 정도면 충분합니다.
다음은 Dartpad의 전체 예제입니다:
// This code is distributed under the MIT License.
// Copyright (c) 2022 Remi Rousselet.
// You can find the original at https://github.com/rrousselGit/riverpod.
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>(
// We return the default sort type, here name.
(ref) => ProductSortType.name,
);
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));
}
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
// When the sort type changes, this will rebuild the dropdown
// to update the icon shown.
value: ref.watch(productSortTypeProvider),
// When the user interacts with the dropdown, we update the provider state.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
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} \$'),
);
},
),
);
}
}
provider를 두 번 읽지 않고 이전 값을 기반으로 상태를 업데이트하는 방법
간혹 이전 값을 기반으로 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: () {
// 이전 값에서 상태를 업데이트하는 중이므로 결국 provider를 두 번 읽게 되었습니다!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}
이 스니펫에 특별히 잘못된 점은 없지만 구문이 약간 어색합니다.
구문을 좀 더 개선하기 위해 업데이트 함수를 사용할 수 있습니다. 이 함수는 현재 상태를 수신하고 새 상태를 반환할 것으로 예상되는 콜백을 받습니다.
이 함수를 사용하여 이전 코드를 다음과 같이 리팩터링할 수 있습니다:
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);
},
),
);
}
}
'Flutter > Package' 카테고리의 다른 글
Flutter Riverpod Concepts(6-1) - Providers (0) | 2023.07.18 |
---|---|
Flutter Riverpod All Providers (5-7) - ChangeNotifierProvider (0) | 2023.07.18 |
Flutter Riverpod All Providers (5-5) - StreamProvider (0) | 2023.07.17 |
Flutter Riverpod All Providers (5-4) - FutureProvider (0) | 2023.07.17 |
Flutter Riverpod All Providers (5-3) - StateNotifierProvider (0) | 2023.07.16 |