플러터

Flutter - Bloc패턴 아키텍처에 대한 이해

김염인 2022. 5. 10. 21:45

 

기존 사용하는 statefull을 이용한 숫자 증감소 그리고
BloC패턴을 이용한 숫자 증감을 비교하며 Bloc패턴에 대한 이해도를 높여보았다.

 


 

먼저 Bloc패턴이란? 무엇일까

 

1) BloC 패턴

BLoC(Business Logic Component)은 상태가 변화할때마다 렌더링 되는 Flutter에서 UI Business Logic을 분리하여 사용하는 패턴이다.
공식문서에 따르면 Presentation layer와 Business layer를 분리함으로써 아래와 같은 효과를 얻을 수 있다고 작성되있다.

  • 빠른 속도(fast)
  • 테스트 용이성(easy to test)
  • 재사용성(reusable)

이런 BLoC 패턴은 예를 들어 아무때나 애플리케이션 내에서 사용되는 상태에 대해서 알고 싶을 때, 애플리케이션이 적절하게 반응하고 있는지 쉽게 테스트를 해보고 싶을 때 등 사용할 수 있다. BLoC은 Simple, Powerful 그리고 Testable, 이 3가지의 핵심 가치를 중심으로 개발되었다고 한다.

 

Flutter에 BLoC을 사용할 수 있도록 많은 패키지가 있다. 대표적으로는 package:bloc package:flutter_bloc 등이 있는데 첫번째 패키지에 대한 이해가 있어야 나머지 패키지를 수월하게 사용할 수 있다.

 

 


그리고 BloC패턴에 대한 이해를 하기 위해서는 Stream에 대해서 알아야 한다.

2) Stream

스트림은 데이터나 이벤트가 들어오는 통로이다.

앱을 만들다 보면 데이터를 처리할 일이 많은데, 어느 타이밍에 데이터가 들어올지 확실히 알기 어렵다. 
스트림은 이와 같은 비동기 작업을 할 때 주로 쓰인다.예컨대 네트워크에서 데이터를 받아서 UI에 보여주는 상황을 생각해보면,
언제 네트워크에서 데이터를 다 받을지 알기 어렵습니다. 신호가 약한 와이파이를 쓸 수도 있고, 빵빵한 통신을 쓰고 있을 수도 있다.이런 문제를 스트림은 데이터를 만드는 곳과 소비하는 곳을 따로 둬서 이 문제를 해결할 수 있다.

스트림이란 데이터의 추가나 변경이 일어나면 이를 관찰하던데서 처리하는 방법이다. (옵서버 패턴입니다)
- Bloc패턴에서 쓰이는 Stream은 기존 Kotlin의 Mvvm패턴의 LiveData를 Observing하는 과정과 비슷하다.

 

Future와 다른 점은?

Dart의 비동기 프로그래밍은 Future 및 Stream 클래스로 주로 처리합니다.
Future는  즉시 완료되지 않는 계산을 나타냅니다. 일반 함수가 결과를 반환하는 경우 비동기 함수는 Future를 반환하며 
결과에 포함됩니다. 결과가 준비되면 Future에 알 여주는 것입니다.

스트림은 일련의 비동기 이벤트입니다. 
요청 시 다음 이벤트를 받는 대신 스트림이 준비되면 이벤트가 있음을 알려주는 비동기 Iterable과 같습니다.

 

< 정리 >

  • Stream : 비동기적인 데이터의 sequence(== Iterable한 비동기적 데이터)
    • Stream은 요청한 작업의 다음 event를 가져오는 것이 아니라, 준비가 되면 준비가 된 event가 있다고 알려준다.
    • Iterable하기 때문에 한 번에 여러 Future를 전달해준다.
    • Stream를 사용하기 위해서는 함수 body를 async*로 감싸준다.
    • 데이터 처리방법
      • await for : for loop와 함께 사용되는 것을 말한다.
      • listen
    • Stream 종류
      • Single Subscription (1:1) : 순차적으로 잃어버리지 않고 전송되어야한다. 예를 들어서 File, API관련 event가 발생하면 chunk로 제공된다.
      • Broadcast (1:N) : 한 번에 하나 독립적인 처리가 이루어진다. 언제든지 listen 가능하며 listen하는 동안 발생(fired) 가능하다.
    • 비동기 작업 반환(return type)
      • async* : 요청이 있을 때까지는 연산하는 걸 미루고 필요할 때 처리한다. (== lazily, 게으르게)
      • yield : return과 유사하다. yield는 함수를 종료시키지 않는다. 열린 채로 있어서 필요할 때 다른 연산이 가능하다.
      • yield* : Iterable 또는 Stream 함수를 재귀적으로 호출할 때 사용한다.
  • Future : 비동기 작업의 결과, (시간이)오래 걸릴 수 있다.
    • Future를 사용하기 위해서는 함수 body를 async로 감싸고 Future 결과가 모두 올때까지 기다리게 하는 await를 작성해준다.
    • await를 통한 결과는 바로 사용가능하며, await가 없다면 <Instance of Future>가 반환된다.
    • Error handling은 try-catch를 보편적으로 사용한다.

 


3) SatafulWidget 구현

먼저 가장 간단한 StateFullWidget으로 구현,

componetns와 ui로 나누어 주었다.

 

3-1) PlusStatefulDisolayWidget.dart

import 'package:flutter/material.dart';

import '../components/count_view_sateless.dart';

class PlusStatefulDisplayWidget extends StatefulWidget {
  const PlusStatefulDisplayWidget({Key? key}) : super(key: key);

  @override
  State<PlusStatefulDisplayWidget> createState() => _PlusStatefulDisplayWidgetState();
}

class _PlusStatefulDisplayWidgetState extends State<PlusStatefulDisplayWidget> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("기본 StateFull"),
      ),
      body: CountViewStateless(count : count),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: (){
              setState(() {
                count++;
              });
            },
          ),
          IconButton(
            icon: Icon(Icons.remove),
            onPressed: (){
              setState(() {
                count--;
              });
            },
          ),
        ],
      ),
    );
  }
}

- flutter프로젝트를 생성하면 나오는 widget과 같이 staful하게 구성, setState()를 통해 build()시 count가 hotReload를 통해 지속적으로 변경될 수 있다.

 

 

3-2) CountViewSateless.dart

import 'package:flutter/material.dart';


class CountViewStateless extends StatelessWidget {
  int count;
  CountViewStateless({Key? key, required this.count}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        count.toString(),
        style: TextStyle(fontSize: 80),
      ),
    );
  }
}

- ui와 Componets를 나누어 주는게 sateful 위젯 구성시 필요사항이다 아니면 너무 난잡해지므로!! 기억해두자

 

 

 


 

4) BloC패턴 구현

bloc, components, ui 구분

 

- 여기서 가장 중요한 것은 bloc directory를 통해 ui와 비즈니스 로직을 나누어 주었다는 점이다.

 

4-1) BlocDisplayWidet.dart

import 'package:flutter/material.dart';

import '../bloc/count_bloc.dart';
import '../components/count_view.dart';

late CountBloc countBloc;

class BlocDisplayWidget extends StatefulWidget {

  const BlocDisplayWidget({Key? key}) : super(key: key);

  @override
  State<BlocDisplayWidget> createState() => _BlocDisplayWidgetState();
}

class _BlocDisplayWidgetState extends State<BlocDisplayWidget> {

  @override
  void initState(){
    super.initState();
    countBloc = CountBloc();
  }

  void dispose(){
    super.dispose();
    countBloc.dispose();
  }
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc 패턴'),
      ),
      body: CountView(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: (){
              setState(() {
                countBloc.add();
              });
            },
          ),
          IconButton(
            icon: Icon(Icons.remove),
            onPressed: (){
              setState(() {
                countBloc.subtract();
              });
            },
          ),
        ],
      ),
    );
  }
}

countBloc이라는 Bloc패턴 구조의 add함수와 subtract함수를 불러옴

 

 

4-2) CountView.dart

import 'package:bloc_pattern/src/bloc_pattern/ui/bloc_display_widget.dart';
import 'package:flutter/material.dart';

class CountView extends StatelessWidget {
  const CountView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: StreamBuilder(
        stream: countBloc.count,
        initialData: 0,
        builder: (BuildContext context, AsyncSnapshot<int> snapshot){
          if(snapshot.hasData){
            return Text(snapshot.data.toString(),
            style: TextStyle(fontSize: 80),);
          }
          return CircularProgressIndicator();
        },
      ),
    );
  }
}

- 여기서 중요한 것은 StreamBuilder를 stream을 불러온 뒤 통해 snapshot을 관찰하여 data가 있는 경우 ui에 반영 되게 하였다.

 

 

4-3) CountBloc.dart

import 'dart:async';

class CountBloc{
  int _count = 0;
  final StreamController<int> _countSubject = StreamController<int>.broadcast();
  Stream<int> get count => _countSubject.stream;

  add(){
    _count++;
    _countSubject.sink.add(_count);
  }

  subtract(){
    _count--;
    _countSubject.sink.add(_count);
  }

  dispose(){
    _countSubject.close();
  }
}

- 초기 count를 0으로 설정 한후 StreamController를 설정해 Observing해준다. 이때. broadcast()를 통해 2개이 상의 count 변수가 참조 됐을 경우도 생각해준다. broadcast()는 한 번에 하나 독립적인 처리가 이루어진다. 언제든지 listen 가능하며 listen하는 동안 발생(fired) 가능하다. 이렇게 설정한 add(), subtract()함수를 불러오고 dispose()를 통해 종료 조건 또한 설정해준다.

 


정리 : 하지만 Bloc 패턴의 경우 간단한 로직 하나 구현하는데도 최소 4개의 클래스를 작성해야 하는 불편함이 있다. 그래서 등장한 것이 Provider이다. Provider는 내가 졸업프로젝트, 인턴활동을 하였을 때 유용하게 사용했던 상태관리 로직이다. Provider를 이용하여 관리를 하는 방법 또한 다음 시간에 복습해봐야겠다.