[Flutter] 상태 관리 5편(State Management) - Provider

Tyger·2023년 1월 11일
3

State Management

목록 보기
5/14

상태 관리 5편(State Management) - Provider

상태 관리(State Management) 1편 - State Ful
상태 관리(State Management) 2편 - Value Listenerable
상태 관리(State Management) 3편 - Get X [Simple]
상태 관리(State Management) 4편 - Get X [Reactive]
상태 관리(State Management) 6편 - Bloc
상태 관리(State Management) 7편 - Cubit
상태 관리(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)

provider | Flutter Packages

이전 글에서는 현재 내가 현업에서 사용하는 Get X에 대해서 알아보았고, 이번 글부터는 가장 좋아하고 주로 사용하는 provider / bloc / cubit에 대해서 알아보도록 하겠다.

개인적으로는 Get X를 가장 많은 시간동안 사용하고는 있지만 아직도 개인 프로젝트나 상태 관리 공부는 전부 provider + bloc을 사용하고 있다.

Get X도 분명 좋은 라이브러리이고 매우 강력한 기능을 가지고 있다고 생각이 들지만 작은 규모의 프로젝트를 넘어서면 디버깅이 정말 힘들어진다는 분명한 단점을 느꼇기에 작은 규모의 프로젝트에서는 provider, 중규모 provider+bloc, 대규모 bloc이라는 생각은 아직도 변함이 없다.

Get X를 싫어한다고 생각할 수 있지만 그래도 현업에서는 잘 사용하고 있다.

Provider 라이브러리는 제가 뽑은 최고의 상태 관리 라이브러리라고 생각이 된다. 해당 상태 관리에 대해서는 워낙 잘 설명을 해주신 다른 글들이 많기에 여기서는 자세히 다루지는 않을 예정이다.

dependencies

provider: ^6.0.5

main.dart

Provider 사용을 위해서 아래 코드를 추가해 준다.

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  Provider.debugCheckInvalidValueType = null;
  runApp(const App());
}

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)),
        )),
      ),
    ),
  );
}

Provider를 사용하기 위해서는 ChangeNotifierProvider를 사용하여 생성한 provider를 사용 전 미리 등록을 하여야 한다.

Provider 등록에는 한 개만 등록을 할 수도 있고, 여러 개를 한꺼번에 등록하게 해주는 MultiProvider를 사용하여 등록하여도 된다.

중요한건 모든 상태 관리 라이브러리들이 그렇듯이 사용 전 반드시 생성을 하여 등록을 해주는 절차가 필요하다.

Provider를 UI 상태에서 소비할 때는 Consumer를 사용하도 되고 context.read / context.watch를 사용하여도 된다. 주로 빌더 형태에서 개발을 하는 것을 선호하다 보니 여기서는 Consumer를 사용하였다.

Consumer에서는 BuildContext, Provider, Widget을 받아와야 하며, 사용할 때는 Provider에 접근해서 사용하면 된다.

아래에서 Provider를 value라는 값으로 사용하고 있고, value가 바로 생성한 CountAppProvider이다.

ChangeNotifierProvider<CountAppProvider>(
      create: ((context) => CountAppProvider()),
      child: Consumer<CountAppProvider>(builder: (context, value, child) {
        return Scaffold(
          appBar: appBar(title: 'Count App With Provider'),
          body: countScreenPublicUI(
            context: context,
            count: value.count,
            selectCount: value.selectCount,
            onIncrement: () => value.updated(true),
            onDecrement: () => value.updated(false),
            onReset: () => value.updated(null),
            onCount: (int number) => value.selected(number),
          ),
        );
      }),
    );

Provider

Provider를 생성하려면 ChangeNotifier를 상속받아서 사용하여야 하는데, extends / with 둘 다 사용할 수 있지만 기능이 조금 다르다.
둘의 차이에 대해서는 나중에 다루도록 하겠다.

여기서도 count와 selectCount 값을 변수로 생성해주고, 아래 기능을 만들어서 사용하면 된다.

updated()부분을 보면 isAdd라는 불리언 값이 없으면 count 값을 초기화 하고 true면 현재 count 값에서 선택된 selectCount 값을 더해주면 된다.
false일 때는 빼주면 된다.

Get에서는 update()를 사용하였다면 provider에서는 notifyListeners()를 사용하면 되는데 우리가 provider를 처음 생성할 때 ChangeNotifier를 상속 받아왔기에 notifyListeners()로 변경 알림을 요청하여야 호출되는 시점에 상태 변경을 알려준다.

class CountAppProvider extends ChangeNotifier {
  int count = 0;
  int selectCount = 1;

  void selected(int number) {
    HapticFeedback.mediumImpact();
    selectCount = number;
    notifyListeners();
  }

  void updated(bool? isAdd) {
    HapticFeedback.mediumImpact();
    if (isAdd != null) {
      count = isAdd ? count + selectCount : count - selectCount;
      notifyListeners();
    } else {
      count = 0;
      notifyListeners();
    }
  }
}

Result

Git

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

마무리

Provder에 대해서 간단하게 알아보았지만 이전 Get 라이브러리에 대해서 알아봤을 때 처럼 자세히 다루지는 못했다. 해당 글은 상태관리 라이브러리들 간의 간단한 차이점만을 다루는 글이기에 Provider에 대한 자세한 내용에 대한 글도 추후 자세히 작성하여 올리도록 하겠다.

다음 시간에는 flutter의 공식 상태 관리인 Bloc pattern에 대해서 작성하겠다.

profile
Flutter Developer

0개의 댓글