[Flutter] Bloc <> Other Bloc, Bloc 간 통신 방법

Tyger·2023년 7월 8일
2

State Management

목록 보기
14/14

Bloc <> Other Bloc, Bloc 간 통신 방법

flutter_bloc | Flutter Packages

예제로 배워보는 BLoC Pattern이란 ?
Count App With BLoC
Count App With Cubit

이번 글에서는 Bloc 간 통신 방법에 대해서 알아보도록 하자.

Bloc 사용시 Bloc과 Other Bloc 간을 호출하기 위해 잘 못 사용하는 경우가 있어 한 번 살펴보려고 한다.

해당 방법은 100% 맞다고 볼 수는 없고, 좋은 방법이 있다면 댓글 남겨 주시길 바란다.

Bloc Pattern을 사용할 때에 난감하고 헷갈리는 부분이 바로 BuildContext가 필요하다는 부분이다. 이런 부분을 해소하기 위한 목적으로 DI(Dependency Injection), Service Locator를 사용하기도 하고, 저 또한 DI를 사용하는 경우가 많아 context에서 보다 자유롭게 블록 간 통신을 서로 시키기도 한다.

하지만 다른 Bloc을 호출하는 부분이 Bloc 내부에 있으면 안된다. Service Locator를 사용하면 context 없이도 Bloc을 생성하고 소비할 수 있게 되는데, 그렇다고 해도 Bloc 내부에서의 호출은 사용하시면 안된다.

Bloc 간 호출을 통한 통신을 서로 하기 위해서는 BlocListener를 활용해주면 좋다.

Flutter

BlocListener를 사용하여 Bloc의 상태 변화를 수신하여야 하는데, 간단한 예제를 사용해서 살펴보도록 하자.

dependencies

dependencies:
	flutter_bloc: ^8.1.3

먼저 한 페이지 영역에 2개의 위젯을 위 아래로 배치하여 각각의 Bloc으로 카운트 값을 생성해 줄 예정이다.

B 영역을 터치하여 B의 상태를 증가시킬 때에 10증가할 때마다 A의 상태를 1씩 증가해주는 간단한 예제이다.

image.jpg1image.jpg2

UI 코드이다.

BlocProvider<ABloc>(
      create: (_) => ABloc(),
      child: BlocBuilder<ABloc, int>(
        builder: (context, state) {
          return Scaffold(
            body: Column(
              children: [
                Container(
                  width: MediaQuery.of(context).size.width,
                  height: 200,
                  color: Colors.deepOrange,
                  child: Center(
                    child: Text(
                      "A State : $state",
                      style: const TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                          fontSize: 30),
                    ),
                  ),
                ),
                BlocProvider<BBloc>(
                  create: (_) => BBloc(),
                  child: BlocConsumer<BBloc, int>(
                    listener: (context, state) {},
                    builder: (context, state) {
                      return GestureDetector(
                        onTap: () {},
                        child: Container(
                          width: MediaQuery.of(context).size.width,
                          height: 200,
                          color: Colors.deepPurple,
                          child: Center(
                            child: Text(
                              "B State : $state",
                              style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                  color: Colors.white,
                                  fontSize: 30),
                            ),
                          ),
                        ),
                      );
                    },
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );

Bloc을 생성하도록 하자. ABloc과 BBloc의 코드는 동일하다.

Increment 이벤트를 사용해서 State를 1씩 증가해주는 이벤트만 사용하도록 하겠다.

class ABloc extends Bloc<AEvent, int> {
  ABloc() : super(0) {
    on<AIncrementEvent>((event, emit) => emit(state + 1));
  }
}

abstract class AEvent {}

class AIncrementEvent extends AEvent {}
import 'package:flutter_bloc/flutter_bloc.dart';

class BBloc extends Bloc<BEvent, int> {
  BBloc() : super(0) {
    on<BIncrementEvent>((event, emit) => emit(state + 1));
  }
}

abstract class BEvent {}

class BIncrementEvent extends BEvent {}

자 이제 B State 영역의 위젯의 터치 이벤트를 등록하도록 하자. B의 상태를 증가시켜 주는 이벤트를 작동시켜 값이 1씩 증가하는지 확인해 보자.

onTap: () => context.read<BBloc>().add(BIncrementEvent()),

B의 상태가 10씩 증가될 때마다 A의 상태를 1씩 증가시켜야 한다면, 어떻게 해야될까 ?

이 때에 사용할 수 있는 기능이 B의 상태 변화를 수신할 수 있는 Listener이다. Listener는 Bloc의 State가 변경되면 무조건 호출되게 되어있어서 해당 리스너에 조건을 넣어 사용해야 한다.

물론 계속 수신을 받아야 하는 상황이라면 Listener만 사용해도 된다.

이제 Listener의 수신 조건을 넣어주기 위해 ListenWhen을 사용해서 B의 상태가 10씩 증가될 때에 작동될 수 있도록 해주도록 하자.

previous는 이전 State이고, current는 변경된 State를 가지고 있다. 우리는 현재의 상태가 10씩 증가된 것만 확인하면 되기에, current 상태를 10으로 나눈 몫이 0인 경우를 사용해 보도록 하겠다.

listenWhen: (previous, current) => current % 10 == 0,

이제 Listener 부분에 ABloc의 Increment 이벤트를 작동시켜 주도록 하자. 정상적으로 작동하는 것을 확인할 수 있다.

 listener: (context, state) => context.read<ABloc>().add(AIncrementEvent()),
image.jpg1image.jpg2

이번에는 API로 무료 이미지를 호출 받아 보여주는 화면을 2개의 영역으로 생성해서, 각각의 Bloc의 상태를 만들어 주도록 하겠다.

ABloc이 페이지 진입 시 API를 호출하고, 데이터를 정상적으로 받아오면 ABloc의 State를 변경시켜, 리스너를 통해 BBloc을 호출해 보자.

Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            BlocConsumer<ABloc, List<String>?>(
              listener: (context, state) {
                ...
              },
              builder: (context, state) {
                return _form(
                  title: "A Area",
                  images: state,
                );
              },
            ),
            BlocBuilder<BBloc, List<String>?>(builder: (context, state) {
              return _form(
                title: "B Area",
                images: state,
              );
            }),
          ],
        ),
Column _form({
    required String title,
    required List<String>? images,
  }) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.only(left: 20, top: 40, bottom: 12),
          child: Text(
            title,
            style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
        ),
        if (images == null) ...[
          const SizedBox(
            height: 120,
            child: Center(child: CircularProgressIndicator()),
          )
        ],
        if (images != null) ...[
          SizedBox(
            height: 120,
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: [
                  ...List.generate(
                      images.length,
                      (index) => Padding(
                            padding: const EdgeInsets.only(left: 20),
                            child: Container(
                              height: 120,
                              width: 100,
                              color: const Color.fromRGBO(245, 245, 245, 1),
                              child: Image.network(
                                images[index],
                                fit: BoxFit.cover,
                              ),
                            ),
                          ))
                ],
              ),
            ),
          )
        ],
      ],
    );
  }

먼저 ABloc은 Bloc이 소비될 때에 자동적으로 AEvent를 호출할 수 있도록 생성해 주었다.

class ABloc extends Bloc<AEvent, List<String>?> {
  ABloc() : super(null) {
    on<AEvent>(_init);
    add(AEvent());
  }
  Future<void> _init(AEvent event, Emitter<List<String>?> emit) async {
    await Future.delayed(const Duration(seconds: 1), () async {
      List<String>? _result = await ImageRepository.instance.fetch(pageNo: 0);
      emit(_result);
    });
  }
}

class AEvent {}

BBloc도 동일한 구조를 가지고 있지만, 호출 시점은 ABloc의 상태가 변경될 때에 호출할 예정이다.

class BBloc extends Bloc<BEvent, List<String>?> {
  BBloc() : super(null) {
    on<BEvent>(_init);
  }

  Future<void> _init(BEvent event, Emitter<List<String>?> emit) async {
    await Future.delayed(const Duration(seconds: 2), () async {
      List<String>? _result = await ImageRepository.instance.fetch(pageNo: 3);
      emit(_result);
    });
  }
}

class BEvent {}

listener 부분에서 state가 변경되어 데이터가 정상적으로 세팅되면, Null이 아닌 상태로 변경되기 때문에, Null이 아닌 경우 BBloc을 호출할 수 있도록 해주었다.

listener: (context, state) {
	if (state != null) {
		context.read<BBloc>().add(BEvent());
	}
}

마무리

간단하게 Bloc에서 다른 Bloc을 호출하는 방법에 대해서 살펴보았다.

생각만큼 어렵거나 하지 않고 오히려 심플한 구조로 개발을 할 수 있다. 하지만 Bloc에 의존되는 상태가 많아지면 많아질 수록 매우 복잡하게 State 관리를 해야하기 때문에, Bloc을 각각의 기능에만 집중될 수 있도록 하여 통신을 하는 형태로 개발할 수도 있다.

Bloc을 처음 접할 때는 Listener, When 등의 기능이 잘 이해되지 않고, 개념이 조금 어려웠는데 자주 사용하다보니, 없으면 엄청 불편해진다.

Get, Provider도 각자의 매력이 있는 충분히 좋은 상태 관리 방법이지만, 함수를 리턴해서 처리하거나 코드 블록 안에서 처리해야 하는 부분이 나중에 디버깅을 어렵게 하는 것 같다.
이런 면에서도 Bloc의 강점이 보인다.

다음에도 Bloc에 대해서 좀 더 복잡한 예제를 준비해서 이해하기 쉽게 글을 작성해 보겠다.

profile
Flutter Developer

0개의 댓글