[Flutter] state 관리와 provider

[Flutter] state 관리와 provider

반응형

https://docs.flutter.dev/development/data-and-backend/state-mgmt/declarative

해당 문서는 플러터 공식 문서를 기반으로 작성합니다.

0. 개요

본 문서는 Flutter의 UI의 기본적인 개념과 App State에 대한 정의를 익힌 뒤 State관리를 위한 Provider 사용법에 대해 배울 예정입니다.

1. Declarative UI에 대한 소개

먼저 소개해 드릴 내용은 Declarative 라는 개념입

니다.

플러터는 기본적으로 Declarative style 프레임 워크입니다. 다른 프레임워크(안드로이드나 IOS 같은 경우는 imperative style 이고요

뜻을 보면 다음과 같습니다.

Declarative : 선언적

Imperative : 지시적

가령 아래와 같은 ViewB b를 배경색을 바꾸고 아이템 목록도 바꾼다고 쳐 봅시다.

Imperative에 대한 예시

윈도우나 안드로이드 IOS같은경우는 UI를 작성할때 UI를 관리하는 여러 함수들이 있고 이를 통해 UI를 조작합니다.

아래가 대표적인 Imperative에 스타일의 코드입니다.

// Imperative style b.setColor(red) b.clearChildren() ViewC c3 = new ViewC(...) b.add(c3)

코드를 보시면 아시겠지만 View b에 대응하는 객체를 얻은 뒤에 color를 set 해주고 childeren을 clear 해 주고 ViewC를 일일이 프로그래머의 수작업으로 달아줍니다. 이것이 바로 Imperative 스타일에 대한 예시입니다.

Declarative에 대한 예시

자 그럼 이를 Flutter 방식으로 바꾸면 어떻게 될까요?

// Declarative style return ViewB( color: red, child: ViewC(...), )

Declarative에서는 View의 구성요소들은 immutable 입니다. immutable이니 화면을 바꾸기 위해서는 위젯을 그냥 통째로 rebuild 해 버립니다. 그리고 이를 위해서는 setState를 호출해야 하죠.

매번 rebuild를 하면 속도가 느려지지 않을까 하는 생각이 드실 겁니다. 허나 플러터는 빠릅니다. 그 이유는 다음과 같습니다.

위젯을 새로 rebuild 한다 하였는데 한가지 주목할 점은 위 코드에서 return 하는 위젯은 경량화 된 데이터 입니다.

그리고 플러터에서는 UI를 구성하기위해 RenderObject라는 것을 사용합니다. RenderObject는 화면의 메 프레임마다 존재합니다.

만약 경량화된 데이터가 setState로 인해 변한다면 경량화된 데이터는 RenderObject에게 이를 알리고 UI를 다시 구성하도록 합니다.

리빌드를 하지만 실제로는 경량화된 데이터만 리빌드 되기 때문에 플러터는 높은 퍼포먼스를 유지할 수 있습니다.

그럼 Declarative는 무슨 장점이 있나요?

가령 위젯의 배경화면 색에대한 데이터가 바뀌었다고 칩시다.

그럼 imperative는 다음과 같이 명시적으로 적어 주어야 합니다.

b.setColor(red)

허나 플러터의 경우 바로 rebuild 되기 때문에 이렇게 imperative하게 적어줄 필요가 없습니다.

코드관리가 한결 수월하죠.

2. Ephemeral state vs App state

플러터를 하시면서 state라는 단어를 꾸준히 보실 것 입니다. 이런 state를 관리하는 방법을 익히는건 필수겠지요.

여기서 state라고 다 똑같은 state가 아닙니다. 플러터에서는 Ephemeral state와 App state라는 개념으로 state개념을 명확히 나누고 있습니다.

결론부터 말하면 다음과 같습니다.

Ephemeral State : 다른 위젯에서 필요하지 않고 현재 위젯만이 필요한 state

App State : 여러 위젯들이 접근해야할 데이터, state

Ephemeral State에 대한 예제

PageView에서 현재 Page Index는 보통 PageView 위젯에서만 필요하므로 Ephemeral State입니다.

App Sate에 대한 예제

간단한 예를 들면 어떤 앱에서 notification 정보가 어느 페이지를 가든 보여야 한다고 칩시다.

그럼 notification에 대한 정보는 App Sate가 됩니다.

여기서 한가지 기억할 점은 Ephemeral Sate와 Aps State는 명확한 기준이 없습니다.

PageView의 index가 다른 위젯에서 필요하다면 이는 App State가 될 수도 있는 것 입니다.

아래 다이어그램을 참고하면 이해가 쉽습니다.

3. State관리의 필요성, 문제상황 인지

Ephemeral State와 App state에 대한 개념을 배웠습니다. 이제 우리가 주목할 state는 App state라고 감이 잡히시겠지요?

아래와 같은 위젯 트리가 있다고 칩시다.

Root 노드에 어떤 State를 가지고 있다고 가정합니다.

가장 아래 주황색 Widget에서 이 State가 필요합니다.

Root 노드에서는 이 주황색 위젯에 State를 전달하기 위해 생성자를 통해 모든 위젯에 State를 내려주었습니다.

만약 이상태에서 State가 변하면?

State가 필요한건 주황색 위젯이므로 주황색 위젯만 rebuild 해 주면 되는데 모든 위젯들이 rebuild 되어 버립니다.

퍼포먼스에 문제가 생기겠지요?

이 문제를 해결하기 위해서 플러터는 주황색 위젯에서 바로 root 위젯으로 접근 할 수 있는 InheritedWidget이라는걸 제공하는데 앞으로 배울 Provider가 더 좋은거니깐 이 글에선 생략하도록 하겠습니다.

4. State는 어디에 저장해야 하는가?

먼저 다음과 같은 쇼핑앱이 있다고 칩시다.

위 위젯은 다음과 같이 구분됩니다.

MyCatalog위젯과 MyCart 위젯 페이지가 있습니다.

MyCatalog에는 MyAppBar와 쇼핑목록인 MyListItem이 있습니다

MyCatalog에서 아이템들을 선택하면 MyCart에서 선택한 아이템들과 함께 total price가 표시됩니다.

여기서 주목할 점이 있지요? 우리는 state관리에 대해 배울겁니다. 앱의 영상을 보시면 아이템 목록을 add 하면 MyCart에 데이터가 저장됩니다.

그럼 이러한 State는 어느 위젯에 저장해야 할 까요?

Lifting state up

정답은 바로 / state를 사용하는 위젯의 / 위쪽에 있는 위젯에 / state를 저장해야 합니다.

이렇게 하면 우리는 MyCart를 리빌드 할 때 MyApp에서 그냥 state만 전달해주면 끝나기 때문입니다.

만약 state를 MyApp이 아닌 다른곳에 저장한다면? 가령 예를 들어 MyListItem에 저장한다면 MyCart에 데이터를 전달해 주기 위해 길을 뚫어주고 인터페이스를 만드는 작업들을 해야 겠죠. 상당히 불편합니다.

5. 부모의 State에 접근하기, Provider

자 여러분은 이제 state를 어디에 저장해야되는지 알게되었습니다. 그럼 자식위젯이 부모의 State에 어떻게 접근할까요?

콜백을 사용하여 MyListItem에 전달하고 MyListItem에서 state가 바뀌면 MyApp에서 호출되도록 할까요?

그럼 콜백을 계속 생성자에 내려줘야 하기에 쉽지 않습니다. 좋은방법은 아닙니다.

Flutter는 이를 해결하기 위해 InheritedWidget, InheritedNotifier, InheritedModel 같은 위젯을 제공합니다.

허나 지금 이 내용을 소개하는 구글페이지에서는 수준낮은 위젯이고 이 페이지에서 사용하지 않을거라 합니다.

(그럼 왜만들었냐?)

대신 우리는 이를 해결하기 위해 provider 라는 패키지를 사용할 것입니다.

아래처럼 pubspec.yaml 에 프로바이더를 설치 해 주세요.

name: my_name description: Blah blah blah. # ... dependencies: flutter: sdk: flutter provider: ^6.0.0 dev_dependencies: # ...

그리고

import 'package:provider/provider.dart';

를 하시면 provider 사용 준비는 끝났습니다.

6. Provider의 세가지 필수 개념

Provider를 사용하기 위해선 다음 세가지 개념을 필수로 이해해야 합니다.

ChangeNotifier

ChangeNotifierProvider

Consumer

7. ChangeNotifier

ChangeNotifier는 이름에서 보면 알 수 있듯이 state가 변화면 Listner에게 state변화를 알리는 클래스 입니다.

사용방법은 다음과 같습니다.

원하는 클래스에 ChangeNotifier를 상속합니다.

값이 변하면 notifyListeners() 함수를 호출합니다.

class CartModel extends ChangeNotifier { /// 내부적으로 사용될 Cart의 목록 final List _items = []; /// An unmodifiable view of the items in the cart. UnmodifiableListView get items => UnmodifiableListView(_items); /// 현재 아이템들의 모든 가격 int get totalPrice => _items.length * 42; /// 아이템이 추가되면 리스너에게 아이템이 추가됨을 알린다. void add(Item item) { _items.add(item); // 해당 ChangeNorifiter를 감시하고있는 위젯들에게 // 상태변화를 알리고 rebuild 하도록 한다. notifyListeners(); } /// 아이템 목록을 클리어 하고 리스너에게 이를 알린다. void removeAll() { _items.clear(); notifyListeners(); } }

여기서 notifyListenser() 함수는 ChangeNotifier를 감시하고 있는 리스너에게 state변화를 알리고 위젯을 리빌드 하도록 하는 함수 입니다.

8. ChangeNotifierProvider

ChageNotifierProvider는 위젯입니다. 그리고 이 위젯은 자식위젯에게 ChangeNotifier을 전달 해 줍니다.

그리고 위 예제에서는 MyApp 위젯에 ChangeNotifierProvider를 제공해 줘야겠죠?

void main() { runApp( ChangeNotifierProvider( create: (context) => CartModel(), child: const MyApp(), ), ); }

만약 여러개의 ChangeNotifierProvider를 제공해주고 싶다면 다음과 같이 MultiProvider를 사용합니다.

void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => CartModel()), Provider(create: (context) => SomeOtherClass()), ], child: const MyApp(), ), ); }

9. Consumer

자 우리는 이제 위젯 최상단인 MyApp 위젯에 ChangeNotifier인 CartModel을 ChangeNotifierProvider로 제공하였습니다.

그럼 이제 이걸 자식 위젯에서 사용해 봅시다! 바로 Consumer를 이용하는 겁니다.

return Consumer( builder: (context, cart, child) { return Text("Total price: ${cart.totalPrice}"); }, );

여기서 주의할점은 Consumer를 사용할때 제네릭 타입을 반드시 지정해줘야 한다는 점입니다.

위 예제에서는 Consumer 이 되겠지요?

Consumer에서는 builder 인자를 요구합니다. 이 Builder는 ChangeNotifier에서 notifyListeners() 함수를 호출할 때 불리게 됩니다.

이렇게 하면 MyCart 위젯에서 값이 변할 때 마다 totalPrice 값이 바뀌게 되겠지요?

여기서 builder 함수는 세가지 인자를 요구합니다

첫번째 인자는 context 입니다. 거의 모든 위젯에서 필요하니 넘어갑시다

두번째로요구하는 인자는 ChangeNotifier의 instance입니다. 여기서는 CartModel의 인스턴스가 되겠지요

여기서는 CartModel의 인스턴스가 되겠지요 세번째 인자는 child 입니다. 퍼포먼스 최적화를 위해 존재합니다. child위젯의 subtree가 너무 클 경우, 그리고 ChangeNotifier가 바뀌어도 영향이 없는 경우 해당 child를 활용합시다.

아래가 대표적인 child활용의 예시입니다.

return Consumer( builder: (context, cart, child) => Stack( children: [ // 이렇게 하면 child를 매번 리빌드 할 필요가 없습니다. if (child != null) child, Text("Total price: ${cart.totalPrice}"), ], ), // 이곳에서 child를 한번 생성해 줍니다. child: const SomeExpensiveWidget(), );

그리고 Consumer는 가능하면 위젯트리의 가장 아래에 두어야 합니다.

그래야 불필요한 rebuild를 하지 않으니 깐요.

다음과 같이 사용하지 마세요

// DON'T DO THIS return Consumer( builder: (context, cart, child) { return HumongousWidget( // ... child: AnotherMonstrousWidget( // ... child: Text('Total price: ${cart.totalPrice}'), ), ); }, );

아래처럼 Consumer를 가장 아래에 두어야 올바른 예시입니다.

// DO THIS return HumongousWidget( // ... child: AnotherMonstrousWidget( // ... child: Consumer( builder: (context, cart, child) { return Text('Total price: ${cart.totalPrice}'); }, ), ), );

10. Provider.of

간혹가다가 그저 부모의 데이터만 조작하고 rebuild를 원하지 않는 경우가 있습니다.

그럴경우에 Provider.of 함수에 listen값을 false로 주면 됩니다.

Provider.of(context, listen: false).removeAll();

반응형

from http://lucky516.tistory.com/121 by ccl(A) rewrite - 2021-12-14 17:26:28