Flutter State Management — 1
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)
형태로 참조 하여 상태를 다룬다.
주의사항
- 상태관리에 대해서는
InheritedWidget
을 감싼 하위 위젯만 가능하다 .of
메소드가 호출되는경우.of
메소드가 존재하는 widget들의 build context에 대해서 다시 build한다는 점- 2에 따라서, 만약 무거운 작업을 하는 위젯이 다시 build되는 경우 성능저하가 발생 할 수 있다
- 직접 짜야하는 코드의 양이 많아진다 -> 실수할 가능성과 작업속도가 떨어진다
Scoped Model
Scoped Model 패키지는 InheritedWidget
을 이용해서 만든 패키지이며, 사실 Provider
가 이미 있는 이상, 사용해야 하는 이유를 찾기가 힘들어 졌다.
사용방법은 pubspec.yaml
에서 아래와 같이 작성 해 주면 된다.
dependencies:
...
scoped_model: ^1.0.1
InheritedWidget
과 Provider
그 중간에 있는 패키지라고 해도 무방하다. 주의해야 할 사항도 그에 따라 비슷하다.
따로 설명은 생략하고 뒤에서 설명할 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();
}
}
ChangeNotifier
와 mixin하여 사용하며, 상태 변경이 끝난 후 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),
],
),
);
}
}
그 후에, 상태모델에 대해서 접근하기 위해 여기서 사용한 방법이 두가지가 있다.
Provider.of<Model>(context)
Consumer<Model>(builder : …)
1번에 대해서는 앞에서 보았던 InheritedWidget
과 유사하다고 생각하면된다. 2번처럼 사용하는 이유는 앞에서 InheritedWidget
에서 보았던 것 처럼, .of
메소드를 호출하게 되면, 상태모델을 참조하는 widget들이 다시 build가 되기때문에, 무거운 작업을 하는 widget이 rebuild되면서, 성능 저하를 일으킬 수 있기 때문이다.
주의사항
.of
메소드가 호출되는경우.of
메소드가 존재하는 widget들의 build context에 대해서 다시 build한다는 점- 잘못사용하면 성능적인 면에서
InheritedWidget
과 차이가 없다. - Rebuild에 민감한 Widget들은 ld를 시키지 않게 사용.
Provider에 대해서 정말 짧게나마 알아 본 것이며, 더 다양한 기능들을 제공한다. 더 자세한 기능들은 Provider 패키지를 참고하면 좋다.
마무리
결국은 Provider
가 주류지만, InheritedWidget
과 ScopedModel
을 이해해야 왜 Provider
가 사랑받는지에 대해서 이해 할 수 있을 것 이다.
다음 편에서는 Provider
에 대해서 더 깊숙히 알아 볼 것이며, BLoC에 대해서 살짝 맛만 볼 것이다.