Flutter Counter
이 튜토리얼에서는 Bloc 라이브러리를 사용해서 Flutter로 카운터 앱을 만들어 봅니다.

핵심 주제
섹션 제목: “핵심 주제”- BlocObserver로 상태 변화 관찰하기.
- BlocProvider로 하위 위젯에 bloc 제공하기.
- BlocBuilder로 상태 변화에 따라 위젯 다시 그리기.
- Bloc 대신 Cubit 사용하기. 차이점이 뭔가요?
- context.read로 이벤트 추가하기.
프로젝트 설정
섹션 제목: “프로젝트 설정”먼저 새로운 Flutter 프로젝트를 생성합니다.
flutter create flutter_counter그 다음 pubspec.yaml 파일을 아래 내용으로 교체합니다.
name: flutter_counterdescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ">=3.10.0 <4.0.0"
dependencies: bloc: ^9.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0
dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter integration_test: sdk: flutter mocktail: ^1.0.0
flutter: uses-material-design: true의존성을 설치합니다.
flutter pub get프로젝트 구조
섹션 제목: “프로젝트 구조”├── lib│ ├── app.dart│ ├── counter│ │ ├── counter.dart│ │ ├── cubit│ │ │ └── counter_cubit.dart│ │ └── view│ │ ├── counter_page.dart│ │ ├── counter_view.dart│ │ └── view.dart│ ├── counter_observer.dart│ └── main.dart├── pubspec.lock├── pubspec.yaml이 프로젝트는 기능 기반 디렉토리 구조를 사용합니다. 이런 구조를 사용하면 각 기능이 독립적으로 구성되어 프로젝트 확장이 쉬워집니다. 이 예제에서는 카운터 기능 하나만 있지만, 실제 복잡한 앱에서는 수백 개의 기능이 있을 수 있습니다.
BlocObserver
섹션 제목: “BlocObserver”먼저 BlocObserver를 만들어 봅니다. 이걸 사용하면 앱 전체의 상태 변화를 관찰할
수 있습니다.
lib/counter_observer.dart 파일을 생성합니다:
import 'package:bloc/bloc.dart';
/// {@template counter_observer}/// [BlocObserver] for the counter application which/// observes all state changes./// {@endtemplate}class CounterObserver extends BlocObserver { /// {@macro counter_observer} const CounterObserver();
@override void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) { super.onChange(bloc, change); // ignore: avoid_print print('${bloc.runtimeType} $change'); }}여기서는 onChange만 override해서 모든 상태 변화를 확인합니다.
main.dart
섹션 제목: “main.dart”다음으로 lib/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());}방금 만든 CounterObserver를 초기화하고, runApp에 다음에 만들 CounterApp
위젯을 전달합니다.
Counter App
섹션 제목: “Counter App”lib/app.dart 파일을 생성합니다:
CounterApp은 MaterialApp이고 home으로 CounterPage를 지정합니다.
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를 살펴봅니다.
Counter Page
섹션 제목: “Counter Page”lib/counter/view/counter_page.dart 파일을 생성합니다:
CounterPage 위젯은 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
섹션 제목: “Counter Cubit”lib/counter/cubit/counter_cubit.dart 파일을 생성합니다:
CounterCubit 클래스는 두 가지 메서드를 제공합니다:
increment: 현재 상태에 1을 더합니다.decrement: 현재 상태에서 1을 뺍니다.
CounterCubit이 관리하는 상태 타입은 int이고 초기 상태는 0입니다.
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);}다음으로 상태를 사용하고 CounterCubit과 상호작용하는 CounterView를
살펴봅니다.
Counter View
섹션 제목: “Counter View”lib/counter/view/counter_view.dart 파일을 생성합니다:
CounterView는 현재 카운트 값을 표시하고, 카운터를 증가/감소시키는 두 개의
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( 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(), ), ], ), ); }}BlocBuilder로 Text 위젯을 감싸서 CounterCubit 상태가 바뀔 때마다 텍스트를
업데이트합니다. 또한 context.read<CounterCubit>()을 사용해서 가장 가까운
CounterCubit 인스턴스를 찾습니다.
Barrel 파일
섹션 제목: “Barrel 파일”lib/counter/view/view.dart 파일을 생성합니다:
view.dart를 추가해서 counter view의 공개 부분을 export합니다.
export 'counter_page.dart';export 'counter_view.dart';lib/counter/counter.dart 파일을 생성합니다:
counter.dart를 추가해서 counter 기능의 모든 공개 부분을 export합니다.
export 'cubit/counter_cubit.dart';export 'view/view.dart';끝입니다! 프레젠테이션 레이어와 비즈니스 로직 레이어를 분리했습니다.
CounterView는 사용자가 버튼을 누를 때 무슨 일이 일어나는지 모릅니다. 단지
CounterCubit에 알릴 뿐입니다. 마찬가지로 CounterCubit은 상태(카운터 값)가
어떻게 표시되는지 모릅니다. 메서드 호출에 대한 응답으로 새로운 상태를 emit할
뿐입니다.
flutter run으로 앱을 실행하면 기기나 시뮬레이터/에뮬레이터에서 확인할 수
있습니다.
이 예제의 전체 소스 코드(단위 테스트와 위젯 테스트 포함)는 여기에서 확인할 수 있습니다.