Flutter

7-1. Debounce 실습

모리선생 2023. 4. 3. 07:05
728x90

Debounce

 

정의
여러번 발생하는 이벤트에서 가장 마지막 이벤트만을 실행하는 방법

 

예시
쇼핑몰 장바구니의 작동원리에 빗대어서 설명하자면 사용자가 장바구니에 상품을 추가하거나 삭제할때마다 서버에 요청을 보내는 것이 아니라, 사용자가 장바구니 편집을 완료한 후에 한번만 서버에 요청을 보내는 것.

 

코드예시

 

Debounce

 

작동 순서

  • BasketProvider 클래스의 생성하면서 updateBasketDebounce 인스턴스가 생성이 됨
  • UpdateBasketDebounce의 values Stream이 patchBasket 함수를 리스닝함
  • addToBasket 또는 removeFromBasket 함수가 호출이 됨
  • 상품을 추가하거나 삭제하면서 상태를 업데이트 함 (Optimistic Response)
  • updateBasketDebounce의 setvalue 메서드를 호출하여 Stream에 null값을 전달함
  • Duration이 지난 후에 다른 값이 들어오지 않으면 Stream에 null 값을 흘려보냄
  • patchBasket 함수가 실행되어 서버와 동기화함 (Debounce)

(참고) Optimistic Response
요청에 대한 응답이 성공했다고 가정하고 상태를 먼저 업데이트 하는 방법

 

예시코드 (참고:sht-3756.log - 흐름이 어떤식으로 진행되는지만 한번 알아보자.)

class BasketProvider extends StateNotifier<List<BasketItemModel>> {
  final UserMeRepository repository;
  // debounce 선언 및 초기화
  final updateBasketDebounce = Debouncer(
    Duration(seconds: 1),
    // 초기값은 null 로, super 생성자에 파라미터로 넘겨줄게 없기 떄문
    initialValue: null,
    checkEquality: false,
  );

  BasketProvider({required this.repository}) : super([]) {
    // patchBasket 함수 실행
    updateBasketDebounce.values.listen((state) {
      patchBasket();
    });
  }

  // 장바구니에 아이템 추가 로직
  Future<void> addToBasket({
    // ProductModel 을 전부다 받는다. ( 추가하려는 상품 id )
    required ProductModel product,
  }) async {
    ...
    // Optimistic Response (긍정적 응답)
    // 요청에 대한 응답이 성공했다고 가정하고 상태를 먼저 업데이트
    // await patchBasket();
    updateBasketDebounce.setValue(null);
  }

  // 장바구니 삭제
  Future<void> removeFromBasket({
    required ProductModel product,
    bool isDelete = false,
  }) async {
  ...
    // Optimistic Response (긍정적 응답)
    // 요청에 대한 응답이 성공했다고 가정하고 상태를 먼저 업데이트
    // await patchBasket();
    updateBasketDebounce.setValue(null);
  }
}

이런식으로 흐름이 흘러간다고 보면 된다.


그럼 이제 실제 예제를 만들면서 한번 Debounce를 어떻게 활용할 수 있는지 알아보자.

 

전체예제

질문: “Debounce를 사용 시 장바구니에 물건 수량과 금액이 담기는 것까지의 화면을 구성해줘, 화면 하단에는 총액이 있어야하고 결제 버튼이 하단에 있으면 되. 하지만 결제 버튼이 실제로 결제 기능을 가지고 있을 필요는 없어. 다만 버트만 생성해놓으면 되. 장바구니는 floating button으로 해주고. 주문 가능한 물건은 사과 (2000 원), 레몬 (3000 원), 바나나 (4000원)으로 해줘.

 

설명: Flutter에서 Debounce를 구현하는 방법은 여러 가지가 있습니다. 가장 간단한 방법은 Timer 클래스를 사용하는 것입니다. Timer 클래스는 일정 시간 후에 콜백 함수를 실행할 수 있게 해줍니다.

 

예시코드: 

이 코드는 TextField의 값이 변경될 때마다 500 밀리초 동안 대기하고, 그 사이에 다른 값이 입력되면 타이머를 취소합니다. 따라서 마지막으로 입력된 값만 처리하게 됩니다.

Timer? _debounce;

void _onChanged(String value) {
  if (_debounce?.isActive ?? false) _debounce?.cancel();
  _debounce = Timer(const Duration(milliseconds: 500), () {
    // do something with value
  });
}

 

로직:

  1. 장바구니 화면을 구성하기 위해서는 먼저 주문 가능한 물건들을 나열하는 ListView와 장바구니 버튼을 가진 Scaffold 위젯을 만들어야 합니다.
  2. ListView의 itemBuilder에서는 각 물건의 이름, 가격, 수량을 보여주고, 수량을 조절할 수 있는 Stepper 위젯과 장바구니에 추가할 수 있는 Checkbox 위젯을 배치합니다.
  3. Scaffold의 floatingActionButton으로는 장바구니 버튼을 만들고, onPressed 이벤트에서는 장바구니에 담긴 물건들의 목록과 총액을 보여주는 다이얼로그를 띄웁니다. 다이얼로그에는 결제 버튼도 추가합니다.

예시코드

import 'package:flutter/material.dart';
import 'dart:async';

class ShoppingCartScreen extends StatefulWidget {
  const ShoppingCartScreen({Key? key}) : super(key: key);

  @override
  _ShoppingCartScreenState createState() => _ShoppingCartScreenState();
}

class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
  // 주문 가능한 물건들의 리스트
  List<Item> items = [
    Item(name: '사과', price: 2000),
    Item(name: '레몬', price: 3000),
    Item(name: '바나나', price: 4000),
  ];

  // 장바구니에 담긴 물건들의 리스트
  List<Item> cart = [];

  // 장바구니 총액
  int total = 0;

  // 타이머 객체
  Timer? _debounce;

void _onChanged(String value) {
  if (_debounce?.isActive ?? false) _debounce?.cancel();
  _debounce = Timer(const Duration(milliseconds: 500), () {
    // do something with value
  });
}

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('장바구니 화면'),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          Item item = items[index];
          return ListTile(
            title: Text('${item.name} (${item.price} 원)'),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                // 수량 조절 위젯
                Stepper(
                  value: item.quantity,
                  min: 0,
                  max: 10,
                  onChanged: (value) {
                    setState(() {
                      item.quantity = value;
                    });
                  },
                ),
                // 장바구니 추가 체크박스 위젯
                Checkbox(
                  value: item.checked,
                  onChanged: (value) {
                    setState(() {
                      item.checked = value ?? false;
                      // 장바구니에 추가하거나 제거하는 로직
                      if (item.checked) {
                        cart.add(item);
                      } else {
                        cart.remove(item);
                      }
                      // 총액을 계산하는 로직
                      total = 0;
                      for (Item i in cart) {
                        total += i.price * i.quantity;
                      }
                    });
                  },
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.shopping_cart),
        onPressed: () {
          // 장바구니 버튼을 누르면 다이얼로그를 띄우는 로직
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              title: Text('장바구니'),
              content:Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  // 장바구니에 담긴 물건들의 목록을 보여주는 위젯
                  ListView.builder(
                    shrinkWrap: true,
                    itemCount: cart.length,
                    itemBuilder: (context, index) {
                      Item item = cart[index];
                      return ListTile(
                        title: Text('${item.name} (${item.price} 원)'),
                        subtitle: Text('수량: ${item.quantity}'),
                      );
                    },
                  ),
                  // 총액을 보여주는 위젯
                  Text('총액: $total 원'),
                ],
              ),
              actions: [
                // 결제 버튼을 만드는 로직
                ElevatedButton(
                  child: Text('결제'),
                  onPressed: () {
                    // 결제 기능은 구현하지 않고 다이얼로그를 닫는 로직
                    Navigator.pop(context);
                  },
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

// 물건 클래스
class Item {
  String name; // 이름
  int price; // 가격
  int quantity; // 수량
  bool checked; // 장바구니에 추가 여부

  Item({required this.name, required this.price})
      : quantity = 0,
        checked = false;
}

 

사용화면

: 준비중

 

참고:

  1. sht-3756.log https://velog.io/@sht-3756/Debounce%EC%99%80Throttle%EC%82%AC%EC%9A%A9

    2. RxJSMarble https://rxmarbles.com/

728x90

'Flutter' 카테고리의 다른 글

10. freezed  (0) 2023.04.09
7-2. Throttle 실습  (0) 2023.04.03
[Practice] shared preference  (0) 2023.04.02
[Practice] carousel_slider  (0) 2023.03.31
[플러그인] 사용하기 좋은 플러그인 7가지 소개  (0) 2023.03.30