관리 메뉴

Bull

[Flutter] Bloc 상태 관리 본문

Software Framework/Flutter

[Flutter] Bloc 상태 관리

Bull_ 2024. 7. 8. 22:10

Bloc이란?


Bloc은 Business Logic Component의 약자로, 복잡한 상태 관리를 위한 강력한 패턴입니다. 이벤트 기반의 상태 관리 방식으로, 대규모 애플리케이션에서 효과적입니다.

  • 설계 철학: Provider는 간단함과 유연성을 중시하며, Bloc은 명확한 구조와 테스트 용이성을 강조합니다.
  • 상태 관리 방식: Provider는 ChangeNotifier를 통해 상태를 관리하고, Bloc은 이벤트와 상태를 구독하고 전송하는 방식으로 관리합니다.
  • 복잡도: Provider는 비교적 배우기 쉽고 간단한 반면, Bloc은 학습 곡선이 더 가파릅니다.
  • 성능: 두 솔루션 모두 성능이 우수하지만, Bloc은 더 큰 규모의 상태 관리에 적합합니다.

Bloc 실전 적용해보기


bloc으로 상태관리를 하기 위해 카운트 증가/감소를 통한 예제를 소개합니다.

상태 정의 및 로직 구현


파일 구조

그 전에 디렉토리 구조는 다음과 같습니다.

counter_bloc_example/
├── lib/
│   ├── bloc/
│   │   ├── counter_bloc.dart
│   │   ├── counter_event.dart
│   │   └── counter_state.dart
│   └── main.dart

파일 설명 (코드)

counter_event.dart

import 'package:equatable/equatable.dart';

abstract class CounterEvent extends Equatable {
  const CounterEvent();

  @override
  List<Object> get props => [];
}

class CounterIncrementPressed extends CounterEvent {}

class CounterDecrementPressed extends CounterEvent {}

class CounterSetPressed extends CounterEvent {
  final int value;

  const CounterSetPressed(this.value);

  @override
  List<Object> get props => [value];
}

카운트가 증가/감소하거나 set하여 정해주는 함수를 정의합니다.

counter_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<CounterIncrementPressed>((event, emit) {
      emit(CounterState(state.counter + 1));
    });

    on<CounterDecrementPressed>((event, emit) {
      emit(CounterState(state.counter - 1));
    });

    on<CounterSetPressed>((event, emit) {
      emit(CounterState(event.value));
    });
  }
}

counter_event.dart에서 이벤트에 대한 정의에 로직을 구현해주는 부분입니다.

 

on((event, emit) { ... })와 emit(State) 메서드는 Bloc 패턴에서 이벤트를 처리하고 상태를 업데이트하는 데 사용됩니다.
event: 발생한 이벤트를 나타내는 객체입니다.
emit: 새로운 상태를 내보내는 함수입니다.

 

이 함수는 Bloc의 상태를 업데이트하는 데 사용됩니다.
emit을 호출하면 Bloc의 현재 상태가 새로운 상태로 변경되고, Bloc을 구독하고 있는 모든 UI 위젯이 새 상태를 반영하도록 다시 빌드됩니다.

 

하나의 예로, 증가하는 이벤트로직을 이렇게 해석해봅니다.
on:CounterIncrementPressed라는 이벤트는
emit:CounterState라는 상태를 가져와 상태를 변경해줍니다.

counter_state.dart

import 'package:equatable/equatable.dart';

class CounterState extends Equatable {
  final int counter;

  const CounterState(this.counter);

  @override
  List<Object> get props => [counter];
}

bloc을 통해 상태를 정의합니다. 이를 counter_bloc.dart에서 초기화 하거나 상태를 변경하는 로직을 수행할 때 emit의 파라미터로 포함되는 부분을 확인 할 수 있습니다.

상태를 통한 UI 적용(빌더) 및 부수효과 적용(리스너)

상태를 정의하였으니 상태에 대한 이용을 해야합니다.


이를 BlocBuilder와 BlocListener, BlocConsumer 세 가지로 나눠볼 수 있습니다.
BlocBuilder와 BlocListener, BlocConsumer를 사용하기 전에 위젯 클래스 부모에 BlocProvider를 선언해야합니다.
BlocProvider는 자식 위젯에서 Bloc을 사용할 수 있도록해주는 클래스입니다.
간단하기에 깊게 설명은 들어가지 않겠습니다.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

BlocConsumer는 상태를 감지하여 UI를 다시 빌드합니다.
BlocListener는 똑같이 상태를 감지하지만 빌더와 다르게 부수적인 효과를 적용할 때 사용합니다.
BlocConsumer는 빌더와 리스너를 합쳐 UI 빌드와 부수효과를 적용을 동시에 하고 싶을 때 사용합니다.
아래 코드에서는 사용하지 않고 설명만 하겠습니다.

 

다음은 빌더를 통해 UI에서 원의 크기를 count에 따라 커졌다가 작아지는 애니메이션을 만들고, 상태 업데이트는 증가/감소/set 세 가지를 구현했습니다.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/counter_bloc.dart';
import 'bloc/counter_event.dart';
import 'bloc/counter_state.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final TextEditingController _controller = TextEditingController();

    return Scaffold(
      appBar: AppBar(title: Text('Flutter Bloc Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BlocBuilder<CounterBloc, CounterState>(
              builder: (context, state) {
                double circleSize = state.counter % 2 == 0 ? 100.0 : 110.0;
                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      'Counter Value: ${state.counter}',
                      style: TextStyle(fontSize: 24.0),
                    ),
                    SizedBox(height: 20.0),
                    AnimatedContainer(
                      duration: Duration(milliseconds: 500),
                      width: circleSize,
                      height: circleSize,
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        shape: BoxShape.circle,
                      ),
                    ),
                  ],
                );
              },
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                controller: _controller,
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: 'Set Counter',
                ),
                keyboardType: TextInputType.number,
              ),
            ),
            ElevatedButton(
              onPressed: () {
                final int value = int.parse(_controller.text);
                BlocProvider.of<CounterBloc>(context).add(CounterSetPressed(value));
              },
              child: Text('Set Counter'),
            ),
            BlocListener<CounterBloc, CounterState>(
              listener: (context, state) {
                if (state.counter == 0) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Counter is zero!')),
                  );
                }
              },
              child: Container(),
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: () {
              BlocProvider.of<CounterBloc>(context).add(CounterIncrementPressed());
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
          SizedBox(height: 8.0),
          FloatingActionButton(
            onPressed: () {
              BlocProvider.of<CounterBloc>(context).add(CounterDecrementPressed());
            },
            tooltip: 'Decrement',
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

요약정리


상태 및 이벤트 정의

이벤트 로직 구현(counter_bloc.dart)

  • 상태 관리를 처리할 이벤트에 대하여 로직을 구현합니다. 
class CounterBloc extends Bloc<[이벤트], [상태]> {
    CounterBloc() : super(const 상태(초기값)) {
        on<[이벤트]>((event, emit) { 
        emit(상태(업데이트 값)); 
    });
...
}

이벤트 정의(counter_event.dart)

  • 상태를 업데이트해줄 이벤트를 정의해줍니다.

상태 정의(counter_state.dart)

  • 상태값에 대한 정의를 해주는 클래스입니다.

상태 사용 및 업데이트

BlocProovider

  • 자식 위젯이 상태에 접근할 수 있도록 해줍니다.
  • 인자*
  • create: 생성할 상태 로직을 반환하도록 합니다.
  • child: 접근 가능하도록 할 자식 위젯을 사용합니다.

BlocBuilder<[상태로직],[상태정의]>

  • UI 등 상태를 이용한 로직을 구현하려면 이 클래스 내에서 적용합니다.

인자

  • builder: 상태를 이용한 로직을 구현하고 원하는 위젯을 반환합니다.

BlocListener<[상태로직],[상태정의]>

  • 상태를 감지하고 부수적인 로직을 추가할 수 있습니다.

인자

  • listener: 상태를 이용한 부수적인 로직효과를 구현합니다.
  • child: 빌더처럼 다시 업데이트 되지는 않지만 특정 위젯을 추가하고 싶다면 자식위젯으로 넣습니다.

BlocConsumer<[상태로직],[상태정의]>

  • UI를 업데이트하고, 동시에 부수 효과를 처리해야 할 때 사용됩니다.

인자

  • listener: 상태 변화에 따라 부수 효과를 처리합니다. 상태가 변경될 때마다 호출됩니다.
  • builder: 상태 변화에 따라 UI를 업데이트합니다. 상태가 변경될 때마다 호출되어 새로운 위젯을 반환합니다.