출처
[Flutter Festival GDG Songdo] GetX, provider, bloc 패턴 비교 분석 - 유병욱
count state를 Stateful Widget 내에 선언
// stateful widget
class CountPage extends StatefulWidget {
const CountPage({Key? key}) : super(key: key);
@override
_CountPageState createState() => _CountPageState();
}
// state of the widget
class _CountPageState extends State<CountPage> {
int _count = 0; // state 선언
@override
Widget build(BuildContext context) {
...
counter의 +, - 버튼에 setState로 state 업데이트
children: [
FloatingActionButton(
onPressed: () => setState(() {
_count++;
}),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => setState(() {
_count--;
}),
child: const Icon(Icons.remove),
),
],
setState 코드
@protected
void setState(VoidCallback fn) {
...
_element!.markNeedsBuild();
}
markNeedsBuild 메서드는 flutter에 다음 프레임에 UI를 re-render하도록 알린다.
markNeedsBuild 메서드를 보자.
/// Marks the element as dirty and adds it to the global list of widgets to
/// rebuild in the next frame.
///
/// Since it is inefficient to build an element twice in one frame,
/// applications and widgets should be structured so as to only mark
/// widgets dirty during event handlers before the frame begins, not during
/// the build itself.
void markNeedsBuild() {
...
if (dirty)
return;
_dirty = true;
owner!.scheduleBuildFor(this);
}
setState()를 이용하여
1. state를 업데이트 하고,
2. flutter에게 UI를 re-render하도록 알림언제쓸까?
- 특정한 Widget 내에서 단기적으로 쓰이고 말 때, 사용하면 편한 상태관리
(한 파일 내에서 쓰기에 편한 상태관리 방식)
Stream을 Flutter Project 내에 적극적으로 사용해야한다면 사용해야하는 라이브러리!
Cubit은 function으로 state를 관리 할 수 있게해주는 class
Cubit 생성
import 'package:flutter_bloc/flutter_bloc.dart';
class CountCubit extends Cubit<int> {
CountCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
Cubit 사용
class CountPage extends StatelessWidget {
const CountPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CountCubit(),
child: const CountView(),
);
}
}
BlocProvider
BlocBuilder
class CountView extends StatelessWidget {
const CountView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('State Manager - BloC'),
),
body: Center(
child: BlocBuilder<CountCubit, int>(
builder: (BuildContext context, int state) {
return Text('$state', style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold));
},
),
),
...
}
context.read
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CountCubit>().increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => context.read<CountCubit>().decrement(),
child: const Icon(Icons.remove),
),
],
),
BLoc 사용
추후 flutter로 로그인 기능 구현에서...
Cubit
- state 변경을 function으로 관리
- 변경 된 값을 가져올 때, emit / onChanged 등으로 개발자가 시점을 특정할 수 있음
- 단순 UI 핸들링에 효과적
Bloc
- state 변경을 event로 관리
- 여러 요인으로 인한 잦은 변경이 많은 관리
- 주체에 대해 event로 관리되기에 변화에 대한 추적 가능
- 로그인 유무 / 특정 Action에 대한 추적을 Stream으로 관리하기에 효과적
언제쓸까?
- Stream을 적극적으로 활용해야될 때 (event기반의 Bloc, function기반의 Cubit)
- View와 비즈니스Logic에 대한 분명한 분리
state 관리 그 자체만을 위하여...
관리할 state model 생성
class CountModel extends ChangeNotifier {
int counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
void decrementCount() {
counter--;
notifyListeners();
}
}
ChangeNotifier class
Provider 사용하기
class CountApp extends StatelessWidget {
const CountApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'State Manager - Provider',
home: MultiProvider(
providers: [
ChangeNotifierProvider<CountModel>(
create: (_) => CountModel(),
child: const CountPage(),
),
],
),
);
}
}
Root Widget에서 provider 사용 (하위(자식) Widget에서 state관리 가능)
ChangeNotifierProvider
Consumer와 Provider.of(context)
CountModel이 ChangeNotifierProvider에 의해 하위 Widget들에게 제공되는 상황에서 이를 사용하기 위한 두 가지의 방식이 있다.
@override
Widget build(BuildContext context) {
return Consumer<CountModel>(
builder: (context, model, child) {
return Center(
child: Text(
'${model.counter}',
style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final countModel = Provider.of<CountModel>(context);
return Center(
child: Text(
'${countModel.counter}',
style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
),
);
}
return Consumer<CountModel>(
builder: (context, model, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
if (child != null) child,
Text('Counter: ${model.counter}'),
],
),
// Build the expensive widget here.
child: const SomeExpensiveWidget(),
);
builder 함수의 child argument에 할당한 SomeExpensiveWidget()은 CountModel의 state가 바뀌어도 rebuild하지 않는다.
위와 같은 경우는 큰 Widget의 부모로 Consumer Widget이 필수적으로 있어야 하는 경우 사용할 수 있는 방법이지만, 굳이 부모에 있을 필요가 없다면 최대한 하위 Widget으로 Consumer를 배치하는 것이 성능적으로 유리하다. flutter docs
// DON'T DO THIS
return Consumer<CountModel>(
builder: (context, model, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Counter: ${model.counter}'),
),
);
},
);
// DO THIS
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CountModel>(
builder: (context, model, child) {
return Text('Counter: ${model.counter}');
},
),
),
);
Consumer와 Provider.of(context) 둘 다 함수를 이용하여 state 변경 가능
return Scaffold(
appBar: AppBar(
title: const Text('State Manager - Provider'),
),
body: const CountView(),
floatingActionButton: Consumer<CountModel>(
builder: (context, model, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => model.incrementCounter(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => model.decrementCount(),
child: const Icon(Icons.remove),
),
],
);
},
),
);
final countModel = Provider.of<CountModel>(context);
return Scaffold(
appBar: AppBar(
title: const Text('State Manager - Provider'),
),
body: const CountView(),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => countModel.incrementCounter(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => countModel.decrementCount(),
child: const Icon(Icons.remove),
),
],
),
);
Provider
- InheritedWidget을 좀 더 쓰기 편한 형태로 발전시킨 상태관리 모델
- Flutter에서 적극 추천하는 상태관리 모델
- Provider는 Flutter 기반을 최대한 유지 및 활용하는 형태로 구현 가능 (Provider 객체를 가지고 올 때 Flutter 기존의 방식인 BuildContext 객체를 활용 할 정도)
- MultiProvider 형태로 동시에 여러 Provider 객체를 운용 가능
- ProxyProvider로 여러 Provider 값을 한데 모아 활용 가능
언제쓸까?
- 단순 전역적인 상태관리가 필요할 때 사용하기 좋을 것 같다
Flutter 안에서 쓸 수있는 새로운 프레임워크 같은 라이브러리...
Flutter를 쓰면서 다소 귀찮아질 수 있는 BuildContext와 StatefulWidget의 State객체의 존재를 지우고 많은 것을 GetX로 쓸 수 있다. (Navigator부터 Connection까지...)
Flutter가 어떤 구조로 돌아가는지 생각하게 해주지 않고 GetX 기능을 쓰면 한방에 해결되기에 개발 공부에 필요한 생각을 하지 않게된다는 말도 나온다...
추후에...