State management
state 관리의 의의
setState 메소드
setState를 거치는 함수를 통해 State 객체의 내부 상태를 변경하여 하위 위젯 트리에 영향을 미치도록 한다. 플러터에서 변경 알림은 콜백을 통해 위젯 계층 구조를 따라 위로 흐르고, 현재 상태는 아래로 화면을 그리는 Stateless 위젯까지 이동한다. setState를 호출하여 프레임워크에 이 객체의 내부 상태가 하위 위젯 UI에 영향을 줄 수 있도록 변경되었음을 알리며, 이에 따라 프레임워크는 이 State 객체에 대한 빌드를 수행한다.
상태 변경을 위해 setState를 사용하면 아래와 같은 문제가 발생한다.
1. 비효율성: 한 위젯의 상태를 변경하기 위해 모든 하위 위젯을 전부 리빌드해주어야 한다.
2. 비동시성: 한 위젯의 state를 변경하면 다른 위젯이 이를 알 수 없다. 동시에 여러 위젯을 업데이트 할 수 없다.
state management란
위젯이 쉽게 데이터에 접근할 수 있는 방법과 변화된 데이터에 맞게 UI를 다시 그려주는 기능을 제공해야 한다.
플러터 공식문서: simple app state management
상태 거슬러 올라가기
위와 같은 위젯 트리 구조 상의 앱에서 MyCart에 담긴 상품 개수(UI)를 변경하려면 상위 위젯에서 상태를 내려주어야 한다. 외부에서 MyCart 위젯 메소드를 호출하여 위젯을 변경해서는 안된다.
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
//요렇게 cart 위젯을 직접 업데이트하면 안되고, 이렇게 하더라도 상위 위젯인 MyApp을 업데이트 해야하는 문제가 남는다.
MyCart를 직접 업데이트 하는 메소드를 호출하는 대신, MyCart(contents) (생성자)를 사용한다. 플러터는 위젯의 contents가 변경될때마다 위젯을 새로 빌드한다. 상위 위젯의 빌드 메소드에서만 새로운 위젯을 생성할 수 있으므로, contents를 변경하기 위해 contents는 MyCart의 상위 위젯에 위치해야 한다.
// GOOD
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
// GOOD
Widget build(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
return SomeWidget(
// Just construct the UI once, using the current state of the cart.
// ···
);
}
//상위 위젯에서 업데이트하여 새로 빌드한 상태를 MyCart에서 리턴해주는듯?
MyApp에 위치한 contents가 변경될 때마다 위에서부터 MyCart를 다시 빌드한다. 빌드될 때마다 이전의 MyCart 위젯은 사라지고 새로운 위젯으로 대체된다. 이것이 MyCart의 라이프사이클에 대해 고려할 필요가 없는 선언적 방식이다.
상태에 접근하기
사용자가 카탈로그 아이템 중 하나를 클릭하면 해당 항목이 장바구니에 추가되어야 한다고 하자. 위 위젯 구조에서 장바구니는 MyListItem보다 상위에 위치하는데 어떻게 해야 할까?
간단한 옵션은 MyListItem이 클릭될 때 호출할 수 있는 콜백을 제공하는 것이다. Dart의 함수는 일급 객체이므로 원하는 대로 이를 전달 가능하다.
@override
Widget build(BuildContext context) {
return SomeWidget(
// 위젯을 만들고 위의 메소드에 참조로 전달한다.
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}
이는 괜찮은 방법이지만 여러 곳에서 변경해야 하는 앱 상태의 경우, 콜백을 많이 전달해야해서 번거로워질 수 있다. 이 경우 모든 하위 위젯에 전역적으로 상태를 전달하는 라이브러리 provider를 사용할 수 있다.
Provider
Provider를 사용하기 위해 패키지 설치가 필요하다(설치하기). 프로바이더도 하나의 위젯이며, 데이터를 필요로 하는 위젯보다 항상 위에 위치해야 한다.
Provider를 사용하기 위해 아래 3가지 개념을 이해해야 한다.
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
Provider 기본 생성자를 이용해 새로 생성된 객체를 노출한다. Create method로 전달하려는 데이터가 있는 클래스를 리턴하여 프로바이더의 차일드가 된 모든 위젯에서 클래스 인스턴스에 접근 가능하다.
Provider(
create: (_) => MyModel(),
child: ...
)
Provider.of를 사용하면 거슬러올라가 가장 가까운 위젯 트리에서 원하는 타입의 인스턴스(Provider<T>)를 반환한다.
Provider(
create: (context) {
return Model(Provider.of<Something>(context, listen: false)),
},
)
ChangeNotifier 클래스
ChangeNotifier는 클래스 내 데이터가 변했을때, 관련된 위젯에 변경사항을 알려주고 UI를 rebuild하기 위해 사용한다(Observable의 한 형태).
Provider에서 ChangeNotifier는 애플리케이션 상태를 캡슐화하는 한 가지 방법이다. 매우 간단한 앱의 경우 단일 ChangeNotifier로 충분하나, 복잡한 앱의 경우 여러 ChangeNotifier가 필요하다.
쇼핑 앱 예제에서 장바구니의 상태를 ChangeNotifier를 통해 관리하는 방법을 살펴보자.
class CartModel extends ChangeNotifier {
/// ChangeNotifier를 상속받아 카트의 내부적인 상태를 만든다.
final List<Item> _items = [];
/// 카트 내 아이템에 대한 변경 불가능한 화면
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// 모든 아이템이 42달러라고 가정했을시 전체 아이템 가격 합계
int get totalPrice => _items.length * 42;
///아이템을 카트에 추가하기
void add(Item item) {
_items.add(item);
// 이 모델을 Listen하는 위젯이 리빌드하도록 한다.
notifyListeners();
}
/// 전체 아이템 카트에서 제거하기
void removeAll() {
_items.clear();
// 이 모델을 Listen하는 위젯이 리빌드하도록 한다.
notifyListeners();
}
}
ChangeNotifier와 관련된 유일한 코드는 notifyListeners()를 호출하는 부분으로, 이 메서드는 모델이 앱의 UI를 변경해야 할 때마다 이를 Listen하는 위젯에 상태가 변경되었음을 알린다. CartModel의 나머지 부분은 모델 자체와 해당 비즈니스 로직이다.
addListener 메서드로 업데이트하려는 위젯에서 콜백 메서드를 등록해준다. addListener를 등록해주어야 데이터 변경 사항을 전달할 수 있다. addListener는 자동으로 제거되지 않아, removeListner로 필요없는 addListner를 dispose 시켜주어야 한다. addListener로는 데이터가 변경되었다는 것을 알려줄 수 있으나, UI를 자동으로 업데이트 할 수 없다.
ChangeNotifier의 단점
1. addListner와 removeListener로 등록과 제거를 수동으로 해야함
2. 생성자를 통해 하위 위젯에 인스턴스를 매번 전달해야함
3. UI 리빌드도 수동으로 해야함
이러한 단점을 극복하기 위해 ChangeNotifierProvider를 사용한다.
ChangeNotifierProvider
장점
1. 모든 위젯들이 listen할 수 있는 ChangeNotifier 인스턴스 생성
2. 자동으로 불필요한 ChangeNotifier 제거
3. Provider.of를 통해 위젯들이 쉽게 ChangeNotifier 인스턴스에 접근할 수 있게 해줌
4. 필요시 UI 리빌드 가능
5. 굳이 UI를 리빌드 할 필요가 없는 위젯을 위해 listen: false 기능 제공
상태를 전달하려는 위젯 상위에 ChangeNotifierProvider를 위치시킨다. 쇼핑 앱 예제에서는 MyApp에 추가해야 한다.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
위와 같이 CartModel의 새 인스턴스를 만드는 빌더를 정의하였다. ChangeNotifierProvider는 CartModel을 꼭 다시 빌드해야 할 때만 다시 빌드하도록 한다. 또한 해당 인스턴스가 더 이상 필요하지 않을 때 CartModel의 dispose()를 자동으로 호출한다.
Provider.of
가끔 UI를 변경하기 위해 모델의 데이터가 실제로 필요하지 않지만 여전히 해당 데이터에 접근해야 할 수 있다. 예를 들어 ClearCart 버튼으로 사용자가 장바구니에서 모든 항목을 제거할 수 있도록 허용하려고 한다고 하자. 이 버튼은 장바구니의 내용을 표시할 필요가 없으며, 단순히 clear() 메서드를 호출하면 된다.
이 경우 Consumer<CartModel>을 사용할 수 있지만 이는 프레임워크에게 다시 빌드할 필요가 없는 위젯을 다시 빌드하도록 요청하게 되어 비효율적이다. 이 경우 listen 매개변수를 false로 설정하여 Provider.of를 사용할 수 있다.
Provider.of<CartModel>(context, listen: false).removeAll();
이렇게 하여 notifyListeners가 호출될 때 이 위젯을 다시 빌드하지 않게 된다.
'앱 개발 공부' 카테고리의 다른 글
플러터 학습기(7)-옵션 추가할 수 있는 드롭다운 만들기 (0) | 2024.02.18 |
---|---|
플러터 학습기 (6) - 바텀 내비게이션 바 커스텀하기, theme 설정하기 (0) | 2024.02.10 |
플러터 기본 학습 - 상태 관리 (1) state, Stateful widget (1) | 2024.01.12 |
플러터 학습기 (5) - 핫 리로드, Element tree, Render tree (1) | 2024.01.07 |
Generics, Collections (0) | 2024.01.06 |
댓글