[Flutter] 상태 관리 2편(State Management) - Value Listenable

Tyger·2023년 1월 9일
3

State Management

목록 보기
2/14
post-thumbnail

상태 관리 2편(State Management) - Value Listenable

상태 관리(State Management) 1편 - State Ful
상태 관리(State Management) 3편 - Get X [Simple]
상태 관리(State Management) 4편 - Get X [Reactive]
상태 관리(State Management) 5편 - Provider
상태 관리(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)

Dart Packages

이번 글에서는 지난 글에 이어서 상태 관리 2편으로 Value Listenable 이라는 위젯의 기능을 활용해서 상태 관리를 할 수 있는 방법에 대해서 작성하려고 한다.

상태 관리에 대한 설명은 1편에서 작성을 했기에 여기서는 설명하지는 않겠다.

상태 관리 라이브러리를 사용하지 않으면서 StatefulWidget을 사용하지 않고 상태를 변경할 수 있는 방법이 없을까 고민이 될 때 적당하게 사용할 수 있는 기능이 바로 ValueListenableBuilder를 활용한 상태 변경 방법이다.

지금까지 Flutter로 개발을 진행하면서 모르고 있던 위젯인데, 사용할 기회가 생겨 사용해보니 엄청 유용한 기능이어서 간단한 구조의 상태를 관리할 때 자주 사용하고 있다.

Count App

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

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

Value Listenable

Value Listenable은 ValueNotifier로 선언된 변수를 ValueListenableBuilder 안에서 상태 변경 알림을 보내 ValueNotifier의 변수 값을 변경할 수 있는 기능이다.
StatelessWidget을 사용하여 StatefulWidget이 가지고 있는 setState 없이도 상태를 변경해 줄 수 있게 된다.

UI

앞으로 사용하는 UI 구조와 조금 다르게 구성하였다. 아무래도 ValueListenableBuilder 안에 UI 구조를 넣어야 해서 여기 글에서만 UI 코드를 다르게 작성하였다.

물론 기능은 동일하다.

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

Scaffold(
      appBar: appBar(title: 'Count App With Listenerable'),
      body: Stack(
        children: [
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                  width: MediaQuery.of(context).size.width,
                  child: Center(
                    child: ValueListenableBuilder<int>(
                        valueListenable: _count,
                        builder: (
                          BuildContext context,
                          int count,
                          Widget? child,
                        ) {
                          return Text(
                            "$count",
                            style: const TextStyle(
                                fontSize: 60, fontWeight: FontWeight.bold),
                          );
                        }),
                  )),
              const SizedBox(height: 24),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  GestureDetector(
                    onTap: () {
                      HapticFeedback.mediumImpact();
                      _count.value = _count.value + _selectCount.value;
                    },
                    child: const Icon(
                      Icons.add_circle_outline,
                      size: 40,
                    ),
                  ),
                  const SizedBox(width: 24),
                  GestureDetector(
                    onTap: () {
                      HapticFeedback.mediumImpact();
                      _count.value = _count.value - _selectCount.value;
                    },
                    child: const Icon(
                      Icons.remove_circle_outline,
                      size: 40,
                    ),
                  )
                ],
              ),
              const SizedBox(height: 24),
              GestureDetector(
                onTap: () {
                  HapticFeedback.mediumImpact();
                  _count.value = 0;
                },
                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: ValueListenableBuilder<int>(
                    valueListenable: _selectCount,
                    builder: (
                      BuildContext context,
                      int number,
                      Widget? child,
                    ) {
                      return Column(
                        children: [
                          countAppSelectedCountBox(
                              onTap: (number) {
                                HapticFeedback.mediumImpact();
                                _selectCount.value = number;
                              },
                              selectNumber: _selectCount.value,
                              number: 1),
                          countAppSelectedCountBox(
                              onTap: (number) {
                                HapticFeedback.mediumImpact();
                                _selectCount.value = number;
                              },
                              selectNumber: _selectCount.value,
                              number: 10),
                          countAppSelectedCountBox(
                              onTap: (number) {
                                HapticFeedback.mediumImpact();
                                _selectCount.value = number;
                              },
                              selectNumber: _selectCount.value,
                              number: 20),
                          countAppSelectedCountBox(
                              onTap: (number) {
                                HapticFeedback.mediumImpact();
                                _selectCount.value = number;
                              },
                              selectNumber: _selectCount.value,
                              number: 50),
                          countAppSelectedCountBox(
                              onTap: (number) {
                                HapticFeedback.mediumImpact();
                                _selectCount.value = number;
                              },
                              selectNumber: _selectCount.value,
                              number: 100),
                        ],
                      );
                    }),
              ),
            ),
          ),
        ],
      ),
    );

Value Notifier

StatelessWidget의 build아래에 ValueNotifier<타입>로 선언한 변수를 선언해보자.
여기서는 카운트 값을 보여줄 count 변수를 0으로 초기화해서 선언하고 얼마를 증감시킬지 선택하는 selectCount 변수를 초기 값 1로 선언하였다.

 	ValueNotifier<int> _count = ValueNotifier<int>(0);
    ValueNotifier<int> _selectCount = ValueNotifier<int>(1);

위에서 사용할 값을 소비할 위젯을 ValueListenableBuilder<타입>으로 만들어 준뒤 BuildContext, int, Widget?의 값을 받아올 수 있도록 해준다.

이렇게 되면 ValueNotifier로 선언된 변수를 변경시킬 때 ValueListenableBuilder안에 있는 위젯의 해당되는 변수 값의 변경을 빌드하게 된다.

ValueNotifier의 변수 타입으로 선언된 변수는 선언된 타입의 변수가 아니기에 .value 형태로 값을 꺼내서 사용하여야 한다.

 ValueListenableBuilder<int>(
        valueListenable: _count,
		builder: (
		BuildContext context,
		int count,
		Widget? child,
		) {
			return Text(
				"$count",
				style: const TextStyle(
					fontSize: 60, fontWeight: FontWeight.bold),
					);
			}),

increment

값을 증가시킬 때 현재 count 값에 선택되어진 selectCount 값을 더해준다.

onTap:(){
 _count.value = _count.value + _selectCount.value;
},

decrement

값을 감소시킬 때도 현재 count 값에 선택되어진 selectCount 값을 빼준다.

onTap:(){
 _count.value = _count.value - _selectCount.value;
},

reset

리셋인 경우에는 그냥 0으로 만들어 주면 된다.

onTap:(){
 _count.value = 0;
},

select

selectCount 값을 변경 시킬 때 숫자 박스에서 받아오는 number 변수를 selectCount 값으로 변경해주면 된다.

onTap:(int number){
_selectCount.value = number;
},

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/blob/main/lib/count_app/count_screen_with_listenerable.dart

마무리

Value Listenable은 간단하게 상태 관리를 하는 경우에는 편하게 사용할 수 있지만 관리해야할 state가 많아지면 구조가 매우 복잡해지고 UI와 로직을 분리하기가 쉽지 않아 간단한 상태 관리에만 사용하는게 좋을 것 같다.

다음 글부터 본격적인 상태 관리 라이브러리를 활용한 방법에 대해서 작성을 하도록 하겠다.

profile
Flutter Developer

0개의 댓글