반응형
1. Bloc이란?
*bloc : Business Logic Component
bloc은 비즈니스 로직과 프레젠테이션 영역을 분리하기 위해 도입된 패턴이다. 계층을 분리함으로써 코드의 재사용성을 높이고, 테스트를 더 쉽게 진행할 수 있게 한다.
bloc은 UI 이벤트를 받아 State를 변경한다.
2. Bloc 패턴 작성과 사용
a. bloc 생성
이벤트와 bloc을 작성
abstract class CounterEvent {}
// event
class CounterIncrementPressed extends CounterEvent {}
// bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
}
처리하고자 하는 이벤트에 대한 이벤트 핸들러를 등록
emit() : 상태를 변화시키는 메서드
abstract class CounterEvent {}
// event
class CounterIncrementPressed extends CounterEvent {}
// bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
// event handler
on<CounterIncrementPressed>((event, emit) {
emit(state + 1);
});
}
}
b. bloc 사용
bloc 인스턴스를 생성하고, add() 메서드를 통해 만들어뒀던 *CounterIncrementPressed()*를 인자로 넘긴다.
그러면 이 이벤트가 이벤트핸들러를 통해 상태를 변화시키는 트리거 역할을 한다.
// basic usage
Future<void> main() async {
final bloc = CounterBloc();
print(bloc.state); // 0
bloc.add(CounterIncrementPressed());
await Future.delayed(Duration.zero);
print(bloc.state); // 1
await bloc.close();
}
// stream usage
Future<void> main() async {
final bloc = CounterBloc();
final subscription = bloc.stream.listen(print); // 1
bloc.add(CounterIncrementPressed());
await Future.delayed(Duration.zero);
await subscription.cancel();
await bloc.close();
}
bloc 상태 관찰 방법
- onChange, onTransition 사용
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
super.onTransition(transition);
print(transition);
}
}
void main() {
CounterBloc()
..add(CounterIncrementPressed())
..close();
}
console
Transition { currentState: 0, event: Increment, nextState: 1 }
Change { currentState: 0, nextState: 1 }
**onTransition()*이 *onChange()*보다 먼저 실행됨
- state를 관찰하기 위한 또 다른 방법으로, Observer를 직접 커스텀할 수도 있다.
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType} $transition');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}
void main() {
Bloc.observer = SimpleBlocObserver();
CounterBloc()
..add(CounterIncrementPressed())
..close();
}
Console
Transition { currentState: 0, event: Increment, nextState: 1 }
CounterBloc Transition { currentState: 0, event: Increment, nextState: 1 }
Change { currentState: 0, nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
*Oberver의 *onChange()*는 bloc의 onChange() 이후에 호출됨.
3. bloc 사용 예시
아래 문서를 꼼꼼히 읽어보면 감을 잡을 수 있다.
bloc을 활용하여 타이머 앱 만들기 : https://bloclibrary.dev/#/fluttertimertutorial
요약하자면 크게 4가지를 만들어야 한다. State, Event, Bloc, 화면 UI
- State(타이머는 어떤 상태를 가질 수 있는가)
part of 'timer_bloc.dart';
abstract class TimerState extends Equatable {
const TimerState(this.duration);
final int duration;
@override
List<Object> get props => [duration];
}
class TimerInitial extends TimerState {
const TimerInitial(super.duration);
@override
String toString() => 'TimerInitial { duration: $duration }';
}
class TimerRunPause extends TimerState {
const TimerRunPause(super.duration);
@override
String toString() => 'TimerRunPause { duration: $duration }';
}
class TimerRunInProgress extends TimerState {
const TimerRunInProgress(super.duration);
@override
String toString() => 'TimerRunInProgress { duration: $duration }';
}
class TimerRunComplete extends TimerState {
const TimerRunComplete() : super(0);
}
- Event(어떤 이벤트가 발생할 수 있는가 ⇒ 상태 변경을 유발할 이벤트들을 등록해둠)
part of 'timer_bloc.dart';
abstract class TimerEvent extends Equatable {
const TimerEvent();
@override
List<Object> get props => [];
}
class TimerStarted extends TimerEvent {
const TimerStarted({required this.duration});
final int duration;
}
class TimerPaused extends TimerEvent {
const TimerPaused();
}
class TimerResumed extends TimerEvent {
const TimerResumed();
}
class TimerReset extends TimerEvent {
const TimerReset();
}
class TimerTicked extends TimerEvent {
const TimerTicked({required this.duration});
final int duration;
@override
List<Object> get props => [duration];
}
- Bloc (이곳에 이벤트 핸들러를 등록 ⇒ 이벤트 발생 시 타이머를 어떤 상태로 바꿀 것인가)
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(const TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<TimerResumed>(_onResumed);
on<TimerReset>(_onReset);
on<TimerTicked>(_onTicked);
}
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onResumed(TimerResumed resume, Emitter<TimerState> emit) {
if (state is TimerRunPause) {
_tickerSubscription?.resume();
emit(TimerRunInProgress(state.duration));
}
}
void _onReset(TimerReset event, Emitter<TimerState> emit) {
_tickerSubscription?.cancel();
emit(const TimerInitial(_duration));
}
void _onTicked(TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: const TimerRunComplete(),
);
}
}
- 화면 UI (상태 변화를 지속적으로 추적하며 그에 맞는 적절한 화면 보여주기)
import 'package:flutter/material.dart';
import 'package:flutter_timer/app.dart';
void main() => runApp(const App());
import 'package:flutter/material.dart';
import 'package:flutter_timer/timer/timer.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Timer',
theme: ThemeData(
primaryColor: const Color.fromRGBO(109, 234, 255, 1),
colorScheme: const ColorScheme.light(
secondary: Color.fromRGBO(72, 74, 126, 1),
),
),
home: const TimerPage(),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_timer/ticker.dart';
import 'package:flutter_timer/timer/timer.dart';
class TimerPage extends StatelessWidget {
const TimerPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TimerBloc(ticker: Ticker()),
child: const TimerView(),
);
}
}
class TimerView extends StatelessWidget {
const TimerView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Timer')),
body: Stack(
children: [
const Background(),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 100.0),
child: Center(child: TimerText()),
),
Actions(),
],
),
],
),
);
}
}
class TimerText extends StatelessWidget {
const TimerText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final duration = context.select((TimerBloc bloc) => bloc.state.duration);
final minutesStr =
((duration / 60) % 60).floor().toString().padLeft(2, '0');
final secondsStr = (duration % 60).floor().toString().padLeft(2, '0');
return Text(
'$minutesStr:$secondsStr',
style: Theme.of(context).textTheme.headline1,
);
}
}
아래 코드가 핵심인데, BlocBuilder(새 상태에 대한 응답으로 위젯 빌드를 처리하는 Flutter 위젯)을 사용하여 만들어두었던 bloc을 사용.
class Actions extends StatelessWidget {
const Actions({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<TimerBloc, TimerState>(
buildWhen: (prev, state) => prev.runtimeType != state.runtimeType,
builder: (context, state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (state is TimerInitial) ...[
FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () => context
.read<TimerBloc>()
.add(TimerStarted(duration: state.duration)),
),
],
if (state is TimerRunInProgress) ...[
FloatingActionButton(
child: Icon(Icons.pause),
onPressed: () => context.read<TimerBloc>().add(TimerPaused()),
),
FloatingActionButton(
child: Icon(Icons.replay),
onPressed: () => context.read<TimerBloc>().add(TimerReset()),
),
],
if (state is TimerRunPause) ...[
FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () => context.read<TimerBloc>().add(TimerResumed()),
),
FloatingActionButton(
child: Icon(Icons.replay),
onPressed: () => context.read<TimerBloc>().add(TimerReset()),
),
],
if (state is TimerRunComplete) ...[
FloatingActionButton(
child: Icon(Icons.replay),
onPressed: () => context.read<TimerBloc>().add(TimerReset()),
),
]
],
);
},
);
}
}
댓글