[Flutter] 상태 관리 7편(State Management) - Cubit

Tyger·2023년 1월 14일
1

State Management

목록 보기
7/14

상태 관리 7편(State Management) - Cubit

상태 관리(State Management) 1편 - State Ful
상태 관리(State Management) 2편 - Value Listenerable
상태 관리(State Management) 3편 - Get X [Simple]
상태 관리(State Management) 4편 - Get X [Reactive]
상태 관리(State Management) 5편 - Provider
상태 관리(State Management) 6편 - Bloc
상태 관리(State Management) 8편 - Riverpod
상태 관리(State Management) 9편 - Mobx

Top 7 Flutter State Management Libraries In 2022
Most Popular Packages for State Management in Flutter (2023)

flutter_bloc | Flutter Packages

이번 글에서는 이전 글에서 다뤘던 Bloc 라이브러리에 내장된 또다른 상태 관리 방식인 cubit에 대해서 글을 작성해 보겠다.

만약에 bloc처럼 복잡하고 어려운 상태 관리에 cubit이 없었다면 이렇게 까지 bloc을 쉽게 사용해 볼수 있었을까 라는 생각이들 정도로 cubit은 bloc의 복잡한 부분을 간편하게 사용할 수 있게 해주고 있다.

Cubit은 bloc에 비하면 정말 심플하고 가볍다. 프로젝트를 진행하다 가벼운 기능과 상태는 cubit으로 사용할 수 있도록 해줬기에 bloc이 많은 선택을 받을 수 있지 않았을까 라는 생각이 된다.

dependencies

bloc: ^8.1.0
flutter_bloc: ^8.1.1

Count App

카운터 앱은 Flutter 프로젝트 최초 생성시 기본으로 있는 카운트 앱을 약간 변형하여 리셋 기능을 추가하고 단순히 카운트 상태를 증가/감소만 하는 것이 아닌 얼마 만큼을 증가/감소 시킬지에 대한 상태를 추가하여 해당 값 만큼 증가/감소하는 기능을 가지게끔 만든 예제이다.

모든 상태관리 예제는 해당 기능을 가진 카운트 앱으로 만들어 볼 것이다.

UI

앞으로 모든 상태관리에 동일한 UI파일을 사용할 거여서 상태관리 편에서 UI 내용은 다른 글과 동일할 것이다.

UI는 가운데 카운트를 보여줄 숫자가 있고 바로 하단 Row위젯안에 더하기, 마이너스 아이콘을 배치해뒀다. 그 아래로 reset 기능을 호출할 버튼을 만들었다.

카운트 기능을 사용하는게 단순히 숫자만 올리고 내리는 것이 아니라 얼만큼을 증가시키고 감소시킬지를 선택할 수 있는 넘버 박스들을 왼쪽 상단에 수직으로 배치하여 구성하였다.

여기서는 간단한 상태 관리만 보여주는 정도의 UI여서 다른 글에서 각각의 상태 관리에 대해서 더 깊숙하고 복잡한 UI 구조를 만들어서 사용해 볼 예정이다.

아래 공유한 Git Repository를 방문하면 소스 코드를 오픈해 뒀습니다 !

Stack countScreenPublicUI({
  required BuildContext context,
  required int count,
  required int selectCount,
  required Function() onIncrement,
  required Function() onDecrement,
  required Function() onReset,
  required Function(int) onCount,
}) {
  return Stack(
    children: [
      Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          SizedBox(
              width: MediaQuery.of(context).size.width,
              child: Center(
                child: Text(
                  "$count",
                  style: const TextStyle(
                      fontSize: 60, fontWeight: FontWeight.bold),
                ),
              )),
          const SizedBox(height: 24),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              GestureDetector(
                onTap: onIncrement,
                child: const Icon(
                  Icons.add_circle_outline,
                  size: 40,
                ),
              ),
              const SizedBox(width: 24),
              GestureDetector(
                onTap: onDecrement,
                child: const Icon(
                  Icons.remove_circle_outline,
                  size: 40,
                ),
              )
            ],
          ),
          const SizedBox(height: 24),
          GestureDetector(
            onTap: onReset,
            child: Container(
              width: MediaQuery.of(context).size.width / 3,
              height: 48,
              decoration: BoxDecoration(
                  color: const Color.fromRGBO(71, 71, 71, 1),
                  borderRadius: BorderRadius.circular(12)),
              child: const Center(
                child: Text(
                  'Reset',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ),
            ),
          ),
          const SizedBox(height: 40),
        ],
      ),
      Positioned(
        top: 20,
        child: SizedBox(
          height: MediaQuery.of(context).size.height,
          child: Padding(
            padding: const EdgeInsets.only(left: 20),
            child: Column(
              children: [
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 1),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 10),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 20),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 50),
                countAppSelectedCountBox(
                    onTap: onCount, selectNumber: selectCount, number: 100),
              ],
            ),
          ),
        ),
      ),
    ],
  );
}
GestureDetector countAppSelectedCountBox({
  required Function(int) onTap,
  required int number,
  required int selectNumber,
}) {
  return GestureDetector(
    onTap: () => onTap(number),
    child: Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Container(
        width: 48,
        height: 48,
        decoration: BoxDecoration(
            color: selectNumber == number
                ? const Color.fromRGBO(91, 91, 91, 1)
                : const Color.fromRGBO(61, 61, 61, 1),
            borderRadius: BorderRadius.circular(12)),
        child: Center(
            child: Text(
          '$number',
          style: TextStyle(
              fontWeight: FontWeight.bold,
              color: selectNumber == number
                  ? Colors.white
                  : const Color.fromRGBO(155, 155, 155, 1)),
        )),
      ),
    ),
  );
}

Cubit의 생성 방식은 bloc과 똑가팅 생성하고 사용할 수 있다.
차이점은 cubit에는 event가 없기에 context.read로 접근하여 바로 기능을 호출할 수 있다는 점이다.

    return BlocProvider<CountAppCubitCubit>(
      create: (context) => CountAppCubitCubit(),
      child: BlocBuilder<CountAppCubitCubit, CountAppCubitState>(
        builder: (context, state) {
          return Scaffold(
            appBar: appBar(title: 'Count App With Cubit'),
            body: countScreenPublicUI(
              context: context,
              count: state.count,
              selectCount: state.selectCount,
              onIncrement: () {
                HapticFeedback.mediumImpact();
                context.read<CountAppCubitCubit>().increment();
              },
              onDecrement: () {
                HapticFeedback.mediumImpact();
                context.read<CountAppCubitCubit>().decrement();
              },
              onReset: () {
                HapticFeedback.mediumImpact();
                context.read<CountAppCubitCubit>().reset();
              },
              onCount: (int number) {
                HapticFeedback.mediumImpact();
                context.read<CountAppCubitCubit>().select(number);
              },
            ),
          );
        },
      ),
    );

Cubit

Cubit은 bloc에 비해 심플하고 event 개념이 없기에 provider, get과 유사한 상태 흐름을 보여주고 있다.

count_state.dart

State 생성 방식은 bloc과 똑같이 사용하면 된다.

class CountAppCubitState extends Equatable {
  final int count;
  final int selectCount;

  const CountAppCubitState({
    this.count = 0,
    this.selectCount = 1,
  });

  CountAppCubitState copyWith({
    int? count,
    int? selectCount,
  }) {
    return CountAppCubitState(
      count: count ?? this.count,
      selectCount: selectCount ?? this.selectCount,
    );
  }

  
  List<Object> get props => [count, selectCount];
}

Count_cubit.dart

Cubit을 생성하여 상태를 초기화 하는 방식도 bloc과 같지만 cubit에는 event가 없기에 cubit 블럭안에서 기능을 만들어 상태를 변경하여 사용할 수있다.

class CountAppCubitCubit extends Cubit<CountAppCubitState> {
  CountAppCubitCubit() : super(const CountAppCubitState());

  void increment() {
    emit(state.copyWith(count: state.count + state.selectCount));
  }

  void decrement() {
    emit(state.copyWith(count: state.count - state.selectCount));
  }

  void reset() {
    emit(state.copyWith(count: 0));
  }

  void select(int count) {
    emit(state.copyWith(selectCount: count));
  }
}

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/count_app/cubit

마무리

가볍게 다뤄봤지만 이렇게 간단한 기능에도 cubit과 bloc의 차이점은 뚜렷하게 알 수 있다.
Cubit도 다른 라이브러들과 같이 규모가 커지면 관리가 어려워지지만, cubit으로만 생성되는 프로젝트는 거의 없고 보통 bloc의 단점만을 보완할 때 주로 사용되기에 적절한 시점에 cubit을 사용하여 개발하는 습관을 갖는게 좋다.

이렇게 여기까지가 제가 주로 사용하던 라이브러리이고, 앞으로 다뤄볼 상태 관리는 저도 처음 접해본 Riverpod, Mobx에 대해서 작성해 보겠다.

profile
Flutter Developer

0개의 댓글