#F01. Bloc pattern Flutter( Bloc 패턴. 플러터)

kathy·2021년 10월 28일
1

Flutter

목록 보기
1/2
post-thumbnail

flutter 프로젝트를 진행함에 있어서 앱의 기능이 많아지면 많아질수록 한 페이지에 여러 UI와 Data,등이 존재하게된다. 이 모든 코드들을 한 페이지에 구성하게되면 코드가 길어지고 복잡해지면서 추후에 유지,보수하기 어려워진다. 이에 유용한 디자인 패턴 중 하나가 바로 Bloc pattern이다.

Bloc 패턴이란?
Bloc은 Business Logic Component의 약자로 Flutter 프로젝트 속의 UI와 Business Logic을 분리하여 만든 방식을 의미한다. 즉 Flutter의 state를 관리하는 디자인 패턴 중 하나로 코드를 깔끔하고 유지,보수 하기 쉽게 정리하는 스킬이다.

Bloc 패턴을 이용하면 다음과 같은 방식으로 데이터를 주고 받는다.
간단한 앱을 통해 더 쉽게 이해해보자.

Flutter Bloc pattern (with. 미세먼지 측정 앱)

0. Bloc pattern 이전 미세먼지 앱

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 '최악';
    }
  }
}

1.rxdart Pub.get하기

*rxdart 설치하기

dependencies:
  rxdart: ^0.27.2

[rxdart install]-https://pub.dev/packages/rxdart/install

2. bloc 폴더 & airbloc.dart 생성하기

3. main.dart 속의 Business logic 부분을 추출하여 airbloc.dart에 옮기기

여기서 "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 '최악';
    }
  }
}

4. 고찰

위의 미세먼지 앱은 간단한 코드이므로 단순화되었는지 한 눈에 보이지는 않는다. 하지만 더 복잡한 코드에서는 더 정리화되어 보일 것이며 Business logic와 UI를 구별하여 유지 및 보수관리가 용이하며 개별 테스트가 가능하다는 큰 장점이 있다.

📝참고문헌

  • inflearn.(https://www.inflearn.com/course/flutter_%EC%A4%91%EA%B8%89/dashboard).
  • pub.dev(https://pub.dev/packages/rxdart/install)
  • profile
    Here is future Backend Developer's Velog

    0개의 댓글