[Flutter] Bloc 예제

향신료·2023년 10월 4일
0

Flutter의 상태관리

목록 보기
2/4

이전에 Bloc에 대한 기초 정보를 알아보았으니, 더욱 깊게 알아보기 위해

기초 Bloc 사용법을 익히기 위해 Bloc, State, Event 파일로 나누어진 + , - 카운터 앱 예제를 만들어보도록 하겠습니다.






📍 왜 Bloc, State, Event 파일로 나뉘나요?



구조화와 관리를 위한 이유로 앱이 커지고 복잡해지는 경우를 위한 이유입니다.


이러한 분리된 구조를 사용하면 각 파일은 자신만의 역할을 담당하므로 앱의 로직이 명확하게 분리되며 각 역할에 맞게 구성되므로 코드의 가독성, 앱의 개발 및 유지 관리가 훨씬 쉬워집니다. 또한 다른 컴포넌트 간의 결합도를 낮추어 앱의 확장성을 향상시킬 수 있습니다.





💡 그럼 각 파일은 무슨 역할을 담당하나요?



Bloc (비즈니스 로직 컴포넌트)

Bloc 파일은 비즈니스 로직을 정의하고 상태와 이벤트를 관리합니다.

이벤트를 받아 상태를 업데이트하고, 특정 이벤트가 발생할 때 상태가 어떻게 변경되는지를 정의합니다. 이러한 역할을 통해 앱의 동작을 조율하고 데이터 흐름을 관리합니다.


State (상태)

State 파일은 앱의 현재 상태를 정의합니다.

UI에 표시되는 데이터나 화면의 상태와 관련된 정보를 정의합니다, 본 글에서 제작할 카운터 앱에서 현재 카운터 값 등을 포함할 수 있습니다.


Event (이벤트)

Event 파일은 앱에서 발생하는 이벤트나 액션을 정의합니다.

주로 사용자 동작 또는 시스템 이벤트에 관한 정보를 정의합니다, 카운터 앱에서는 "+" 버튼을 누르는 이벤트나 "-" 버튼을 누르는 이벤트를 정의할 수 있습니다.







카운터 앱 제작


dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: 

우선, flutter_bloc 패키지를 프로젝트에 추가하기 위해 pubspec.yaml 파일에 위와 같은 코드를 추가하고 패키지를 가져옵니다.





counter_event

// 이벤트 정의
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

카운터의 증가와 감소에 관한 이벤트를 정의해 줍니다.

이때 abstract Class를 만들어 일관성을 유지하고, 이후 이벤트 확장을 편리하게 만들어줍니다.

또한 기능에 따라 이벤트들이 특정 규칙을 따르도록 만들 수 있습니다.





counter_state

// 상태 정의
class CounterState {
  final int count;

  CounterState({this.count = 0});

  CounterState copyWith({
    int? count,
  }) {
    return CounterState(
      count: count ?? this.count,
    );
  }
}

카운트에 필요한 count 상태를 정의해 줍니다.

copyWith를 정의함으로써 불변성을 유지하면서 객체의 일부 필드를 업데이트할 수 있도록 해줍니다.

해당 메서드를 사용하여 새로운 상태를 생성할 때, 변경하려는 count 값을 전달하면 해당 필드만 변경되며 만약 count 값을 전달하지 않으면 현재의 count 값은 그대로 유지됩니다.





counter_bloc

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

// 이벤트 별 동작 정의
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState()) {
    on<IncrementEvent>(_handleIncrementEvent);
    on<DecrementEvent>(_handleDecrementEvent);
  }

  void _handleIncrementEvent(
    IncrementEvent event,
    Emitter<CounterState> emit,
  ) {
    print('IncrementEvent 발생');
    emit(state.copyWith(count: state.count + 1));
  }

  void _handleDecrementEvent(
    DecrementEvent event,
    Emitter<CounterState> emit,
  ) {
    print('DecrementEvent 발생');
    emit(state.copyWith(count: state.count - 1));
  }
}

Bloc<CounterEvent, CounterState>

각 이벤트와 상태 클래스를 상속하여 카운터 앱의 비즈니스 로직을 정의합니다.



CounterBloc() : super(CounterState())

초기 상태를 기본 초기화 상태로 적용해 줍니다.



on<IncrementEvent>(_handleIncrementEvent)

IncrementEvent 이벤트가 발생했을 때 _handleIncrementEvent 메서드를 호출하도록 등록합니다.



emit(state.copyWith(count: state.count + 1))

현재 상태를 변경하지 않고 현 상태의 카운터 값을 1 증가시킨 새로운 상태를 반환하고 새로운 상태는 UI로 전달해 카운터 값이 업데이트합니다.





📍  on ? emit ?


on

Bloc 클래스의 생성자 내에서 호출되며, 이벤트와 핸들러 메서드를 연결하는 역할을 합니다.

핸들러 메서드는 이벤트가 발생했을 때 실행되며, 이벤트와 현재 상태에 따라 특정 작업을 수행하고 새로운 상태를 업데이트하는 역할을 합니다.



emit

기본적으로 상태를 변경하지 않고 새로운 상태를 반환합니다.

해당 메서드를 호출하면 새로운 상태는 Bloc의 상태 스트림에 추가되고, 이 스트림을 구독하고 있는 위젯이나 컴포넌트가 새로운 상태를 수신하여 UI를 업데이트하는 역할을 합니다.







호출 적용 화면


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

  @override
  Widget build(BuildContext context) {
    return ScaffoldLayout(
      child: BlocProvider(
        create: (context) => CounterBloc(),
        child: Center(
          child: BlocBuilder<CounterBloc, CounterState>(
            builder: (context, state) {
              return Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    '${state.count}',
                    style: const TextStyle(fontSize: 50),
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      FloatingActionButton(
                        onPressed: () {
                          context.read<CounterBloc>().add(DecrementEvent());
                        },
                        child: const Icon(Icons.remove),
                      ),
                      FloatingActionButton(
                        onPressed: () {
                          context.read<CounterBloc>().add(IncrementEvent());
                        },
                        child: const Icon(Icons.add),
                      ),
                    ],
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}


BlocProvider(create: (context) => CounterBloc(), ~

CounterBloc을 생성하고 이를 현재 화면의 context에서 사용할 수 있게 합니다.



BlocBuilder<CounterBloc, CounterState>

CounterBloc의 상태를 감시하고, 상태가 변경될 때마다 UI를 업데이트합니다.

이때, builder 함수 내에서 현재 상태를 전달하고 UI를 빌드 합니다.



context.read<CounterBloc>().add(IncrementEvent())

IncrementEvent 를 발생시켜, CounterBloc 내에서 상태의 count 값이 증가하고 UI가 변경됩니다.



context.read<CounterBloc>().add(DecrementEvent())

DecrementEvent 를 발생시켜, CounterBloc 내에서 상태의 count 값이 감소하고 UI가 변경됩니다.





💡 BlocProvider? BlocBuilder?



BlocProvider

앱의 위젯 트리 중간에 Bloc 인스턴스를 제공하고 관리하기 위한 Widget

  • 지연 생성 가능 (lazy)
  • 하위 계층 위젯 접근 가능
  • Bloc 생성 후 메모리 반환은 자동으로 진행


BlocBuilder

BlocProvider를 이용해 생성된 Bloc를 사용할때 쓰는 widget

  • Bloc 옵션 미설정으로 사용시 현 context로 Bloc를 찾아 변화를 감지 합니다.
    Bloc를 지정하는 케이스의 경우는 특이사항 케이스에서만 사용하기를 권장 합니다. ex. 다이얼로그
  • BuildWhen 옵션을 통해 필요한 조건일때만 변화를 줄 수 있습니다.













해당 코드들의 전문은 Github 레파지토리에서 확인 가능합니다!






공식 사이트
하위 사이트에서 더욱 다양한 예제를 볼 수 있으니 참고하시면 좋을 듯 합니다!

Bloc State Management Site

profile
드문드문 기초 정보를 올리는 블로그

0개의 댓글