이코노딩

[Flutter] 내가 보려고 정리한 Provider 예시 본문

언어/Dart & Flutter

[Flutter] 내가 보려고 정리한 Provider 예시

떼이로 2022. 12. 14. 15:44

간단한 상태관리를 하기 위한 Provider 예시

 

1. ChangeNotifier 클래스 만들기

 

위젯에 쓰일 상태변경이 필요한 함수, 변수 등을 모아 클래스로 만들어 준다.

예를 들어 배달의 민족과 같은 앱을 모델로 생각했을 때,

장바구니로 기준을 잡고 모델을 구축한다.

 

장바구니에 필요한 모델

예시) 주문리스트, 부분선택 체크박스 리스트, 전체선택 값 등

▶Model

class Order{
  String productName;
  int price;
  int count;

  Order({
    required this.price,
    required this.productName,
    required this.count,
  });
}

▶Provider (ChangeNotifier은 with을 해도되고 extends해도된다 상황에 맞게 쓰기)

class TestProvider with ChangeNotifier{
  List<Order> orderList = []; //주문리스트
  List<bool> checkList = []; //주문리스트의 부분선택리스트
  bool allCheck = true; //전체선택

  void addOrder(Order order){
    orderList.add(order); //장바구니 버튼을 누르면 오더리스트에 하나 넣기
    checkList.add(true); //orderList 길이만큼 체크리스트도 필요함.
    notifyListeners(); //상태 업데이트
  }

  void removeOrder(int index){
    orderList.removeAt(index); //장바구니에서 삭제 버튼 누르면 해당 인덱스로 접근하여 삭제
    checkList.removeAt(index); //체크리스트도 해당 인덱스 제거
    notifyListeners(); //업데이트
  }

  void changeCheck(int index){
    //체크박스누를 때 실행되는 함수 체크되있으면 해제, 해제되있으면 체크되게 조건문
    checkList[index] ? checkList[index] = false : checkList[index] = true;

    //전체선택 조건검사
    if(checkList.contains(false) && allCheck){
      //체크리스트에 false가있는데 전체 선택이 참인경우
      allCheck = false; //전체선택을 꺼줌
    }else if(checkList.every((element) => element == true) && !allCheck){
      //체크리스트가 전부 true인데 전체선택이 꺼진경우
      allCheck = true; //전체선택 켜기
    }
    notifyListeners();//업데이트
  }

  void changeAllCheck(){
    //전체선택을 눌렀을 때
    if(allCheck){
      allCheck = false;
      //체크리스트 아이템들을 전부 false로 바꾼후 checkList에 넣기
      checkList = checkList.map((e) => e = false).toList();
    }else{
      allCheck = true;
      checkList = checkList.map((e) => e =  true).toList();
    }
    notifyListeners(); //업데이트
  }

  void pressBuyBtn(){
    List<Order> buyList = []; //최종구매리스트
    //최종 구매하기 버튼 클릭시, 체크되어있는 부분만 확인하기
    for(var i = 0; i<checkList.length; i++){
      //체크리스트 인덱스 중 참인경우에만 최종구매리스트에 넣어줌
      if(checkList[i]){
        buyList.add(orderList[i]);
      }
    }
  }
}

2.  main.dart 최상위에 create하기

최상위에 해야하는 이유 https://iconoding.tistory.com/38 참고

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    //Provider를 여러개 쓰는 경우 MultiProvider 근데 보통 하나의 앱에 프로바이더는 수두룩하게 쓰이니까...
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_)=> TestProvider()),
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const TestPage(title: 'Provider Demo Test Page'),
      ),
    );
  }
}

이제 위젯에 가져다 쓰기만 하면된다.

3.  Widget만들기

class TestPage extends StatefulWidget {
  const TestPage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<TestPage> createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  //프로바이더 선언시 context가 필요하므로 나중에 넣어주기
  late TestProvider testProvider;

  @override
  Widget build(BuildContext context) {
    testProvider = Provider.of<TestProvider>(context);
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 52,
        title: Text(widget.title),
        centerTitle: true,
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
        child: CustomScrollView(
          slivers: [
            //비어있니?
            testProvider.orderList.isEmpty ?
                const SliverToBoxAdapter() : SliverToBoxAdapter(
             child: Row(children: [
               Checkbox(value: testProvider.allCheck, onChanged: (value) => testProvider.changeAllCheck()),
               const Text('전체선택')
             ],),
            ),
            testProvider.orderList.isEmpty ?
                SliverToBoxAdapter(
                  child: SizedBox(
                    height: MediaQuery.of(context).size.height - 52,
                    width: MediaQuery.of(context).size.width - 52,
                    child: const Center(child: Text('장바구니 비어있음'),),
                  ),
                ) :
                SliverFixedExtentList(
                    delegate: SliverChildBuilderDelegate(
                      childCount: testProvider.orderList.length,
                      (context, index) => Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Checkbox(value: testProvider.checkList[index], onChanged: (value) => testProvider.changeCheck(index),),
                          Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                            children: [
                              Text(testProvider.orderList[index].productName),
                              Text('${testProvider.orderList[index].price}원'),
                              Text('${testProvider.orderList[index].count}개')
                            ],
                          ),
                          IconButton(onPressed: ()=> testProvider.removeOrder(index), icon: const Icon(Icons.delete))
                        ],
                      ),
                    ),
                    itemExtent: 100),
            testProvider.orderList.isEmpty ?
                const SliverToBoxAdapter() : SliverToBoxAdapter(
              child: ElevatedButton(
                child: const Text('구매하기'),
                onPressed: ()=> testProvider.pressBuyBtn(),
              ),
            )
          ],
        ),
      ),
    );
  }
}

Widget을 만들고 Provider.of(context)로 접근하여 사용하면된다.

아니면 <T>는 Provider 

✅ context.read<T>() //상태변화를 읽지는 않고 메소드에 접근할 때 사용

✅ context.watch<T>() //상태변화를 읽음

를 사용하면 된다.

결과

OrderList가 비어있을때

동작하는데 문제는 없다.

삭제되었을때도 전체선택 검사가 필요해 보인다.

 


▶ Consumer사용하기

Context를 사용하지 않고 Provider를 사용할 수 있게해주는 위젯이다.

Stateless를 사용하고 가벼운 상태변경에 쓰거나 화면구성중 작은 부분만 변경할 때, 사용하기 좋을 것 같다.

class ConsumerTestPage extends StatelessWidget {
  const ConsumerTestPage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 52,
        title: Text(title),
        centerTitle: true,
      ),
      body: Consumer<TestProvider>(
        builder: (context, provider, widget){
          if(provider.orderList.isEmpty){
            return SizedBox(
              height: MediaQuery.of(context).size.height -52,
              width: MediaQuery.of(context).size.width,
              child: const Center(child: Text('장바구니 비어있음'),),
            );
          }else{
            return CustomScrollView(
              slivers: [
                SliverToBoxAdapter(
                  child: Row(children: [
                    Checkbox(value: provider.allCheck, onChanged: (value) => provider.changeAllCheck()),
                    const Text('전체선택')
                  ],),
                ),
                SliverFixedExtentList(
                    delegate: SliverChildBuilderDelegate(
                      childCount: provider.orderList.length,
                          (context, index) => Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Checkbox(value: provider.checkList[index], onChanged: (value) => provider.changeCheck(index),),
                          Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                            children: [
                              Text(provider.orderList[index].productName),
                              Text('${provider.orderList[index].price}원'),
                              Text('${provider.orderList[index].count}개')
                            ],
                          ),
                          IconButton(onPressed: ()=> provider.removeOrder(index), icon: const Icon(Icons.delete))
                        ],
                      ),
                    ),
                    itemExtent: 100),
                SliverToBoxAdapter(
                  child: ElevatedButton(
                    child: const Text('구매하기'),
                    onPressed: ()=> provider.pressBuyBtn(),
                  ),
                )
              ],
            );
          }
        },
      ),
    );
  }
}

 동작은 위 이미지와 똑같이 잘 동작함


Git: https://github.com/Inhochoi0201/ProviderSample