flutter 프로젝트를 진행함에 있어서 앱의 기능이 많아지면 많아질수록 한 페이지에 여러 UI와 Data,등이 존재하게된다. 이 모든 코드들을 한 페이지에 구성하게되면 코드가 길어지고 복잡해지면서 추후에 유지,보수하기 어려워진다. 이에 유용한 디자인 패턴 중 하나가 바로 Bloc pattern이다.
Bloc 패턴이란?
Bloc은 Business Logic Component의 약자로 Flutter 프로젝트 속의 UI와 Business Logic을 분리하여 만든 방식을 의미한다. 즉 Flutter의 state를 관리하는 디자인 패턴 중 하나로 코드를 깔끔하고 유지,보수 하기 쉽게 정리하는 스킬이다.
Bloc 패턴을 이용하면 다음과 같은 방식으로 데이터를 주고 받는다.
간단한 앱을 통해 더 쉽게 이해해보자.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_dust/models/airresult.dart';
import 'package:http/http.dart' as http;
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',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Main(),
);
}
}
class Main extends StatefulWidget {
const Main({Key key}) : super(key: key);
@override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
AirResult _result;
Future<AirResult> fetchData() async {
var response = await http.get(
'https://api.airvisual.com/v2/nearest_city?key=(개인 KEY 값)');
AirResult result = AirResult.fromJson(json.decode(response.body));
return result;
}
@override
void initState() {
super.initState();
fetchData().then((airResult) {
setState(() {
_result = airResult;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _result == null
? CircularProgressIndicator()
: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'현재 위치 미세먼지',
style: TextStyle(fontSize: 30),
),
SizedBox(
width: 16.0,
),
Card(
child: Column(
children: <Widget>[
Container(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: <Widget>[
Text(
'${_result.data.city}',
),
Text(
'${_result.data.current.pollution.aqius}',
style: TextStyle(fontSize: 40),
),
Text(
getString(_result),
style: TextStyle(fontSize: 20),
),
],
),
color: getColor(_result),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Image.network(
'https://airvisual.com/images/${_result.data.current.weather.ic}.png',
width: 32,
height: 32,
),
Text(
'${_result.data.current.weather.tp}',
style: TextStyle(fontSize: 40),
),
SizedBox(
width: 16,
),
],
),
),
Text('습도 ${_result.data.current.weather.hu} %'),
Text(
'풍속 ${_result.data.current.weather.ws} m/s'),
],
),
],
),
),
SizedBox(
width: 16.0,
),
ClipRRect(
borderRadius: BorderRadius.circular(30),
child: MaterialButton(
padding: const EdgeInsets.symmetric(
vertical: 15.0, horizontal: 50),
onPressed: () {},
color: Colors.orange,
child: Icon(
Icons.refresh,
color: Colors.white,
),
),
),
],
),
),
),
),
);
}
Color getColor(AirResult result) {
if (result.data.current.pollution.aqius <= 50) {
return Colors.greenAccent;
} else if (result.data.current.pollution.aqius <= 100) {
return Colors.yellow;
} else if (result.data.current.pollution.aqius <= 150) {
return Colors.orange;
} else {
return Colors.red;
}
}
String getString(AirResult result) {
if (result.data.current.pollution.aqius <= 50) {
return '좋음';
} else if (result.data.current.pollution.aqius <= 100) {
return '보통';
} else if (result.data.current.pollution.aqius <= 150) {
return '나쁨';
} else {
return '최악';
}
}
}
*rxdart 설치하기
dependencies: rxdart: ^0.27.2
여기서 "BehaviorSubject"를 넣기 위해 rxdart가 사용이 된다.
rxdart는 Dart Streams 기능을 확장시키는StreamControllers 역할을 한다.
airbloc.dart
import 'dart:convert';
import 'package:flutter_dust/models/airresult.dart';
import 'package:http/http.dart' as http;
import 'package:rxdart/rxdart.dart';
class AirBloc{
final _airSubject = BehaviorSubject<AirResult>();
AirBloc() {
fetch();
}
Future<AirResult> fetchData() async {
var response = await http.get(
'https://api.airvisual.com/v2/nearest_city?key=(개인 KEY 값)');
AirResult result = AirResult.fromJson(json.decode(response.body));
return result;
}
void fetch() async {
var airResult = await fetchData();
_airSubject.add(airResult);
}
Stream<AirResult> get airResult => _airSubject.stream;
}
Business logic 부분을 추출하고 나머지 UI부분을 정리하면 다음과 같이 간단하게 나타낼 수 있다.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_dust/bloc/airbloc.dart';
import 'package:flutter_dust/models/airresult.dart';
void main() {
runApp(const MyApp());
}
final airBloc = AirBloc();
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Main(),
);
}
}
class Main extends StatefulWidget {
const Main({Key key}) : super(key: key);
@override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<AirResult>(
stream: airBloc.airResult,
builder: (context, snapshot) {
if (snapshot.hasData) {
return _buildBody(snapshot.data);
} else {
return CircularProgressIndicator();
}
}),
),
);
}
Widget _buildBody(AirResult _result) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'현재 위치 미세먼지',
style: TextStyle(fontSize: 30),
),
SizedBox(
width: 16.0,
),
Card(
child: Column(
children: <Widget>[
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(
'${_result.data.city}',
),
Text(
'${_result.data.current.pollution.aqius}',
style: TextStyle(fontSize: 40),
),
Text(
getString(_result),
style: TextStyle(fontSize: 20),
),
],
),
color: getColor(_result),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Image.network(
'https://airvisual.com/images/${_result.data.current.weather.ic}.png',
width: 32,
height: 32,
),
Text(
'${_result.data.current.weather.tp}',
style: TextStyle(fontSize: 40),
),
SizedBox(
width: 16,
),
],
),
),
Text('습도 ${_result.data.current.weather.hu} %'),
Text('풍속 ${_result.data.current.weather.ws} m/s'),
],
),
],
),
),
SizedBox(
width: 16.0,
),
ClipRRect(
borderRadius: BorderRadius.circular(30),
child: MaterialButton(
padding:
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 50),
onPressed: () {
airBloc.fetch();
},
color: Colors.orange,
child: Icon(
Icons.refresh,
color: Colors.white,
),
),
),
],
),
),
);
}
Color getColor(AirResult result) {
if (result.data.current.pollution.aqius <= 50) {
return Colors.greenAccent;
} else if (result.data.current.pollution.aqius <= 100) {
return Colors.yellow;
} else if (result.data.current.pollution.aqius <= 150) {
return Colors.orange;
} else {
return Colors.red;
}
}
String getString(AirResult result) {
if (result.data.current.pollution.aqius <= 50) {
return '좋음';
} else if (result.data.current.pollution.aqius <= 100) {
return '보통';
} else if (result.data.current.pollution.aqius <= 150) {
return '나쁨';
} else {
return '최악';
}
}
}
위의 미세먼지 앱은 간단한 코드이므로 단순화되었는지 한 눈에 보이지는 않는다. 하지만 더 복잡한 코드에서는 더 정리화되어 보일 것이며 Business logic와 UI를 구별하여 유지 및 보수관리가 용이하며 개별 테스트가 가능하다는 큰 장점이 있다.