민프

[Flutter] BLoC - 2. 카운터 앱에 간단하게 적용해보기 (with Cubit) 본문

[Flutter]

[Flutter] BLoC - 2. 카운터 앱에 간단하게 적용해보기 (with Cubit)

민프야 2023. 10. 20. 12:02

이번 포스팅은 BLoC 패턴을 적용한 카운터 앱에 간단하게 적용해보겠습니다.

 

참고 예제는 공식홈페이지 예제를 참고하였습니다.

https://bloclibrary.dev/#/fluttercountertutorial

 

Bloc State Management Library

Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials.

bloclibrary.dev

 

해당 예제에서는 Cubit이라는 개념이 등장하는데 간단하게 BLoC와 장단점을 비교해보겠습니다. 

Cubit의 장점

  1. 간결성: Cubit은 간단하게 상태와 상태를 변경하는 함수만 정의하면 됩니다. 반면 Bloc은 상태, 이벤트, 이벤트 핸들러 구현을 모두 정의해야 합니다. 따라서 Cubit은 이해하기 쉽고 코드가 더 간결합니다.
  2. 직관성: Cubit에서는 이벤트를 별도로 정의할 필요 없이 함수가 이벤트처럼 작동합니다. 상태 변경을 트리거하려면 Cubit 내의 어디에서나 emit을 호출하기만 하면 됩니다.

Bloc의 장점

  1. 추적 가능성: Bloc의 주요 장점 중 하나는 상태 변경의 순서와 그러한 변경을 유발한 정확한 원인을 알 수 있다는 것입니다. 애플리케이션의 중요한 상태에 대해 이벤트 기반 접근 방식을 사용하면 모든 이벤트와 상태 변경을 캡처할 수 있습니다.
  2. 고급 이벤트 변환: Bloc은 buffer, debounceTime, throttle 등의 반응형 연산자를 활용할 필요가 있을 때 Cubit보다 뛰어납니다. Bloc은 이벤트 싱크를 가지고 있어 이벤트의 유입을 제어하고 변환할 수 있습니다.

 

프로젝트 구조

├── lib
│   ├── app.dart : 앱 그자체
│   ├── counter : 카운터 기능으로 카테고리 구분
│   │   ├── counter.dart : Counter Feature의 모든 부분을 공개함으로써 
							기능(cubit), UI(view,page)에 관련 된 내용을 export
│   │   ├── cubit
│   │   │   └── counter_cubit.dart : Counter 기능 정의
│   │   └── view
│   │       ├── counter_page.dart : CounterCubit 인스턴스를 생성하고 이를 CounterView에 제공하는 역할을 합니다.
│   │       ├── counter_view.dart : UI 렌더링
│   │       └── view.dart : counter View에 대한 모든 부분을 공개함으로써 UI(view, page)를 export 
│   ├── counter_observer.dart : 앱의 상태 변화를 실시간으로 모니터링을 할 수 있음
│   └── main.dart : Root 위젯 설정 및 CounterObserver 설정
├── pubspec.lock
├── pubspec.yaml

 

Flutter Pagekage 추가

pubspec.yaml
--------------
dependencies:
 	flutter:
    		sdk: flutter
	flutter_bloc: ^8.1.3
  	bloc: ^8.1.0

 

 

소스코드

- BLoC Observer : lib/counter_observer.dart

counter_observer는 애플리케이션의 모든 상태 변화를 관찰하는 역할을 합니다. 

import 'package:bloc/bloc.dart';

/// {@template counter_observer}
/// [BlocObserver] for the counter application which
/// observes all state changes.
/// {@endtemplate}

// CounterObserver 클래스로써 
// BlocObserver를 확장하여 애플리케이션의 모든 상태 변경을 관찰합니다.
class CounterObserver extends BlocObserver {
  /// {@macro counter_observer}
  const CounterObserver();

  // BlocBase의 상태 변경을 관찰하는 메서드입니다. 상태가 변경될 때마다 이 메서드가 호출됩니다.
  @override
  void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
    super.onChange(bloc, change);
    
    print('${bloc.runtimeType} $change');
  }
}

 

- main.dart

import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_counter/app.dart';
import 'package:flutter_counter/counter_observer.dart';

void main() {
  Bloc.observer = const CounterObserver();
  runApp(const CounterApp());
}

Flutter 앱의 진입점인 main.dart 파일의 내용을 설정하는 부분입니다.

여기서는 CounterObserver를 초기화하고 CounterApp 위젯을 실행하여 앱을 시작합니다.

 

 

 - CounterApp:  lib/app.dart

CounterApp이 될 것이며 을 (를) MaterialApp지정하고 있습니다 .

import 'package:flutter/material.dart';
import 'package:flutter_counter/counter/counter.dart';

/// {@template counter_app}
/// A [MaterialApp] which sets the `home` to [CounterPage].
/// {@endtemplate}
class CounterApp extends MaterialApp {
  /// {@macro counter_app}
  const CounterApp({super.key}) : super(home: const CounterPage());
}

 

- CounterPage : lib/counter/view/counter_page.dart

CounterPage 위젯을 정의하는 부분입니다.

CounterPage는 StatelessWidget을 확장하며,

주로 CounterCubit 인스턴스를 생성하고 이를 CounterView에 제공하는 역할을 합니다.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_counter/counter/counter.dart';

/// {@template counter_page}
/// A [StatelessWidget] which is responsible for providing a
/// [CounterCubit] instance to the [CounterView].
/// {@endtemplate}
class CounterPage extends StatelessWidget {
  /// {@macro counter_page}
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: const CounterView(),
    );
  }
}

 

- Counter Cubit : lib/counter/cubit/counter_cubit.dart 

CounterCubit 클래스는 앱의 상태를 관리하는 로직을 포함하고 있으며,

 increment와 decrement 메서드를 통해 상태를 변경할 수 있습니다. 

이러한 상태 변경은 CounterView와 같은 위젯에서 UI를 업데이트하는 데 사용됩니다.

 

import 'package:bloc/bloc.dart';

/// {@template counter_cubit}
/// A [Cubit] which manages an [int] as its state.
/// {@endtemplate}
class CounterCubit extends Cubit<int> {
  /// {@macro counter_cubit}
  CounterCubit() : super(0);

  /// Add 1 to the current state.
  void increment() => emit(state + 1);

  /// Subtract 1 from the current state.
  void decrement() => emit(state - 1);
}

각 함수는 emit을 통해 상태를 업데이트하고, 이 변경이 BLoC or Cubit을 구독하는 위젯에 알려집니다. 

 

 

- Counter View : lib/counter/view/counter_view.dart

현재 카운트를 렌더링하고 카운트를 증가/감소시키는 두 개의 FloatingActionButton을 렌더링하는 역할을 합니다.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_counter/counter/counter.dart';

/// {@template counter_view}
/// A [StatelessWidget] which reacts to the provided
/// [CounterCubit] state and notifies it in response to user input.
/// {@endtemplate}
class CounterView extends StatelessWidget {
  /// {@macro counter_view}
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, state) {
            return Text('$state', style: textTheme.displayMedium);
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            key: const Key('counterView_increment_floatingActionButton'),
            child: const Icon(Icons.add),
            onPressed: () => context.read<CounterCubit>().increment(),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            key: const Key('counterView_decrement_floatingActionButton'),
            child: const Icon(Icons.remove),
            onPressed: () => context.read<CounterCubit>().decrement(),
          ),
        ],
      ),
    );
  }
}

 

- Barrel : lib/counter/view/view.dart

이 부분은 Flutter 프로젝트에서 "barrel" 파일을 사용하여 모듈의 공개 인터페이스를 관리하는 방법을 설명합니다. 

Barrel 파일은 특정 디렉토리의 모든 export를 한 곳에 모아다른 파일에서 이들을 쉽게 import할 수 있게 해줍니다.

export 'counter_page.dart';
export 'counter_view.dart';

따라서 view.dart 파일은 counter view의 모든 공개 부분을 export합니다

 

lib/counter/counter.dart

export 'cubit/counter_cubit.dart';
export 'view/view.dart';

counter.dart 파일은 counter feature의 모든 공개 부분을 export 하고,

cubit/counter_cubit.dart와 view/view.dart를 export하여,

이들을 사용하는 다른 파일에서는 단일 import를 통해 필요한 모든 부분에 접근할 수 있게 해줍니다.

 

Page하나를 만들떄의 과정을 정리 해보면

Page를 만들기전에 Cubit, View을 만들어야하고

Cubit을 만들기전에 필요한 경우 State가 만들어져있어야하고

 

State가 만들어져있으면 ->

Cubit에 Import해서 사용하고 ->

이걸가지고 Page에서
BlocProvider에 Cubit을 넣어주고

child에 View를 넣어주면 동작한다. 

 


참고링크

https://bloclibrary.dev/#/fluttercountertutorial

https://bloclibrary.dev/#/coreconcepts?id=cubit-vs-bloc

 

Comments