Flutter State Management — 1

extJo
Flutter Seoul
Published in
10 min readAug 3, 2019

Flutter를 통해서 개발을 하다보면, 중요한 요소들이 여러가지가 있지만, 특히 State를 어떻게 관리하는가가 중요한 부분이 될 수 밖에 없다.

이번 글에서는 아래 세개의 패키지에 대해서 알아 볼 것이다.

  • InheritedWidget & Scoped Model
  • Provider

위 세개의 패키지를 통해서 Flutter의 기본 Count App을 변형시켜 볼 것이다.

가장 기본이 되는 코드는 여기서.

InheritedWidget

InheritedWidget은 상태관리를 위한 Flutter Package에 포함 된 기본적인 위젯이다. 여러분들도 Flutter를 통해서 개발을 하다보면 알게 모르게 사용중이다. 특히 Theme Widget, MediaQuery Widget을 통해서 (위젯이름).of() 형태로 상태에 대해서 참조 하곤 한다.

그러면 InheritedWidget을 어떻게 구현해야하는지 알아보자. (전체코드)

class _InheritedCounter extends InheritedWidget {
_InheritedCounter({
Key key,
@required this.counterState,
@required this.child
}) : assert(counterState != null),
super(key: key, child: child);

final _CounterState counterState;
final Widget child;

@override
bool updateShouldNotify(_InheritedCounter old) => counterState.count != old.counterState.count;
}

InheritedWidget을 상속받는 위젯을 위와같이 설계한다.

해당 위젯은 가지고 있는 상태의 변경에 따라서, 상태의 변경에 대한 알림을 하위 위젯들에게 할지 말지 결정하는 위젯이라고 볼 수 있다.

핵심은 상태를 다룰 State object를 가지고, 업데이트에 대한 조건을 잘 설정하는 것이다.

class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);

@override
_CounterState createState() => _CounterState();

static _CounterState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_InheritedCounter)
as _InheritedCounter).counterState;
}
}

그 후에 상태를 다룰 수 있게 접근 가능하게 하는 .of 메소드를 구현 해 준다.

class _CounterState extends State<Counter> {
int count = 0;

void increment() {
setState(() {
count++;
});
}

void decrement() {
setState(() {
count--;
});
}

@override
Widget build(BuildContext context) {
return _InheritedCounter(
child: CounterUI(),
counterState: this,
);
}
}

State object에 대해서 하위 위젯들과 상태를 다루는 state object를 InheritedWidget이 다룰 수 있도록 감싸준다.

class CounterUI extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${Counter.of(context).count}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
_fab(Icon(Icons.exposure_plus_1), Counter.of(context).increment),
_fab(Icon(Icons.exposure_neg_1), Counter.of(context).decrement),
],
),
);
}
}

그 후에 상태를 다루거나, 변경하려고 하는 곳에서 Counter.of(context)형태로 참조 하여 상태를 다룬다.

주의사항

  1. 상태관리에 대해서는 InheritedWidget을 감싼 하위 위젯만 가능하다
  2. .of 메소드가 호출되는경우 .of 메소드가 존재하는 widget들의 build context에 대해서 다시 build한다는 점
  3. 2에 따라서, 만약 무거운 작업을 하는 위젯이 다시 build되는 경우 성능저하가 발생 할 수 있다
  4. 직접 짜야하는 코드의 양이 많아진다 -> 실수할 가능성과 작업속도가 떨어진다

Scoped Model

Scoped Model 패키지InheritedWidget을 이용해서 만든 패키지이며, 사실 Provider가 이미 있는 이상, 사용해야 하는 이유를 찾기가 힘들어 졌다.

사용방법은 pubspec.yaml에서 아래와 같이 작성 해 주면 된다.

dependencies:
...
scoped_model: ^1.0.1

InheritedWidgetProvider 그 중간에 있는 패키지라고 해도 무방하다. 주의해야 할 사항도 그에 따라 비슷하다.

따로 설명은 생략하고 뒤에서 설명할 Provider와 앞에서 설명한 InheritedWidget을 잘 이해했다면 코드를 보는데에 무리가 없을 것이다.

Provider

Provider 패키지는 커뮤니티를 통해서 만들어진 상태관리 패키지이며, 원래는 Google에서 제작한 provider와 커뮤니티에서 제작한 Provider 패키지가 있었지만, 현재는 커뮤니티에서 만든 Provider로 합쳐져서 googler들도 커뮤니티 버전 Provider에서 유지중이다. 그에 따라서 구글도 Provider를 쓰는 걸 더 권장하는 추세.

Provider 패키지는 InheritedWidget의 syntax sugar에 가까우며, 작성해야 하는 코드를 훨씬 간결하게 만들어준다.

사용방법은 pubspec.yaml에서 아래와 같이 작성 해 주면 된다. (전체 코드)

dependencies:
...
provider: ^3.0.0+1

관리하고 싶은 상태에 대해서 모델링을 아래와 같이 해 준다.

class CounterModel with ChangeNotifier {
int _count = 0;
int get count => _count;

void increment() {
_count++;
notifyListeners();
}

void decrement() {
_count--;
notifyListeners();
}
}

ChangeNotifiermixin하여 사용하며, 상태 변경이 끝난 후 notifyListener를 호출 해 준다.

class Counter extends StatelessWidget {
Counter({Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => CounterModel(),
child: CounterUI()
);
}
}

그 후에 변경을 감지 할 수 있도록 ChangeNotifierProvider를 감싸주며, builder부분에서, 우리가 다루려는 상태모델을 작성 해 주며, 상태를 접근 할 수 있는 child를 작성 해 준다.

class CounterUI extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer<CounterModel>(
builder: (context, counter, child) => Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
),
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
_fab(Icon(Icons.exposure_plus_1),
Provider.of<CounterModel>(context).increment),
_fab(Icon(Icons.exposure_neg_1),
Provider.of<CounterModel>(context).decrement),
],
),
);
}
}

그 후에, 상태모델에 대해서 접근하기 위해 여기서 사용한 방법이 두가지가 있다.

  1. Provider.of<Model>(context)
  2. Consumer<Model>(builder : …)

1번에 대해서는 앞에서 보았던 InheritedWidget과 유사하다고 생각하면된다. 2번처럼 사용하는 이유는 앞에서 InheritedWidget에서 보았던 것 처럼, .of 메소드를 호출하게 되면, 상태모델을 참조하는 widget들이 다시 build가 되기때문에, 무거운 작업을 하는 widget이 rebuild되면서, 성능 저하를 일으킬 수 있기 때문이다.

주의사항

  1. .of 메소드가 호출되는경우 .of 메소드가 존재하는 widget들의 build context에 대해서 다시 build한다는 점
  2. 잘못사용하면 성능적인 면에서 InheritedWidget 과 차이가 없다.
  3. Rebuild에 민감한 Widget들은 ld를 시키지 않게 사용.

Provider에 대해서 정말 짧게나마 알아 본 것이며, 더 다양한 기능들을 제공한다. 더 자세한 기능들은 Provider 패키지를 참고하면 좋다.

마무리

결국은 Provider가 주류지만, InheritedWidgetScopedModel을 이해해야 왜 Provider가 사랑받는지에 대해서 이해 할 수 있을 것 이다.

다음 편에서는 Provider에 대해서 더 깊숙히 알아 볼 것이며, BLoC에 대해서 살짝 맛만 볼 것이다.

--

--

extJo
Flutter Seoul

Node js, Typescript, Android, iOS, Kotlin, Dart, Flutter 😍 & Community Organizer