Develop/Flutter

[Flutter] 플러터 Bloc 패턴 구현

JunJangE 2021. 12. 27. 18:04

이번에는 Flutter에 Bloc 패턴에 대해서 알아보고 구현해보자.

이전에도 설명했듯이 Bloc 패턴의 목적은 상태 관리, UI와 비즈니스 로직의 분리를 하기 위한 것이다.

 

[Flutter] 플러터 Bloc 패턴과 Provider 패턴

Flutter 앱을 개발하다 보면 자주 등장하는 Bloc 패턴과 Provider 패턴에 대해서 간단하게 알아보자. Bloc 패턴은 Google 개발자가 권장하는 Flutter의 상태 관리 시스템이다. 프로젝트를 관리하는데 도움

fre2-dom.tistory.com

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는 다음 링크를 통해 알아보자.

 

[Flutter] 플러터 ChangeNotifierProvider를 통해 Provider 구현

이번에는 ChangeNotifierProvider를 통해 Provider 을 구현해보자. ChangeNotifier는 Flutter SDK에 포함된 클래스로 청취자에게 변경 알림을 제공해주는 것이다. 즉, ChangeNotifier라면 변화에 대해 구독을 할..

fre2-dom.tistory.com

참고

 

https://booiljung.github.io/technical_articles/flutter/state_management/architecture_your_flutter_project_using_bloc_pattern.html

Top BLOC 패턴을 사용한 Flutter 프로젝트 아키텍쳐 설계 원문: Architect your Flutter project using BLOC pattern 여러분 안녕하세요! 저는 Flutter에 관한 새로운 글을 가지고 왔습니다. 이번에는 “Flutter 프로젝

booiljung.github.io

 

Bloc 패턴이 무엇이고, 왜 사용하는 것일까?

안녕하세요 개발하는 남자 개남입니다. 플러터 포스팅의 가장 첫 번째로 Bloc 패턴을 다루게 되었네요. 앞으로 플러터를 공부 및 정리를 하면서 포스팅을 추가해 나아가도록 할 계획입니다. 플러

sudarlife.tistory.com

github

 

GitHub - junjange/Flutter-Learning: 플러터 학습

플러터 학습. Contribute to junjange/Flutter-Learning development by creating an account on GitHub.

github.com