콘텐츠로 이동

Flutter Counter

beginner

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

demo

먼저 새로운 Flutter 프로젝트를 생성합니다.

Terminal window
flutter create flutter_counter

그 다음 pubspec.yaml 파일을 아래 내용으로 교체합니다.

pubspec.yaml
name: flutter_counter
description: A new Flutter project.
version: 1.0.0+1
publish_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

의존성을 설치합니다.

Terminal window
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를 만들어 봅니다. 이걸 사용하면 앱 전체의 상태 변화를 관찰할 수 있습니다.

lib/counter_observer.dart 파일을 생성합니다:

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해서 모든 상태 변화를 확인합니다.

다음으로 lib/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 위젯을 전달합니다.

lib/app.dart 파일을 생성합니다:

CounterAppMaterialApp이고 home으로 CounterPage를 지정합니다.

lib/app.dart
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 위젯은 CounterCubit(다음에 살펴볼 예정)을 생성하고 CounterView에 제공하는 역할을 합니다.

lib/counter/view/counter_page.dart
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(),
);
}
}

lib/counter/cubit/counter_cubit.dart 파일을 생성합니다:

CounterCubit 클래스는 두 가지 메서드를 제공합니다:

  • increment: 현재 상태에 1을 더합니다.
  • decrement: 현재 상태에서 1을 뺍니다.

CounterCubit이 관리하는 상태 타입은 int이고 초기 상태는 0입니다.

lib/counter/cubit/counter_cubit.dart
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를 살펴봅니다.

lib/counter/view/counter_view.dart 파일을 생성합니다:

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

lib/counter/view/counter_view.dart
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(),
),
],
),
);
}
}

BlocBuilderText 위젯을 감싸서 CounterCubit 상태가 바뀔 때마다 텍스트를 업데이트합니다. 또한 context.read<CounterCubit>()을 사용해서 가장 가까운 CounterCubit 인스턴스를 찾습니다.

lib/counter/view/view.dart 파일을 생성합니다:

view.dart를 추가해서 counter view의 공개 부분을 export합니다.

lib/counter/view/view.dart
export 'counter_page.dart';
export 'counter_view.dart';

lib/counter/counter.dart 파일을 생성합니다:

counter.dart를 추가해서 counter 기능의 모든 공개 부분을 export합니다.

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

끝입니다! 프레젠테이션 레이어와 비즈니스 로직 레이어를 분리했습니다. CounterView는 사용자가 버튼을 누를 때 무슨 일이 일어나는지 모릅니다. 단지 CounterCubit에 알릴 뿐입니다. 마찬가지로 CounterCubit은 상태(카운터 값)가 어떻게 표시되는지 모릅니다. 메서드 호출에 대한 응답으로 새로운 상태를 emit할 뿐입니다.

flutter run으로 앱을 실행하면 기기나 시뮬레이터/에뮬레이터에서 확인할 수 있습니다.

이 예제의 전체 소스 코드(단위 테스트와 위젯 테스트 포함)는 여기에서 확인할 수 있습니다.