본문으로 바로가기
  1. Home
  2. Flutter/Package
  3. Flutter Riverpod All Providers (5-6) - StateProvider

Flutter Riverpod All Providers (5-6) - StateProvider

· 댓글개 · Dev_Whale

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);
        },
      ),
    );
  }
}
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.