bloc

devkwon·2023년 2월 20일
0
post-thumbnail

왜? Bloc patter인가?

플러터는 정적인 표현과 동적인 표현이 있다. 보통 정적인 부분은 stateless 동적인 부분은 stateful widget으로 구현한다. stateful widget의 경우 setState를 통해 상태 변경을 할 수 있었다. 허나 이런 방식은 복잡한 위젯 트리가 만들어졌을 때 사용하기 복잡하다. 만약 복잡하게 구현을 하더라도 불필요한 하위 위젯까지 rebuild를 해야하므로 성능 저하도 발생한다. 또한 위젯이 추가되었을 때 트리를 모두 다 수정해야하는 경우가 발생될 수도 있어 유지보수에도 좋지 않다.

이러한 문제점을 해결하기 위해 만들어진게 Bloc(Bussiness Logic Component)이다.
이를 통해 UI요소는 Bussiness Logic과 무관하게 화면만 보여주면 되게 개발할 수 있는 환경을 갖추게 되었다.

Bloc을 사용하게 되면 구조가 다음과 같이 변한다.

이전에 setState를 활용한 상태 관리에서는 상관 없는 하위 위젯들까지 build를 해야했지만, bloc을 사용하게 되면 필요한 부분만 build를 한다. 최하단에 새로운 제어를 받아야하는 위젯이 추가되어도 bloc 상태의 값을 넘겨주기만 하면 된다.

  1. 뷰 영역과 비지니스 영역을 쉽게 구분할 수 있다.

  2. 테스트 하기 쉽고, 재사용을 가능하게 해준다.

  3. 이벤트 트래킹을 통합적으로 관리할 수 있다.

  4. 많은 개발자들이 하나의 코드 베이스로 일을 처리할 수 있다.

bloc에도 단점은 있는데, 패턴을 이해하는데 많은 지식이 필요하다는 것과, 각각의 상태를 관리하기 위해서 파일들의 개수가 많아진다는 단점이 있다.

Stream

BlocProvider

bloc 클래스는 context에 등록을 해야 사용이 가능한데 이를 등록해주는 것이 BlocProvider이다.

lass BlocProviderPage extends StatelessWidget {
  const BlocProviderPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => SampleBloc(),
      lazy: false,
      child: SamplePage(),
    );
  }
}

lazy 옵션을 false를 할 경우 바로 생성. true(default)로 할 경우 처음 호출될 때 생성.

MultiBlocProvider

class _MultiBlocProviderPageState extends State<MultiBlocProviderPage> {
  @override
  Widget build(BuildContext context) {
    // return BlocProvider(
    //   create: (context) => SampleBloc(),
    //   child: BlocProvider(
    //     create: (context) => SampleSecondsBloc(),
    //     child: SamplePage(),
    //   ),
    // );
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: ((context) => SampleBloc())),
        BlocProvider(create: ((context) => SampleSecondsBloc())),
      ],
      child: SamplePage(),
    );
  }
}

두 가지 방식 모두 결과는 같으나 multiblocprovider가 가독성이 더 좋다.

BlocBuilder

child: BlocBuilder<SampleBloc, int>( // <bloc_state, state_type>
          buildWhen: (previous, current) { // state 조건 추가
            return current > 10; 
          },
          builder: (context, state) {
            return Text(
              'index : $state',
              style: const TextStyle(fontSize: 70),
            );
          },
        ),
      ),

buildWhen을 추가하면 해당 조건을 만족하는 변경에 대해 빌더를 호출하여 값을 받는다. 해당 조건을 만족하지 않는 업데이트는 무시한다.

late SampleBloc sampleBloc;
  void _showMessage(BuildContext context) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext _) {
        return AlertDialog(
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
          title: const Text('Title'),
          content: BlocBuilder<SampleBloc, int>(
            bloc: sampleBloc, // bloc 지정
            builder: (context, state) {
              return Text(state.toString());
            },
          ),

bloc 옵션을 사용하여 값을 받을 bloc을 지정할 수 있는데 권장되지 않는 방법이며, dialog 같이 context에서 접근할 수 없는 경우에 사용한다.

RepositoryProvider

class _RepositoryProviderPageState extends State<RepositoryProviderPage> {
  @override
  Widget build(BuildContext context) {
    return RepositoryProvider(
      create: (context) => RepositorySample(),
      child: BlocProvider(
        create: (context) => SampleBlocDI(context.read<RepositorySample>()),
        child: SamplePage(),
      ),
    );
  }
}

BlocSelector

Bloc의 상태중 필요한 부분만 선택적으로 필터링하여 변경에 도움을 주는 Widget

BlocSelector<BlocSelectorBloc, BlocSelectorState, bool>(
              selector: (state) => state.changeState,
              builder: (context, state) {
                print('selector builder');
                return Icon(
                  Icons.favorite,
                  color: state ? Colors.red : Colors.grey,
                  size: 50,
                );
              },
            ),

BlocListener

상태변화에 따른 이벤트만 처리가 필요할 때 사용되는 Widget
단, child 위젯의 경우 rebuild가 발생되지 않는다. 특정 상태가 변경되었을 때 팝업 메세지를 띄우거나 Bloc 간 통신이 필요할 때 사용한다.

               child: BlocListener<SampleBloc, int>(
          listenWhen: (previous, current) => current > 5,
          listener: (context, state) {
            _showMessage(context);
          },
          child: Text( // rebuild 되지 않음.
                  state.toString(),
                  style: const TextStyle(fontSize: 70),
           )

listenWhen으로 리스너를 호출시킬 조건을 걸 수 있다.
참고로 child가 rebuild 되지 않는다고 해서 상태값이 변하지 않는다는 것은 아니다. 상태 값은 이벤트에 따라 변하지만 child의 build만 되지 않는 것이다.

BlocConsumer

BlocBuilder와 BlocListener를 합쳐 놓은 위젯.
이벤트도 처리하면서 동시에 화면도 변경을 해줘야 할 때 사용.
buildWhen, listenWhen을 통해 일정 시점에만 작동하는 변경 및 이벤트 처리를 할 수 있다.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocConsumer<SampleBloc, int>(
          listenWhen: (previous, current) => current > 5,
          listener: (context, state) {
            _showMessage(context);
          },
          buildWhen: (previous, current) => current % 2 == 0,
          builder: (context, state) => Text(
            state.toString(),
            style: const TextStyle(fontSize: 70),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<SampleBloc>().add(AddSampleEvent());
        },
      ),
    );
  }

0개의 댓글