이번에는 Flutter에 Bloc 패턴에 대해서 알아보고 구현해보자.
이전에도 설명했듯이 Bloc 패턴의 목적은 상태 관리, UI와 비즈니스 로직의 분리를 하기 위한 것이다.
Bloc 패턴이 상태 관리, UI와 비즈니스 로직의 분리를 해준다는 것은 알겠는데, 그러면 우리는 왜 Bloc 패턴을 써야 하는가? 의문이 들 수 있다. 예를 들어 배포를 위한 앱을 개발할 때 복잡한 구조의 위젯 트리를 만들었다고 가정해보자. 이때, Scaffold 위젯을 통해 많은 위젯이 제어되는데 제어되는 과정 속에서 모든 하위 위젯들의 불필요한 업데이트가 진행된다. 이게 단순한 앱일 경우에는 체감으로 느끼지 못할 수 있지만 무거운 앱일수록 사용자가 불편하게 느낄 정도로 앱이 느려지는 것을 확인할 수 있을 것이다.
이러한 문제를 막기 위해 Bloc 패턴을 사용하는 것이다. UI와 비즈니스 로직을 분리하고 상태 관리를 하게 된다면 제어해주고 싶은 위젯만을 따로 제어해 효율적이고 깔끔한 코드를 설계할 수 있게 될 것이고 이로 인해 퍼포먼스 측면, 유지보수 측면에서도 큰 도움이 될 것이다.
그럼 본격적으로 Bloc 패턴을 구현해보자.
코드의 이해를 돕기 위해 폴더와 파일의 위치를 다음과 같이 구성한다.
폴더와 파일의 위치를 위 사진과 같이 구성했다면 본격적으로 코드를 작성해보자.
주석을 통해 코드의 흐름을 파악하면 더 이해하기 쉬울 것 같다.
main.dart
import 'package:bloc_pattern/src/ui/bloc_display_widget.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocDisplayWidget() // BlocDisplayWidget 호출
);
}
}
home에서 BlocDisplayWidget()을 호출한다.
bloc_display_wideget.dart
import 'package:bloc_pattern/src/bloc/count_bloc.dart';
import 'package:bloc_pattern/src/components/count_view.dart';
import 'package:flutter/material.dart';
late CountBloc countBloc; // 전역 변수로 CountBloc을 호출하고 late를 통해 나중에 값을 받는다.
class BlocDisplayWidget extends StatefulWidget {
const BlocDisplayWidget({Key? key}) : super(key: key);
@override
_BlocDisplayWidgetState createState() => _BlocDisplayWidgetState();
}
class _BlocDisplayWidgetState extends State<BlocDisplayWidget> {
// initState() : 위젯이 생성될 때 처음으로 호출되는 메서드
// initState()을 통해 CountBloc()을 생성
@override
void initState() {
super.initState();
countBloc = CountBloc();
}
// dispose(): 위젯이 종료될 때 호출되는 메서드
// dispose()을 통해 countBloc을 종료시켜 메모리 누수를 방지한다.
@override
void dispose() {
super.dispose();
countBloc.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Bloc 패턴"),
centerTitle: true,
elevation: 0.0,
),
body: CountView(), // Count만을 관리하는 CountView 호출
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// countBloc에서 add() 이벤트를 호출
countBloc.add();
},
),
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
// countBloc에서 remove() 이벤트를 호출
countBloc.remove();
},
)
],
),
);
}
}
위 코드는 앱이 실행될 때 표시할 UI이다.
위에서부터 코드를 확인하게 되면 전역 변수로 CountBloc을 호출하고 late를 통해 나중에 값을 받게 된다. 다음으로 initState()를 통해 CountBloc을 생성하게 되고 dispose()를 통해 위젯이 종료될 때 메모리 누수를 방지하게 된다. 다음으로 body 부분을 보게 되면 count만을 관리하는 CountView()를 호출하게 된다. 다음으로 floatingActionButton을 통해 더하기 버튼과 빼기 버튼을 만든 것을 확인할 수 있고 버튼을 누르게 되면 각각 CountBloc에 이벤트를 호출하게 된다.
CountBloc에 대해서는 다음 코드를 통해 알아보자.
count_bloc.dart
import 'dart:async';
// 비즈니스 로직 분리
class CountBloc {
int _count = 0;
// StreamController을 통해 여러 이벤트를 처리
final StreamController _countSubject = StreamController.broadcast();
// count는 _countSubject.stream 을 구독하고 있는 모든 위젯에게 변경된 상태를 알림
Stream get count => _countSubject.stream;
// count 덧셈 이벤트 처리
add() {
_count++;
_countSubject.sink.add(_count); // _countSubject.sink 에다가 _count를 넣어준다.
}
// count 뺄셈 이벤트 처리
remove() {
_count--;
_countSubject.sink.add(_count); // _countSubject.sink 에다가 _count를 넣어준다.
}
// _countSubject을 종료
dispose() {
_countSubject.close();
}
}
위 코드는 비즈니스 로직을 분리한 부분이다.
위에서부터 코드를 확인하게 되면 StreamController을 통해 여러 이벤트를 처리하게 되고 count는 _countSubject.stream을 구독하고 있는 모든 위젯에게 변경된 상태를 알리게 된다. 다음으로 count 덧셈 이벤트 처리와 뺄셈 이벤트 처리의 함수는 _count를 연산 후 _countSubject에 sink를 통해 _count를 전달해주게 된다.
그럼 count에 변화를 UI에 뿌려주는 코드인 CountView에 대해서 알아보자.
count_view.dart
import 'package:bloc_pattern/src/ui/bloc_display_widget.dart';
import 'package:flutter/material.dart';
// count만을 보여주는 코드
class CountView extends StatelessWidget {
CountView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
// 비동기 처리(StreamBuilder : 변화되는 값을 계속해서 감지)
// StreamBuilder를 통해 countBloc.count을 감지
child: StreamBuilder(
stream: countBloc.count, // countBloc.count => _countSubject.stream 을 구독중
initialData: 0,
builder: (BuildContext context, AsyncSnapshot snapshot) {
// AsyncSnapshot을 통해 들어온 snapshot을 UI에 뿌려준다.
if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: TextStyle(fontSize: 80),
);
}
return CircularProgressIndicator();
},
),
);
}
}
위 코드는 count 만을 보여준다.
위에서부터 코드를 보게 되면 비동기 처리인 StreamBulider를 통해 변화되는 값인 countBloc.count를 계속해서 감지한다. countBloc.count은 _countSubject.stream을 구독중이기 때문에 _countSubject.stream에 변화에 따라 countBloc.count가 변화게 된다. 이 변화된 값을 StreamBulider는 계속해서 감지하고 있다가 변화가 생기면 AsyncSnapshot을 통해 들어온 snapshot을 UI에 뿌리게 된다.
모든 코드를 한 번에 리뷰를 해보면 다음과 같다.
BlocDisplayWidget에서 floatingButton을 통해 더하기, 빼기 버튼을 누르게 되면 CountBloc에 이벤트를 호출하게 되고 호출된 CountBloc에 이벤트를 수행하면서 _countSubject에 sink를 통해 연산된 _count를 전달하게 된다. 전달된_countSubject.stream을 구독하고 있는 모든 위젯에게 변경된 상태를 count를 통해 알리게 된다. 이때, count만을 보여주는 코드인 CountView의 StreamBulider에서 countBloc.count에 변경된 상태를 감지하고 AsyncSnapshot을 통해 들어온 snapshot을 UI에 뿌리게 된다.
위 코드를 모두 작성하고 실행하면 다음 결과 화면과 같이 실행되는 것을 확인할 수 있다.
하지만 Bloc 패턴의 경우 간단한 로직 하나 구현하는데도 최소 4개의 클래스를 작성해야 하는 불편함이 있다. 그래서 등장한 것이 Provider이다. Provider는 다음 링크를 통해 알아보자.