본문 바로가기

운동하는 개발자/Flutter

13년차 응용프로그래머, Gemini에게 Flutter 배우기 - 7 MVVM (2)

728x90

상품을 빼는 기능을 만들어 보라고 함

MFC에 쩔어버린 나는 ListView에서 바로 삭제할 방법에 대해서 검색해봄
removeAt 함수로 가능한걸 확인하고 코드를 생각없이 바로 작성했는데 에러가 뜸

The method 'setState' isn't defined for the type 'CartListView'.
Try correcting the name to the name of an existing method, or defining a method named 'setState'.

원인은 클래스가 StatelessWidget여서 그렇고 StatefulWidget로 바꿔줘야 가능하다고 함
근데 나는 MVVM패턴을 위해 다 StatelessWidget로 가게할껀데..?
이때 알아차림 View에서 데이터를 조작하려고 했던것을.. ㅠ

결제완료버튼을 보면 이런식으로 ViewModel의 함수를 호출해서 데이터를 수정하고 provider로 갱신을 수신하는 방식임을 다시 확인

onPressed: () => context.read<KioskProvider>().clearCart(),


기존에 ListTile에 trailing에는 금액을 표시하는 Text가 있으므로 Row로 공간을 나눈 뒤 Children으로 분할시켜주고
IconButton을 만들었고 Icons.delete 버튼 사용함(엑스를 예상했는데 세련된 휴지통모양이래서 놀람 ㅎ;)

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

  @override
  Widget build(BuildContext context) {
    // 1. 장바구니 목록 상태 구독
    final cartItems = context.watch<KioskProvider>().cartItems;

    if (cartItems.isEmpty) {
      return const Center(child: Text('장바구니가 비어 있습니다.'));
    }

    return ListView.builder(
      itemCount: cartItems.length,
      itemBuilder: (context, index) {
        final item = cartItems[index];
        return ListTile(
          title: Text(item.product.name),
          subtitle: Text('${item.product.price}원 x ${item.quantity}'),
          trailing: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('${item.totalItemPrice}원', style: const TextStyle(fontWeight: FontWeight.bold)),
              IconButton(
                onPressed: () {
                  context.read<KioskProvider>().removeFromCart(index);
                },
                icon: const Icon(Icons.delete),
              ),
            ],
          ),
        );  // ListTile 닫는 괄호 + 세미콜론
      },
    );      
  }
}

그리고 정상작동 확인

다음스탭으로 넘어가서 화면 알림을 어떻게 구성할지에 대해 진행함

가장 많이 사용하는 방법으로 알려달라고 했더니 주로 Future를 사용한다고 했음
나는 C++에서 Future와 promise에 대해 학습은 해봤지만 실무에서 실제 사용해본적은 없었으나 코드보니 Flutter도 거의 비슷했음

//Kiosk_provider.dart

Future<bool> processPayment() async {
    if (_cartItems.isEmpty) return false;

    try {
      // 1. 가상의 네트워크/DB 지연 시간 (예: 1초)
      await Future.delayed(const Duration(seconds: 1)); 
      
      // 2. 결제 완료 처리
      _cartItems.clear();
      notifyListeners();
      
      // 3. 뷰에게 성공했다고 결과만 툭 던져줍니다.
      return true; 
    } catch (e) {
      return false; // 실패
    }
  }
//main.dart

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

  @override
  Widget build(BuildContext context) {
    // 1. 총 결제 금액 상태 구독
    final totalPrice = context.watch<KioskProvider>().totalPrice;

    return Container(
      color: Colors.brown[50],
      padding: const EdgeInsets.all(24.0),
      child: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('총 결제 금액:', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              Text('$totalPrice원', style: const TextStyle(fontSize: 24, color: Colors.redAccent, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 16),
          SizedBox(
            width: double.infinity,
            height: 60,
            child: ElevatedButton(
              style: ElevatedButton.styleFrom(backgroundColor: Colors.brown),
              // 2. 결제 완료 (장바구니 초기화) 함수 호출
              //onPressed: () => context.read<KioskProvider>().clearCart(), //여기 수정함
              onPressed: () async{
                final isSuccess = await context.read<KioskProvider>().processPayment();

                if(!context.mounted) return; //provider에서 딜레이 처리 중 프로그램 종료 등 상황 체크 필수 놓치면 access violation

                if(isSuccess){
                  showDialog(
                    context: context,
                    builder: (context) => AlertDialog(
                      title: const Text('결제 성공'),
                      content: const Text('주문이 완료되었습니다.'),
                      actions: [
                        TextButton(
                          onPressed: () => Navigator.pop(context),
                          child: const Text('확인'),
                          ),
                      ],
                    ),
                  );
                }
              },
              child: const Text('결제하기', style: TextStyle(fontSize: 20, color: Colors.white)),
            ),
          ),
        ],
      ),
    );
  }
}

이후 정상동작 확인

728x90