안녕하세요.
이번 포스팅에서는 Flutter app 개발 시 상태 관리를 위한 디자인 패턴 중 하나인 Bloc pattern에 대해 알아보겠습니다.
언어: dart
IDE: Android Studio
Framework: Flutter
Test device: Android
Bloc(Business Logic Component) pattern은 Flutter app에서 널리 사용되는 상태 관리 솔루션입니다. 이 패턴은 비즈니스 로직을 UI와 분리하여 명확한 역할 분리를 이루고, 앱의 테스트 및 유지보수를 좀 더 쉽게 할 수 있게 도와줍니다.
핵심 개념
- Event (이벤트):
Event는 Bloc의 입력입니다. 버튼 클릭, API 요청 등 사용자 상호작용을 나타냅니다. - State (상태):
State는 Bloc의 출력입니다. 로딩 중, 데이터 로드 완료, 에러 상태 등 UI의 현재 상태를 나타냅니다. - Bloc:
Bloc은 Event를 입력으로 받아 비즈니스 로직에 따라 State를 출력하는 구성 요소입니다. 별도의 클래스로 구현할 수 있습니다. - Stream (스트림):
Bloc은 Dart의 async 패키지에 포함된 스트림을 사용하여 Event와 State를 처리합니다. Event는 입력 스트림에 추가되고 State는 출력 스트림을 통해 전달됩니다.
Bloc의 동작
- 사용자가 UI와 상호작용(버튼 클릭 등)하면 Event가 트리거 됩니다.
- 해당 Event가 Bloc에 추가됩니다.
- Bloc이 Event를 처리하고 새로운 State를 출력합니다.
- UI는 새로운 State에 따라 다시 빌드됩니다.
이번 포스팅에서는 이전에 작성했던 state hoisting 포스팅의 소스를 기반으로 예제를 만들어 보겠습니다.
https://it-of-fortune.tistory.com/48
우선, 기본 상태의 소스입니다. 현재는 state hoisting을 사용하여 상태를 관리하고 있습니다.
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: LoadingExample(),
));
}
class LoadingExample extends StatefulWidget {
@override
_LoadingExampleState createState() => _LoadingExampleState();
}
class _LoadingExampleState extends State<LoadingExample> {
bool _showMessage = false;
bool _isLoading = false;
void _setMessageVisibility() async {
setState(() {
_isLoading = true;
});
await Future.delayed(Duration(seconds: 3));
setState(() {
_isLoading = false;
_showMessage = !_showMessage;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('State Hoisting Example')),
body: childWidget(
showMessage: _showMessage,
isLoading: _isLoading,
onButtonPressed: _setMessageVisibility,
),
);
}
}
class childWidget extends StatelessWidget {
final bool showMessage;
final bool isLoading;
final VoidCallback onButtonPressed;
const childWidget({
required this.showMessage,
required this.isLoading,
required this.onButtonPressed,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: onButtonPressed,
child: Text('Show message'),
),
SizedBox(height: 20),
if (isLoading) CircularProgressIndicator(),
if (showMessage)
Text(
'Hello World',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
);
}
}
여기서, 버튼을 show와 hide로 나누어주고, message와 circular progress bar 또한 나누어 주겠습니다.
ShowButton 위젯입니다.
main.dart
class ShowButton extends StatelessWidget {
final VoidCallback onShowPressed;
const ShowButton({required this.onShowPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onShowPressed,
child: Text(
"Show"
)
);
}
}
HideButton 위젯입니다.
main.dart
class HideButton extends StatelessWidget {
final VoidCallback onHidePressed;
const HideButton({required this.onHidePressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onHidePressed,
child: Text(
"Hide"
)
);
}
}
Message 위젯입니다.
main.dart
class Message extends StatelessWidget {
final bool showMessage;
const Message({required this.showMessage});
@override
Widget build(BuildContext context) {
return Center(
child: showMessage
? Text(
'Hello World',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
) : SizedBox.shrink(),
);
}
}
Loading(원형 프로그레스 바) 위젯입니다.
main.dart
class Loading extends StatelessWidget {
final bool isLoading;
const Loading({required this.isLoading});
@override
Widget build(BuildContext context) {
return Center(
child: isLoading ? CircularProgressIndicator() : SizedBox.shrink()
);
}
}
이제 상위 클래스명과 세부 사항들을 수정해 주겠습니다.
main.dart
class BlocExample extends StatefulWidget {
@override
_BlocExampleState createState() => _BlocExampleState();
}
class _BlocExampleState extends State<BlocExample> {
bool _showMessage = false;
bool _isLoading = false;
void _show() async {
setState(() {
_isLoading = true;
});
await Future.delayed(Duration(seconds: 3));
setState(() {
_isLoading = false;
_showMessage = true;
});
}
void _hide() async {
setState(() {
_isLoading = true;
});
await Future.delayed(Duration(seconds: 3));
setState(() {
_isLoading = false;
_showMessage = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('State Hoisting Example')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowButton(onShowPressed: _show),
HideButton(onHidePressed: _hide),
Message(showMessage: _showMessage),
Loading(isLoading: _isLoading)
],
)
);
}
}
전체 코드입니다.
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: BlocExample(),
));
}
class BlocExample extends StatefulWidget {
@override
_BlocExampleState createState() => _BlocExampleState();
}
class _BlocExampleState extends State<BlocExample> {
bool _showMessage = false;
bool _isLoading = false;
void _show() async {
setState(() {
_isLoading = true;
});
await Future.delayed(Duration(seconds: 3));
setState(() {
_isLoading = false;
_showMessage = true;
});
}
void _hide() async {
setState(() {
_isLoading = true;
});
await Future.delayed(Duration(seconds: 3));
setState(() {
_isLoading = false;
_showMessage = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('State Hoisting Example')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowButton(onShowPressed: _show),
HideButton(onHidePressed: _hide),
Message(showMessage: _showMessage),
Loading(isLoading: _isLoading)
],
)
);
}
}
class ShowButton extends StatelessWidget {
final VoidCallback onShowPressed;
const ShowButton({required this.onShowPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onShowPressed,
child: Text(
"Show"
)
);
}
}
class HideButton extends StatelessWidget {
final VoidCallback onHidePressed;
const HideButton({required this.onHidePressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onHidePressed,
child: Text(
"Hide"
)
);
}
}
class Message extends StatelessWidget {
final bool showMessage;
const Message({required this.showMessage});
@override
Widget build(BuildContext context) {
return Center(
child: showMessage
? Text(
'Hello World',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
) : SizedBox.shrink(),
);
}
}
class Loading extends StatelessWidget {
final bool isLoading;
const Loading({required this.isLoading});
@override
Widget build(BuildContext context) {
return Center(
child: isLoading ? CircularProgressIndicator() : SizedBox.shrink()
);
}
}
보시는 바와 같이 상태와 관련된 코드들이 늘어났으며, 복잡해 보이는 것을 알 수 있습니다.
이제 이를 Bloc pattern을 사용하도록 수정해 보겠습니다.
Bloc pattern example
가장 먼저 터미널에서 명령어를 입력하여 bloc 관련 패키지를 추가해 줍니다(figure 1 참조).
flutter pub add flutter_bloc
그리고 새로운 dart 파일들을 생성해 줍니다(figure 2 참조).
이제 하나씩 만들어 가보겠습니다.
상태들을 추가합니다.
example_state.dart
abstract class MessageState {}
class InitialState extends MessageState {}
class LoadingState extends MessageState {}
class MessageShownState extends MessageState {}
class MessageHiddenState extends MessageState {}
이벤트를 추가합니다.
example_event.dart
abstract class MessageEvent {}
class ShowMessageEvent extends MessageEvent {}
class HideMessageEvent extends MessageEvent {}
bloc을 추가합니다.
example_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'example_event.dart';
import 'example_state.dart';
class MessageBloc extends Bloc<MessageEvent, MessageState> {
MessageBloc() : super(InitialState()) {
on<ShowMessageEvent>((event, emit) async {
emit(LoadingState());
await Future.delayed(Duration(seconds: 3));
emit(MessageShownState());
});
on<HideMessageEvent>((event, emit) async {
emit(MessageHiddenState());
});
}
}
event가 발생하면 그에 상응하는 동작을 수행 후 state를 발행합니다. 기존의 동작을 약간 수정해서 message가 나타나기 전에는 3초의 loading이 있지만, 사라질 때는 바로 사라지도록 구현하였습니다.
이제 UI 코드를 수정할 차례입니다.
main 함수를 아래와 같이 수정합니다.
main.dart
void main() {
runApp(MaterialApp(
home: BlocProvider(
create: (context) => MessageBloc(),
child: BlocExample()
),
));
}
_BlocExampleState를 지워주고, BlocExample을 StatelessWidget으로 바꿔줍니다. 이제 UI widget 내에서 state를 관리하지 않기 때문입니다.
main.dart
class BlocExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Pattern Example')),
body: BlocBuilder<MessageBloc, MessageState>(
builder: (context, state) {
final isLoading = state is LoadingState;
final showMessage = state is MessageShownState;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowButton(
onShowPressed: () {
context.read<MessageBloc>().add(ShowMessageEvent());
},
),
HideButton(
onHidePressed: () {
context.read<MessageBloc>().add(HideMessageEvent());
},
),
Message(showMessage: showMessage),
Loading(isLoading: isLoading),
],
);
},
),
);
}
}
UI 부분의 전체 코드입니다.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:for_practice/src/example_bloc.dart';
import 'src/example_event.dart';
import 'src/example_state.dart';
void main() {
runApp(MaterialApp(
home: BlocProvider(
create: (context) => MessageBloc(),
child: BlocExample()
),
));
}
class BlocExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Pattern Example')),
body: BlocBuilder<MessageBloc, MessageState>(
builder: (context, state) {
final isLoading = state is LoadingState;
final showMessage = state is MessageShownState;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowButton(
onShowPressed: () {
context.read<MessageBloc>().add(ShowMessageEvent());
},
),
HideButton(
onHidePressed: () {
context.read<MessageBloc>().add(HideMessageEvent());
},
),
Message(showMessage: showMessage),
Loading(isLoading: isLoading),
],
);
},
),
);
}
}
class ShowButton extends StatelessWidget {
final VoidCallback onShowPressed;
const ShowButton({required this.onShowPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onShowPressed,
child: Text(
"Show"
)
);
}
}
class HideButton extends StatelessWidget {
final VoidCallback onHidePressed;
const HideButton({required this.onHidePressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onHidePressed,
child: Text(
"Hide"
)
);
}
}
class Message extends StatelessWidget {
final bool showMessage;
const Message({required this.showMessage});
@override
Widget build(BuildContext context) {
return Center(
child: showMessage
? Text(
'Hello World',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
) : SizedBox.shrink(),
);
}
}
class Loading extends StatelessWidget {
final bool isLoading;
const Loading({required this.isLoading});
@override
Widget build(BuildContext context) {
return Center(
child: isLoading ? CircularProgressIndicator() : SizedBox.shrink()
);
}
}
상태 관리 부분이 빠진 UI 코드는 훨씬 깔끔해 보이며, 알아보기도 더욱 쉬워졌습니다.
이제 테스트를 해보겠습니다.
정상적으로 동작합니다.
이상 포스팅을 마치겠습니다.
감사합니다.
'Flutter Application > 앱 설계' 카테고리의 다른 글
Flutter app 상태 관리 - state hoisting (0) | 2024.11.28 |
---|