개발을 하다보면 여러가지 에러를 마주하게 된다. 기껏 코드를 열심히 짯더니 에러를 뿜어내는 상황을 볼때 마다 눈앞이 깜깜해진다. 정리를 해둔다면 생각해서 하나씩 정리해 보겠다.
우선 이런 에러 메세지가 발생하는 이유는 무엇일까??
발생하는 이유는 bloc 또는 스트림의 생명주기와 관련이 있다.
해당 오류는 주로 닫힌 스트림에 새로운 이벤트를 추가하려고 할 때 발생한다. 주로 스트림이 닫힌 후에 해당 스트림에 새로운 이벤트를 추가하려고 하는 상황에서 발생한다.
Bloc이 사용되는 위젯이 화면에서 벗어날 때, 해당 Bloc이 적절하게 폐기(close)되지 않으면 해당 Bloc과 연결된 리소스들이 해제되지 않을 수 있다.
나 같은 경우에는 2번 상황에서 문제가 발생했다. 기존 bloc을 사용하는 화면에서 벗어날때 bloc을 적절하게 폐기하지 않아서 발생했다. 사용하지 않는 bloc을 폐기 하는 이유는 메모리 누수와 같은 문제가 발생할 수 있다. Bloc을 더 이상 사용하지 않게 되면, 메모리를 효율적으로 관리하기 위해 Bloc을 닫는 것이 좋다.
가장 흔한 경우로, 위젯이 dispose된 후에도 비동기 작업이 완료되어 Bloc에 이벤트를 추가하려고 할 때 발생합니다.
class MyScreen extends StatefulWidget {
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late MyBloc _bloc;
void initState() {
super.initState();
_bloc = MyBloc();
// 비동기 작업 시작
_loadData();
}
Future<void> _loadData() async {
// 네트워크 요청 등 시간이 걸리는 작업
await Future.delayed(Duration(seconds: 3));
// 위젯이 이미 dispose된 후에 실행될 수 있음
_bloc.add(LoadDataEvent()); // ❌ 에러 발생!
}
void dispose() {
_bloc.close(); // dispose에서 close 호출
super.dispose();
}
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
bloc: _bloc,
builder: (context, state) {
return Scaffold(
body: Center(child: Text('Loading...')),
);
},
);
}
}
class MyScreen extends StatefulWidget {
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late MyBloc _bloc;
bool _isDisposed = false; // dispose 상태 추적
void initState() {
super.initState();
_bloc = MyBloc();
_loadData();
}
Future<void> _loadData() async {
await Future.delayed(Duration(seconds: 3));
// 위젯이 dispose되지 않았는지 확인
if (!_isDisposed && !_bloc.isClosed) {
_bloc.add(LoadDataEvent()); // ✅ 안전하게 이벤트 추가
}
}
void dispose() {
_isDisposed = true; // dispose 상태 표시
_bloc.close();
super.dispose();
}
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
bloc: _bloc,
builder: (context, state) {
return Scaffold(
body: Center(child: Text('Loading...')),
);
},
);
}
}
BlocProvider를 사용하지 않고 직접 Bloc을 생성할 때 생명주기 관리가 어려워집니다.
class ProductListScreen extends StatelessWidget {
Widget build(BuildContext context) {
final bloc = ProductBloc(); // 매번 새로운 Bloc 생성
return BlocBuilder<ProductBloc, ProductState>(
bloc: bloc,
builder: (context, state) {
return ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.products[index].name),
onTap: () {
bloc.add(SelectProductEvent(state.products[index]));
// 위젯이 rebuild되면 새로운 Bloc이 생성되어 이전 Bloc은 닫히지 않음
},
);
},
);
},
);
}
}
class ProductListScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ProductBloc()..add(LoadProductsEvent()),
child: _ProductListView(),
);
}
}
class _ProductListView extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
final bloc = context.read<ProductBloc>(); // Provider에서 가져오기
return ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.products[index].name),
onTap: () {
bloc.add(SelectProductEvent(state.products[index])); // ✅ 안전
},
);
},
);
},
);
}
}
여러 화면에서 동일한 Bloc 인스턴스를 공유할 때 생명주기 관리가 복잡해집니다.
class App extends StatelessWidget {
final authBloc = AuthBloc(); // 전역으로 생성
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider.value(
value: authBloc,
child: HomeScreen(),
),
);
}
}
class HomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
final authBloc = context.read<AuthBloc>();
return Scaffold(
body: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileScreen(),
),
);
// HomeScreen이 dispose되면 authBloc도 닫힐 수 있음
},
child: Text('Go to Profile'),
),
);
}
}
class ProfileScreen extends StatelessWidget {
Widget build(BuildContext context) {
final authBloc = context.read<AuthBloc>();
return Scaffold(
body: ElevatedButton(
onPressed: () {
authBloc.add(LogoutEvent()); // ❌ 이미 닫힌 Bloc에 이벤트 추가 시도
},
child: Text('Logout'),
),
);
}
}
class App extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
),
BlocProvider<ProductBloc>(
create: (context) => ProductBloc(),
),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
class HomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileScreen(),
),
);
},
child: Text('Go to Profile'),
),
);
}
}
class ProfileScreen extends StatelessWidget {
Widget build(BuildContext context) {
final authBloc = context.read<AuthBloc>(); // ✅ 안전하게 접근
return Scaffold(
body: ElevatedButton(
onPressed: () {
if (!authBloc.isClosed) {
authBloc.add(LogoutEvent()); // ✅ 상태 확인 후 이벤트 추가
}
},
child: Text('Logout'),
),
);
}
}
Bloc 내부에서 StreamSubscription을 사용할 때 적절히 취소하지 않으면 문제가 발생할 수 있습니다.
class TimerBloc extends Bloc<TimerEvent, TimerState> {
StreamSubscription<int>? _timerSubscription;
TimerBloc() : super(TimerInitial()) {
on<StartTimerEvent>(_onStartTimer);
}
Future<void> _onStartTimer(
StartTimerEvent event,
Emitter<TimerState> emit,
) async {
_timerSubscription = Stream.periodic(
Duration(seconds: 1),
(count) => count,
).listen((count) {
add(TickEvent(count)); // ❌ Bloc이 닫힌 후에도 실행될 수 있음
});
}
Future<void> close() {
// _timerSubscription?.cancel(); // ❌ 취소하지 않음
return super.close();
}
}
class TimerBloc extends Bloc<TimerEvent, TimerState> {
StreamSubscription<int>? _timerSubscription;
TimerBloc() : super(TimerInitial()) {
on<StartTimerEvent>(_onStartTimer);
on<TickEvent>(_onTick);
}
Future<void> _onStartTimer(
StartTimerEvent event,
Emitter<TimerState> emit,
) async {
await _timerSubscription?.cancel(); // 기존 subscription 취소
_timerSubscription = Stream.periodic(
Duration(seconds: 1),
(count) => count,
).listen(
(count) {
if (!isClosed) { // ✅ Bloc이 닫히지 않았는지 확인
add(TickEvent(count));
}
},
onError: (error) {
if (!isClosed) {
add(TimerErrorEvent(error));
}
},
);
}
Future<void> _onTick(
TickEvent event,
Emitter<TimerState> emit,
) async {
emit(TimerTick(event.count));
}
Future<void> close() {
_timerSubscription?.cancel(); // ✅ subscription 취소
return super.close();
}
}
BlocProvider를 사용하면 Bloc의 생명주기가 자동으로 관리됩니다.
class MyScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => MyBloc()..add(InitialEvent()),
child: _MyScreenContent(),
);
}
}
class _MyScreenContent extends StatelessWidget {
Widget build(BuildContext context) {
final bloc = context.read<MyBloc>();
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text('My Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
if (!bloc.isClosed) {
bloc.add(SomeEvent());
}
},
child: Text('Click Me'),
),
),
);
},
);
}
}
BlocProvider를 사용할 수 없는 경우, 수동으로 생명주기를 관리해야 합니다.
class MyScreen extends StatefulWidget {
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late MyBloc _bloc;
bool _mounted = true;
void initState() {
super.initState();
_bloc = MyBloc();
_initializeBloc();
}
Future<void> _initializeBloc() async {
// 비동기 초기화 작업
await Future.delayed(Duration(seconds: 1));
if (_mounted && !_bloc.isClosed) {
_bloc.add(InitialEvent());
}
}
void _safeAddEvent(MyEvent event) {
if (_mounted && !_bloc.isClosed) {
_bloc.add(event);
}
}
void dispose() {
_mounted = false;
_bloc.close();
super.dispose();
}
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
bloc: _bloc,
builder: (context, state) {
return Scaffold(
body: ElevatedButton(
onPressed: () => _safeAddEvent(SomeEvent()),
child: Text('Click'),
),
);
},
);
}
}
반복적으로 사용되는 패턴을 헬퍼 함수로 추출합니다.
extension BlocExtension on Bloc {
void safeAdd(Event event) {
if (!isClosed) {
add(event);
}
}
}
// 사용 예제
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
final bloc = context.read<MyBloc>();
return ElevatedButton(
onPressed: () {
bloc.safeAdd(SomeEvent()); // ✅ 안전하게 이벤트 추가
},
child: Text('Click'),
);
}
}
BlocListener를 사용하여 상태 변화를 안전하게 처리합니다.
class MyScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => MyBloc()..add(LoadDataEvent()),
child: BlocListener<MyBloc, MyState>(
listener: (context, state) {
if (state is MyErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: _MyScreenContent(),
),
);
}
}
class _MyScreenContent extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
if (state is MyLoadingState) {
return Center(child: CircularProgressIndicator());
}
if (state is MyLoadedState) {
return ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.items[index].name),
);
},
);
}
return Center(child: Text('No data'));
},
);
}
}
Bloc 내부에서 사용하는 리소스들도 적절히 관리해야 합니다.
class DataBloc extends Bloc<DataEvent, DataState> {
final DataRepository _repository;
StreamSubscription<Data>? _dataSubscription;
Timer? _pollingTimer;
DataBloc({required DataRepository repository})
: _repository = repository,
super(DataInitial()) {
on<LoadDataEvent>(_onLoadData);
on<StartPollingEvent>(_onStartPolling);
on<StopPollingEvent>(_onStopPolling);
on<DataUpdatedEvent>(_onDataUpdated);
}
Future<void> _onLoadData(
LoadDataEvent event,
Emitter<DataState> emit,
) async {
emit(DataLoading());
try {
final data = await _repository.fetchData();
emit(DataLoaded(data));
} catch (e) {
if (!isClosed) {
emit(DataError(e.toString()));
}
}
}
Future<void> _onStartPolling(
StartPollingEvent event,
Emitter<DataState> emit,
) async {
_pollingTimer?.cancel();
_pollingTimer = Timer.periodic(
Duration(seconds: 5),
(timer) {
if (!isClosed) {
add(LoadDataEvent());
} else {
timer.cancel();
}
},
);
}
Future<void> _onStopPolling(
StopPollingEvent event,
Emitter<DataState> emit,
) async {
_pollingTimer?.cancel();
_pollingTimer = null;
}
Future<void> _onDataUpdated(
DataUpdatedEvent event,
Emitter<DataState> emit,
) async {
if (state is DataLoaded) {
emit(DataLoaded(event.data));
}
}
// StreamSubscription 예제
void _subscribeToDataStream() {
_dataSubscription?.cancel();
_dataSubscription = _repository.dataStream.listen(
(data) {
if (!isClosed) {
add(DataUpdatedEvent(data));
}
},
onError: (error) {
if (!isClosed) {
add(DataErrorEvent(error.toString()));
}
},
);
}
Future<void> close() {
// 모든 리소스 정리
_dataSubscription?.cancel();
_pollingTimer?.cancel();
return super.close();
}
}
void _checkBlocState(Bloc bloc) {
print('Bloc is closed: ${bloc.isClosed}');
print('Bloc state: ${bloc.state}');
}
try {
bloc.add(SomeEvent());
} catch (e, stackTrace) {
print('Error: $e');
print('Stack trace: $stackTrace');
}
class MyBlocObserver extends BlocObserver {
void onCreate(BlocBase bloc) {
super.onCreate(bloc);
print('Bloc created: ${bloc.runtimeType}');
}
void onClose(BlocBase bloc) {
super.onClose(bloc);
print('Bloc closed: ${bloc.runtimeType}');
}
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
print('Bloc error: ${bloc.runtimeType}, Error: $error');
}
void onEvent(BlocBase bloc, Object? event) {
super.onEvent(bloc, event);
print('Event: ${bloc.runtimeType}, Event: $event');
}
void onTransition(BlocBase bloc, Transition transition) {
super.onTransition(bloc, transition);
print('Transition: ${bloc.runtimeType}, $transition');
}
}
// main.dart에서 사용
void main() {
Bloc.observer = MyBlocObserver();
runApp(MyApp());
}
에러를 방지하기 위한 체크리스트:
isClosed 확인"Bad state: Cannot add new events after calling close" 오류는 주로 스트림 관리와 Bloc 생명주기 관리와 관련이 있다. 올바른 Bloc 사용 패턴과 생명주기 관리를 통해 이러한 문제를 효과적으로 해결할 수 있다.
✅ BlocProvider 사용을 통한 자동 생명주기 관리
✅ Bloc 사용 후 반드시 close() 호출
✅ 닫힌 Bloc에 이벤트를 추가하지 않기
✅ isClosed 속성을 확인하여 Bloc 상태 체크
✅ 비동기 작업 완료 후 위젯 상태 확인
✅ StreamSubscription, Timer 등 리소스 적절히 정리
✅ 메모리 누수 방지를 위한 적절한 리소스 관리