[Flutter] BLoC 패턴

송상민·2022년 10월 13일
2

Flutter

목록 보기
8/13
post-thumbnail

BLoC패턴에 대해 알아보자.

참조


BLoC 패턴

BLoC(Business Logic Component)는 파올로 소아레스와 콩 후이라는 개발자에 의해 디자인 되어 2018 DartConf에서 발표되었다. Flutter의 상태 관리를 제어하기 위해서 디자인 되었다. Flutter를 위해서 설계 되었지만, 디자인 패턴이기 때문에, 어떠한 프레임워크나 언어에서도 사용할 수 있다.

BLoC는 Presentaion Layer와 businessLogic을 분리하여 코드를 작성할 수 있게 해준다.


BlocState(상태)를 보유하고 이 StateStream(스트림)을 제공한다. Widget은Stream을 구독하고 있어 State가 변경될 때마다 알림을 받을 수 있다. BlocWidget에게 Event를 받는다. Event가 Bloc으로 전송 되고 Bloc이 transition을 적용하여 Event를 처리한다. transition은 Event에 대한 응답으로 한 상태에서 다른 상태로 변경하는 역할을 한다. transition이 적용된 후 스트림에 알림이 전송되고 1분 정도 뒤 UI에 상태 변경 사항이 반영된다.

BLoC 특징

  • UI에서 여러 BLoC가 존재할 수 있다.
  • UI와 Business Logic을 분리하여 관리한다. (코드 의존성을 낮출 수 있다)
  • BLoC은 여러 UI에서 구독할 수 있다. (재사용이 용이하다)
  • BLoC만을 분리하여 테스트도 가능하다.

BLoC Counter 예제

사전준비

펼치기/접기📖

pubspec.yaml 변경

dependencies:
  bloc: ^8.1.0
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.1

dev_dependencies:
  bloc_test: ^9.1.0
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  mocktail: ^0.3.0
  very_good_analysis: ^3.0.1

모든 dependencies 설치

flutter packages get

프로젝트 구조

├── lib
│   ├── app.dart
│   ├── counter
│   │   ├── counter.dart
│   │   ├── cubit
│   │   │   └── counter_cubit.dart
│   │   └── view
│   │       ├── counter_page.dart
│   │       └── counter_view.dart
│   ├── counter_observer.dart
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml

BLoC Observer

애플리케이션의 모든 상태 변경을 관찰하는 데 도움이 되는 생성 방법.

lib/counter_observer.dart를 생성하여

import 'package:bloc/bloc.dart';

//BlocObserver 애플리케이션의 모든 상태 변화를 관찰하는데 도움이 된다.
class CounterObserver extends BlocObserver {
  
  void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }
}

발생하는 모든 상태 변경을 확인하기 위해 onChange를 재정의한다.

main.dart

main.dart의 내용을 다음과 같이 변경한다.

import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:bloccounterex/app.dart';
import 'package:bloccounterex/counter_observer.dart';

void main() {
  Bloc.observer = CounterObserver(); // 위에 생성해놓은 Observer를 Bloc observer로 초기화
  runApp(const CounterApp());
}

app.dart

lib/app.dart를 생성하여 MaterialApp을 extends하는 CounterApp 클래스를 만든다.

개인적으로 해당 부분은 main과 묶어서 작성해도 되는 부분인 것 같다.

import 'package:flutter/material.dart';
import 'counter/view/counter_page.dart';

class CounterApp extends MaterialApp {
  const CounterApp({super.key}) : super(home: const CounterPage());
}

counter_page.dart

lib/counter/view/counter_page.dart를 생성한다.

CounterPage 위젯은 CounterCubit을 작성하여 CounterView에 제공하는 역할을 한다.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloccounterex/counter/counter.dart';
import 'counter_view.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  
  Widget build(BuildContext context) {
    return BlocProvider( //상태 관리를 위한 BlocProvider
      create: (_) => CounterCubit(), //CounterCubit 생성
      child: const CounterView(), //상태 관리할 View
    );
  }
}

여기서 훨씬 더 테스트 가능하고 재사용 가능한 코드를 얻으려면 Cubit의 생성과 소비를 분리하 것이 중요하다.

counter_cubit

lib/counter/cubit/counter_cubit.dart을 생성한다.

CounterCubit 클래스는 두 가지 메서드를 가진다.

  • increment: 현재 상태에서 1을 더함.
  • decrement: 현재 상태에서 1을 뺌.
import 'package:bloc/bloc.dart';

class CounterCubit extends Cubit<int> { //Cubit이 관리하는 상태 유형을 int로 선언
  CounterCubit() : super(0);

  void increment() => emit(state + 1); //현재 상태에 +1 하는 메서드

  void decrement() => emit(state - 1);//현재 상태에 -1 하는 메서드
}

CounterCubit이 관리하고 있는 상태 유형은 int이고 초기 값은 0이다.

VSCode Extension 또는 IntelliJ Plugin을 사용하면 새로운 Cubit을 자동으로 생성해준다.

counter_view

lib/counter/view/counter_view.dart를 생성한다.

CounterView는 현재 카운트를 렌더링하고 카운터를 증가/감소(increment/decrement)하기 위해 두 개의 FloatingActionButton 을 렌더링하는 역할을 한다.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloccounterex/counter/counter.dart';

class CounterView extends StatelessWidget {
  const CounterView({super.key});

  
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Bloc Counter Example')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>( // 상태 변화에 변경되어야 할 부분은 Text 하나이기에
          builder: (context, state) {  // Text 하나만 Wrapping. 상태가 변화할 때마다 업데이트
            return Text('$state', style: textTheme.headline2); //State호출
          },
        ),
      ),
      floatingActionButton: Column( //+ - 버튼을 구현하기 위한 Column
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            key: const Key('counterView_increment_floatingActionButton'),
            child: const Icon(Icons.add),
            onPressed: () => context.read<CounterCubit>().increment(),//CounterCubit의 increment 함수 호출
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            key: const Key('counterView_decrement_floatingActionButton'),
            child: const Icon(Icons.remove),
            onPressed: () => context.read<CounterCubit>().decrement(),//CounterCubit decrement 함수 호출
          ),
        ],
      ),
    );
  }
}

BlocBuilderCounterCubit의 상태가 변경될 때 마다 텍스트를 업데이트하기 위해 텍스트 위젯을 감싸고 있다. context.read<CounterCubit>은 가장 가까운 CounterCubit 인스턴스를 조회하는 데 사용된다.

counter.dart

lib/counter/counter.dart를 생성한다.

// 소스코드를 캡슐화
export 'cubit/counter_cubit.dart';
export 'view/counter_page.dart';

이 두 작업을 통해 counter.dart만 import하여도 모든 관련 코드를 import할 수 있다.

profile
실력있는 Flutter 개발자가 되어보자

0개의 댓글