[Flutter] Provider안에 Provider 사용하기

겨레·2024년 7월 24일
post-thumbnail

Riverpod의 꽃이라고 할 수 있는 Provider안에 Provider 사용하기를 해보자!

Provider 안에서 여러 개의 Provider를 watch, listen 하는 방법은
매우 중요하니깐 꼭 숙지하자...!


① screen/provider_screen.dart 파일 생성
기존에 만들어봤던 쇼핑 리스트 재현해서 해볼 거임!



② riverpod/provider.dart 파일 생성
ProviderScreen에 넣어줄 쇼핑리스트를 만들어주자!

provider 안에 ref. 하고 무언가를 쓸 때, read는 잘 사용하지 않는다는 점 기억하자!
(대부분 watch를 사용함... watch하고 있는 provider가 변경되면 최상위 Provider도 변경되어야 하므로!)

②-1. shoppingListProvider 불러오기
뭘 watch할거냐면... 전에 만들어둔 state_notifier_provider.dart를 그대로 watch 해볼거임!

filteredShoppingListProvider를 watch하면 어떤 결과가 나올까??

③ ProviderScreen 코드 수정하기
③-1. StatelessWidget → ConsumerWidget
③-2. build 함수에 WidgetRef ref 파라미터 추가

④ watch 해주기

  • provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/provider.dart';

class ProviderScreen extends ConsumerWidget {
  const ProviderScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(filteredShoppingListProvider);

    print(state);

    return DefalutLayout(
      title: 'ProviderScreen',
      body: ListView(
        children: const [],
      ),
    );
  }
}

⑤ HomeScreen에 버튼 추가


이렇게 하고 저장 후, 앱을 재실행해보자!

print(state) 찍힌 거 확인해보면...
✔ 뭐가 print 됐냐면... Instance of 'ShoppingItemModel'들이 프린트됐다.

✔ StateNotifierProvider 안에 있는 상태가 그대로 옮겨져서 온 것을 알 수 있다.

즉, Provider 안에서 ref.watch를 통해 shoppingListProvider를 그대로 넣었더니
shoppingListProvider 안에 있는 상태가 filteredShoppingList Provider에 그대로 전달됐다.


✔ 그럼... 똑같은 값들이라는 걸 증명해보자!
StateNotifierProviderScreen에서 state로 작업한 부분을 그대로 복사해서...
ProviderScreen에다가 붙여넣기 해보자.



  • provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/provider.dart';
import 'package:riverpod_study/riverpod/state_notifier_provider.dart';

class ProviderScreen extends ConsumerWidget {
  const ProviderScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(filteredShoppingListProvider);

    print(state);

    return DefalutLayout(
      title: 'ProviderScreen',
      body: ListView(
        children: state
            .map(
              (e) => CheckboxListTile(
                  title: Text(e.name),
                  // value => 체크박스의 체크 여부
                  // 이미 구매한 상태라면 체크가 되어 있음(true가 반환될테니깐)
                  value: e.hasBought,
                  onChanged: (value) {
                    ref.read(shoppingListProvider.notifier).toggleHasBought(
                        // 변경하고 싶은 값의 이름 넣어주기
                        name: e.name);
                  }),
            )
            .toList(),
      ),
    );
  }
}

그러면 이렇게 화면이 나오는데...

지금 내가 watch하고 있는 건 filteredShoppingListProvider 임!

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/riverpod/state_notifier_provider.dart';

final filteredShoppingListProvider = Provider(
  (ref) => ref.watch(shoppingListProvider),
);




filteredShoppingListProvider는 Provider인데,
지금 또 안에서 ref.watch를 해서 shoppingListProvider를 또 watch하고 있음!
그리고 ProviderScreen에서는 filteredShoppingListProvider를 watch하고 있음!

그럼 그 아래

여기에서 filterShoppingListProvider를 read 해서 함수를 실행해야 할 것 같지만,
그냥 그대로 shoppingListProvider를 사용
했음.

똑같은 shoppingListProvider이기 때문에 ProviderScreen 화면에서
김치를 체크하면, toggleHasBought가 샐행되면서
shoppingListProvider 안에 있는 김치의 hasBought가 true로 변경이 됨.

눌러보자!!

그럼 지금 내가 watch하는 건 분명히 filteredShoppingListProvider 이건데,
shoppingListProvider에서 값을 변경하면 그게 바로 filteredShoppingListProvider에도 반영
됨!


❓ 이게 되는 이유는??

ref.watch를 하면 위젯 안에서 watch를 하는 것처럼 똑같이 watch를 Provider 안에서 할 수 있음!!!

그러니까... ref.watch를 하고 어떤 Provider 값을 넣으면 반환되는 값이
shoppingListProvider의 state인데,
에로우펑션(=>)을 썼기 때문에 그 반환되는 값이 Provider에 또 반환이 됨.

그럼 반환되는 값이 변경될 때마다 Provider 값이 변경된다고 ref.watch에서 인식할 수 있음!
그래서 build가 다시 실행됨.



⑥ action 넣기
⑥-1. actions 코드 작성

⑥-2. 필터링 상태 지정해주기
provider.dart 코드로 돌아가서 먼저 필터링 상태를 지정해주자.

⑥-3. filterProvider 생성
그대로 provider.dart에서 필터링 상태를 지정한 코드 아래에다가
filterProvider 생성하는 코드를 작성해준다.
(그리고 간단한 작업이니까 StateProvider 써보자!)

이어서...

FilterState.all이라고 일단 기본으로 선언하고,
StateProvider 타입도 < >에다가 지정해줘야겠지~?


  • provider.dart 코드
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/riverpod/state_notifier_provider.dart';

final filteredShoppingListProvider = Provider(
  (ref) => ref.watch(shoppingListProvider),
);

// 필터링 상태 지정
enum FilterState {
  notSpicy, // 안매움
  spicy, // 매움
  all, // 전부
}

// filterProvider 생성
final filterProvider = StateProvider<FilterState>((ref) => FilterState.all);


이렇게 생성한 final filterProvider로 뭘 할 수 있을까?

⑥-4. PopupMenuButton
ProviderScreen으로 돌아가서 PopupMenuButton에다가
타입을 변경할 거라고 작성해준다.

그리고 itemBuilder는 일반적으로 쓰는 builder들...
(리스트뷰, 그리드뷰 같은데서 나오는 아이템 빌더랑 똑같음!)

다만, 여기에서는 List로 넣고싶은 옵션들을 넣어주면 됨!
그리고 이걸 팝업 메뉴 아이템으로 매핑해준다.


  • provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/provider.dart';
import 'package:riverpod_study/riverpod/state_notifier_provider.dart';

class ProviderScreen extends ConsumerWidget {
  const ProviderScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(filteredShoppingListProvider);

    print(state);

    return DefalutLayout(
      title: 'ProviderScreen',
      actions: [
        PopupMenuButton<FilterState>(
          // .values 하면 FilterState의 모든 값들을 가져올 수 있음
          // PopupMenuItemdm => PopupMenuButton 눌렀을 때 나오는 각각의 아이템들
          itemBuilder: (_) => FilterState.values
              .map(
                (e) => PopupMenuItem(
                  value: e, // value에는 e를 넣어 Text(e.name) 이 값이라고 보여주기
                  child: Text(
                    e.name,
                  ),
                ),
              )
              .toList(), // .map 했으니까 toList로 변경 해주기
        ),
      ],
      body: ListView(
        children: state
            .map(
              (e) => CheckboxListTile(
                  title: Text(e.name),
                  // value => 체크박스의 체크 여부
                  // 이미 구매한 상태라면 체크가 되어 있음(true가 반환될테니깐)
                  value: e.hasBought,
                  onChanged: (value) {
                    ref
                        .read(shoppingListProvider.notifier)
                        // 변경하고 싶은 값의 이름 넣어주기
                        .toggleHasBought(name: e.name);
                  }),
            )
            .toList(),
      ),
    );
  }
}

그럼 상단에 저렇게(파란색으로 표시해둔 거) 뜸!
그걸 누르면...

이렇게 나옴!!!

여기 나온 옵션들은 FilterState.values .... 해서 매핑해준 값들임!
각각의 메뉴들이 PopupMenuItem에 해당돼서
각각의 값(value: e)은 이 FilterState에 각각 값을 가지고 있고,
글자는 Text(e.name) 해서 이름들이 다 들어가짐.

그럼 이걸 누를 때마다 뭔가 action이 변경되어야겠지?
이걸 위해서 provider.dart에다가 filterProvider를 만든거임!!!



⑦ onSelected 추가하기
⑦-1. ProviderScreen 코드에 onSelected 코드를 추가한다.

그리고 나서 각 PopupMenuItem을 눌렀을 때 PopupMenuItem 이름이 잘 찍히는 걸 볼 수 있다.


⑦-2. ref.read 추가해서 값 변경하기
누를 때마다 filterProvider를 ref.read 해준다!

  • provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/provider.dart';
import 'package:riverpod_study/riverpod/state_notifier_provider.dart';

class ProviderScreen extends ConsumerWidget {
  const ProviderScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(filteredShoppingListProvider);

    print(state);

    return DefalutLayout(
      title: 'ProviderScreen',
      actions: [
        PopupMenuButton<FilterState>(
          // .values 하면 FilterState의 모든 값들을 가져올 수 있음
          // PopupMenuItemdm => PopupMenuButton 눌렀을 때 나오는 각각의 아이템들
          itemBuilder: (_) => FilterState.values
              .map(
                (e) => PopupMenuItem(
                  value: e, // value에는 e를 넣어 Text(e.name) 이 값이라고 보여주기
                  child: Text(
                    e.name,
                  ),
                ),
              )
              .toList(), // .map 했으니까 toList로 변경 해주기
          onSelected: (value) {
            // print(value); // 선택된 값  print하기
            // ref.read 해주기
            // 선택한 값이 (value)에 들어오니까 그 값으로 state(상태)를 매번 update
            ref.read(filterProvider.notifier).update((state) => value);
          },
        ),
      ],
      body: ListView(
        children: state
            .map(
              (e) => CheckboxListTile(
                  title: Text(e.name),
                  // value => 체크박스의 체크 여부
                  // 이미 구매한 상태라면 체크가 되어 있음(true가 반환될테니깐)
                  value: e.hasBought,
                  onChanged: (value) {
                    ref
                        .read(shoppingListProvider.notifier)
                        // 변경하고 싶은 값의 이름 넣어주기
                        .toggleHasBought(name: e.name);
                  }),
            )
            .toList(),
      ),
    );
  }
}

✔ 지금 내가 원하는 것은?

all을 눌렀을 때 모든 아이템들(김치, 라면, 삼겹살, 수박, 카스테라)이 나오고,
spicy를 누르면 spicy가 true인 아에팀들만 나오고,
notSpicy를 누르면 spicy가 false인 애들만 나오는 걸 원함.


이걸 설계하려면???

provider.dart로 돌아가서...
shoppingListProvider를 watch하고 있는 것까지는 좋음!
그런데, 이거를 일반 함수로 만든 다음 다시 watch해준다.

그리고 filterProvider를 어떻게 리스닝하면 되냐면...

이렇게 해 주면 됨.

그리고 실행하면 오류가 발생함!!
에러가 발생한 이유는 아무 값도 Provider에 반환해 주고 있지 않아서임.
즉, Provider는 지금 상태가 없는 것...

상태를 정의해주자!
List 하고 < ShoppingItemModel >을 반환...
그런데 아무 것도 반환 안 하고 있으니까
return [ ]; 해서 빈 리스트를 반환하도록 해 놓자.


다시 앱을 재실행하면 빈 화면이 나온다.

return에 빈 리스트 말고 진짜 가져오고싶은 건??

filterState에 따라서 그러니까 noSpicy면 shoppingListState 안에 있는 값들 중
맵지 않은 것만 가져오고, spicy면 매운 것만 가져오고,
all이면 모든 리스트를 가져오는 것!

요렇게 필터링을 하고 싶음!!!
이건 어떻게 하느냐...

  • provider.dart 코드
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/model/shopping_item_model.dart';
import 'package:riverpod_study/riverpod/state_notifier_provider.dart';

final filteredShoppingListProvider = Provider<List<ShoppingItemModel>>(
  (ref) {
    final filterState = ref.watch(filterProvider);
    final shoppingListState = ref.watch(shoppingListProvider);

    // 만약 filterState가 FilterState.all이면
    // shoppingListState를 통째로 반환
    if (filterState == FilterState.all) {
      return shoppingListState;
    }
    // 나머지 경우에는 쇼핑 리스트를 필터링
    return shoppingListState
        .where((element) =>

            // 만약 filterState가 FilterState.spicy이면
            // element.isSpicy만 반환하고,
            // 아닌 경우에는 !element.isSpicy 아닌 것만 반환
            filterState == FilterState.spicy
                ? element.isSpicy
                : !element.isSpicy)
        .toList();

    // return [];
  },
);

// 필터링 상태 지정
enum FilterState {
  notSpicy, // 안매움
  spicy, // 매움
  all, // 전부
}

// filterProvider 생성
final filterProvider = StateProvider<FilterState>((ref) => FilterState.all);

만약에 filterState에서 all을 누르면 shoppingListState를 그대로 반환.
(이 filterState는 action Button을 누를 때마다 변경할 수 있음)
만약 spicy를 누르면 spicy가 true인 것만 골라 return해 줌.
notSpicy를 누르면 !element.isSpicy(= notSpicy) 필터링해서 notSpicy인 것만 반환해 줌!

저장하고 앱 재실행 해보자!

ProviderScreen 버튼을 누르면 ProviderScreen 화면이 나옴.

체크 박스도 잘 작동함!

PopupMenuButton을 누르고 spicy를 누르면...

김치랑 라면만 남아있음.

왜??

StateNotifierProvider로 가서 보면...
김치랑 라면만 isSpicy가 true로 되어있음.

notSpicy를 눌러보면...

StateNotifierProvider에 isSpicy가 false인 것만 필터링되어 나옴!

all을 누르면...

이렇게 다 나옴! 그 뿐만 아니라 체크했던 상태까지 다 잘 적용되어 나옴!


정리해보기


두 가지를 리스닝 중임!
그 중 하나인 filterProvider는 간단하게 선언해 놓았음.


버튼을 누르면 선택하는 값들에 따라서 filterProvider가 변경됨.
그리고 이 filterProvider 값이 변경될 때마다 이 Provider가 재실행 됨!
왜냐... ref.watch를 해두었으니까!

그리고 체크 박스를 누를 때마다 shoppingListProvider가 업데이트 되고,
이것도 역시 ref.watch 하고 있기 때문에 final filteredShoppingListProvider = Provider 가 실행됨.

그런데!!!
filterProvider가 변경될 때마다
if (filterState == FilterState.all) 라면(버튼 중에서 all을 눌렀을 때라면...)
그냥 shoppingListState에 있는 모든 값들을 다 반환!

만약에 선택한 게 그게 아니고 spicy라면
.where((element) => filterState 필터링 과정을 거치도록
만약 spicy라면 element.isSpicy가 true인 것만 return하고,
notSpicy를 누르면 false(!element.isSpicy)인 것만 return함.

그래서...
final filterState = ref.watch(filterProvider);
final shoppingListState = ref.watch(shoppingListProvider);
이 값들이 변경될 때마다 filteredShoppingListProvider Provider가
계속 다시 실행되면서 새로운 값들을 만들어서 반환해주는 거임!

profile
호떡 신문지에서 개발자로 환생

0개의 댓글