[Flutter]BLoC을 이용한 상태관리

한상욱·2024년 9월 11일
0

Flutter

목록 보기
24/26
post-thumbnail

들어가며

이번에는 Flutter에서 BLoC을 이용한 상태관리를 카운터앱 예제를 통해서 알아보도록 하겠습니다.

시작하기

BLoC을 사용하기 위해서는 Cubit, Bloc이라는 개념을 알아야 합니다. 그에 대한 내용은 아래의 글에서 확인할 수 있습니다.
[Flutter]BLoC에 대해서 알아보자

위 글을 자세하게 읽어보았다면 이번 카운터앱 예제는 Cubit으로도 충분히 구현할 수 있다는 것을 이해하실 것입니다. 하지만, Cubit, Bloc을 모두 이용하여 카운터 예제를 완성해보겠습니다.

최종적으로 완성할 UI는 아래와 같습니다.

초기화면에서는 Cubit, Bloc을 이용한 카운터를 볼 수 있으며 카운터를 탭하면 해당 카운터를 증가 또는 감소시킬 수 있는 Detail 뷰로 이동하게 됩니다.

Cubit을 이용해서 만든 카운터

1. Cubit 생성

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increament() => emit(state + 1);

  void decreament() => emit(state - 1);
}

우리의 state는 단순히 정수형 데이터입니다. 따라서, Cubit은 0을 초기 state로 가지며 각 증가 감소를 메소드를 통해서 이루어지도록 할 것입니다.

2. Counter Cubit View & Detail View

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

  
  Widget build(BuildContext context) {
    return Center(
      child: BlocBuilder<CounterCubit, int>(
          builder: (context, state) => Text(
                state.toString(),
                style: const TextStyle(
                    fontSize: 100,
                    color: Colors.black,
                    fontWeight: FontWeight.bold),
              )),
    );
  }
}

Couter Cubit View는 단순하게 현재의 state인 카운터를 보여줄 것입니다. Cubit을 참조하여 UI의 갱신을 위해서 BlocBuilder 위젯을 사용할 수 있습니다.

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

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            BlocBuilder<CounterCubit, int>(builder: (context, state) {
              return Text(
                state.toString(),
                style: const TextStyle(
                    fontSize: 80,
                    color: Colors.black,
                    fontWeight: FontWeight.bold),
              );
            }),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                IconButton(
                    onPressed: context.read<CounterCubit>().increament,
                    icon: const Icon(
                      Icons.add,
                      size: 40,
                      color: Colors.black,
                    )),
                IconButton(
                    onPressed: context.read<CounterCubit>().decreament,
                    icon: const Icon(
                      Icons.remove,
                      size: 40,
                      color: Colors.black,
                    )),
              ],
            )
          ],
        ));
  }
}

Detail View는 실질적인 Cubit의 state의 변화를 조정합니다. 이곳에서 Cubit의 increase(), decrease()메소드를 이용하여 state를 변경시킬 수 있습니다. 마찬가지로 BlocBuilder를 통해 현재의 state의 변화를 감지하면 UI를 갱신시키면 됩니다.

3. Main UI

...
  Widget _cubit() => GestureDetector(
      onTap: () {
        Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => const CounterCubitDetailView()));
      },
      child: const Column(
        children: [
          Text("Cubit"),
          CounterCubitView(),
        ],
      ));
...

자, 이제 UI를 모두 연결시켰으니 Cubit의 의존성을 가장 최상단에서 주입해야 합니다. 우리는 App이 가장 최상단의 위젯이므로 App으로 접근하기 전에 Cubit을 주입하겠습니다.

4. BlocProvider

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

  
  Widget build(BuildContext context) {
    return BlocProvider(
    	create: (context) => CounterCubit(), 
    	child: const MaterialApp(
        	home: App(),
      	),
      ),
    );
  }
}

BlocProvider는 Cubit 또는 Bloc을 주입시켜주는 역할을 합니다. 이제 Cubit을 이용한 카운터 예제가 완성되었습니다.

Bloc을 이용해서 만든 카운터

1. Bloc 생성

import 'package:flutter_bloc/flutter_bloc.dart';

sealed class CounterEvent {}

final class CounterIncreasementEvent extends CounterEvent {}

final class CounterDecreasementEvent extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncreasementEvent>((event, emit) {
      emit(state + 1);
    });

    on<CounterDecreasementEvent>((event, emit) {
      emit(state - 1);
    });
  }
}

Bloc은 Cubit과 다르게 state를 변경시키기 위한 이벤트를 정의해야 합니다. 감소 또는 증가 이벤트를 각각 CounterIncreasementEvent, CounterDecreasementEvent로 선언하고 이는 CounterEvent를 상속시켜 만들겠습니다.

2. Counter Bloc View & Detail View

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

  
  Widget build(BuildContext context) {
    return Center(
      child: BlocBuilder<CounterBloc, int>(
          builder: (context, state) => Text(
                state.toString(),
                style: const TextStyle(
                    fontSize: 100,
                    color: Colors.black,
                    fontWeight: FontWeight.bold),
              )),
    );
  }
}

Cubit과 비슷하게 BlocBuilder를 통해서 UI를 생성할 수 있습니다. 마찬가지로 비슷하게 Detail View를 생성할 수 있습니다.

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

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            BlocBuilder<CounterBloc, int>(builder: (context, state) {
              return Text(
                state.toString(),
                style: const TextStyle(
                    fontSize: 80,
                    color: Colors.black,
                    fontWeight: FontWeight.bold),
              );
            }),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                IconButton(
                    onPressed: () => context
                        .read<CounterBloc>()
                        .add(CounterIncreasementEvent()),
                    icon: const Icon(
                      Icons.add,
                      size: 40,
                      color: Colors.black,
                    )),
                IconButton(
                    onPressed: () => context
                        .read<CounterBloc>()
                        .add(CounterDecreasementEvent()),
                    icon: const Icon(
                      Icons.remove,
                      size: 40,
                      color: Colors.black,
                    )),
              ],
            )
          ],
        ));
  }
}

다만, Cubit과 다르게 메소드를 호출하는 것이 아닌 state 변경을 위한 이벤트를 전달합니다. 해당 이벤트를 감지하여 Bloc의 state를 변경시킵니다.

3. Main UI

...
  Widget _bloc() => GestureDetector(
      onTap: () {
        Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => const CounterBlocDetailView()));
      },
      child: const Column(
        children: [
          Text("Bloc"),
          CounterBlocView(),
        ],
      ));
...

Main UI는 Cubit과 동일하게 생성하였습니다.

4. MultiBlocProvider

Cubit과 더불어 Bloc도 최상단에서 주입해야 합니다. 여러개의 Cubit 또는 Bloc은 MultiBlocProvider를 통해서 주입할 수 있습니다.

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

  
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (context) => CounterCubit()),
        BlocProvider(create: (context) => CounterBloc())
      ],
      child: const MaterialApp(
        home: App(),
      ),
    );
  }
}

이로써, Bloc을 이용한 카운터 예제도 완성이 되었습니다.

BlocObserver

기존에 Cubit 또는 Bloc은 각각 onChange 메소드를 통해서 state의 변화를 관찰할 수 있다고 하였는데요. BlocObserver를 이용하면 모든 state의 변화를 확인할 수 있습니다.

또한, onTransition을 이용하면 Bloc에서 state의 변경을 요청한 이벤트 또한 관찰할 수 있습니다.

class CounterObserver extends BlocObserver {
  const CounterObserver();

  
  void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
    super.onChange(bloc, change);
    debugPrint("${bloc.runtimeType} $change");
  }

  
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    debugPrint("${bloc.runtimeType} $transition");
  }
}

onChange를 이용한 state 변화 관찰

onTransition을 이용한 Bloc의 state 변화 및 이벤트 관찰

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글