[Flutter] Tab & Page View에서 BLoC 사용 방법

Tyger·2023년 6월 30일
4

State Management

목록 보기
13/14

Tab & Page View에서 BLoC 사용 방법

flutter_bloc | Flutter Packages

예제로 배워보는 BLoC Pattern이란 ?
Count App With BLoC
Count App With Cubit

이번 글에서는 Tab & Page View에서 Bloc Pattern을 사용하는 방법에 대해서 살펴보도록 하겠다.

Bloc 패턴 사용시 어느 시점에 Bloc을 생성해 줘야 하는지에 따라서 Bloc이 탭을 떠났을 때에 Dispose 할지 ? 유지 할지를 고민해봐야 한다.

Bloc을 사용할 때에 이 부분에서 어려움을 느끼시는 분들이 많은 것 같아 글을 작성하게 되었다.

Bloc은 반드시 사용 전 생성 되어야 하는데, Bloc의 생성은 lazy하게 작동하게 된다.
BlocProvider를 사용해서 Bloc을 등록을 하는데, 이 때 Bloc이 instance 과정을 거치지 않고, 사용할 때에 instance 과정을 거치게 된다.

Bloc의 인스턴스 과정은 Builder를 만났을 때에 이루어 지는데, 즉시 인스턴스를 시키고 싶다면, lazy하게 하지 않고 즉시 인스턴스할 수 있다.

Observer

Bloc을 앱 전역에서 관찰하기 위해 Observer를 생성하여 등록해 주도록 하자.

Observer를 통해 Bloc의 인스턴스 및 Dispose를 관찰할 수 있다. Bloc 사용시 Observer Pattern을 등록하여 사용하시는 것이 좋다.

import 'dart:developer' as developer;

class Observer extends BlocObserver {
  
  void onCreate(BlocBase bloc) {
    developer.log('\x1B[37mCreated BLoC $bloc\x1B[0m');
    super.onCreate(bloc);
  }

  
  void onClose(BlocBase bloc) {
    developer.log('\x1B[32mClosed BLoC $bloc\x1B[0m');
    super.onClose(bloc);
  }
}

생성한 Observer를 main() 함수에 넣어 서비스 전체에서 Bloc의 상태를 수신하도록 하자.

void main() {
  Bloc.observer = Observer();
  runApp(const MyApp());
}

UI

먼저 이번 예제에 사용할 UI 구조이다.

탭은 3개로 나눠서 각각 A, B, C 탭을 구성하고 탭뷰 영역에 APage, BPage, CPage 페이지를 생성해 주는데, 여기서 사용되는 UI와 기능은 모두 동일하다.

MainPage.dart

class MainPage extends StatelessWidget {
  const MainPage({super.key});

  
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.black,
          title: const Text("Tab View with BLoC"),
        ),
        body: Column(
          children: [
            TabBar(
                labelStyle: const TextStyle(
                    fontWeight: FontWeight.bold, fontSize: 20),
                labelColor: Colors.black,
                tabs: [
                  ...["A", "B", "C"].map((e) => Tab(
                        text: e,
                      ))
                ]),
            const Expanded(
              child: TabBarView(children: [
                APage(),
                BPage(),
                CPage(),
              ]),
            )
          ],
        ),
      ),
    );
  }
}

APage

A, B, C 페이지 코드는 모두 동일하다.

count 값은 Bloc에서 각각 생성할 state이다.

class CountWidget extends StatelessWidget {
  const CountWidget({super.key});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {},
      child: Container(
        child: Center(
            child: count == null
                ? const CircularProgressIndicator()
                : Text(
                    count.toString(),
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 60,
                    ),
                  )),
      ),
    );
  }
}

Bloc

Bloc도 A, B, C 각각의 상태를 가지는 3개의 Bloc을 사용할 것이다.

초기 State는 Null 상태로 유지하고 3초 후에 값을 0으로 변경할 예정인데, 로딩 인디케이터를 띄우기 위해서 아래와 같이 해주었다.

increment 부분은 숫자를 터치 했을 때에 값을 증가시켜, Bloc이 유지되는지, Dispose 되는지를 확인할 것이다.

class ABloc extends Bloc<AEvent, int?> {
  ABloc() : super(null) {
    on<AInitEvent>(_init);
    on<AIncrementEvent>(_increment);
    add(AInitEvent());
  }

  Future<void> _increment(AIncrementEvent event, Emitter<int?> emit) async {
    emit(state! + 1);
  }

  Future<void> _init(AInitEvent event, Emitter<int?> emit) async {
    await Future.delayed(const Duration(milliseconds: 3000), () {
      emit(0);
    });
  }
}

abstract class AEvent extends Equatable {}

class AInitEvent extends AEvent {
  
  List<Object?> get props => [];
}

class AIncrementEvent extends AEvent {
  
  List<Object?> get props => [];
}

생성한 Bloc을 UI에 맞게 각각 Builder를 생성해 주면된다.

 return BlocBuilder<ABloc, int?>(
      builder: (context, state) {
        return ...
      }
   );

Provider

이제 BlocProvider를 사용해서 Bloc을 등록해 주어야 하는데, 우선은 MainPage 최상단에 MultiBlocProvider를 사용하여 3개의 Bloc을 모두 등록해 줄것이다.

   return MultiBlocProvider(
      providers: [
        BlocProvider(create: (_) => ABloc()),
        BlocProvider(create: (_) => BBloc()),
        BlocProvider(create: (_) => CBloc()),
      ],
      child: DefaultTabController(
      ...
      )
  );

이제 앱을 실행해보면 처음에는 로그창에 ABloc만 Instance 했다고 출력이 될 것이다.

이 부분만 살펴봐도 TabView는 해당 Tab이 노출되기 전까지 랜더링을 하지 않는 것을 확인할 수 있다. 만약에 랜더링을 한 번에 했다면 ABloc만 인스턴스 되는게 아닌 BBloc, CBloc도 함께 인스턴스 됬을 것이다.

[log] Created BLoC Instance of 'ABloc'

탭을 이동해 보면 각각의 페이지에 등록한 Bloc들이 차례로 인스턴스 되는 것을 확인할 수 있다.

다시 탭을 계속 이동해봐도 Bloc은 Dispose 되지 않고 그대로 상태를 유지하고 있다.

이번에는 BlocProvider를 각각의 Page 상단에 등록을 해보자. 위에서 등록한 MultiBlocProvider는 제거 해야 한다.

   return BlocProvider<ABloc>(
      create: (_) => ABloc(),
      child: BlocBuilder<ABloc, int?>(
        builder: (context, state) {
          return ...
          }
      )
  );

이번에도 ABloc만 먼저 인스턴스 됬다가 탭을 이동해 보면 Bloc이 Dispose되고 이동하는 것을 확인할 수 있다.

Bloc의 인스턴스가 제거 되면서 탭을 이동할 때마다 상태가 유지되지 못하고 초기화 되고 있다.

이번에는 각 탭에 동일한 Bloc을 사용하는 경우에 대해서 살펴보도록 하겠다.

다시 A, B, C Bloc을 MainPage에 MultiBlocPovider에 등록해 주고 새로운 Bloc 하나를 APage에 생성하도록 하겠다.

TabView with Public Bloc

단순히 현재 시간을 보여주는 Bloc을 생성해서 A, B, C Page에 사용할 예정이다.

여기서 문제점이 TimeBloc을 하나 만들었는데, 탭에 상태를 유지하면서 주입을 하게 되면 어떻게 될까 ?

첫 번째로, 각 페이지에서 TimeBloc을 등록하게 되면, Tab을 변경할 때마다 TimeBloc은 초기화 되게 된다.

그렇다면 MainPage에 Bloc을 주입하게 되면 Bloc의 상태는 유지될 것이지만, 문제는 하나의 상태를 3개의 페이지에서 동일하게 사용하는 케이스가 된다.

Bloc

먼저 TimeBloc을 생성해주자. 단순히 현재 인스턴스된 시간을 노출할 것이다.

class TimeBloc extends Bloc<TimeEvent, String> {
  TimeBloc() : super("") {
    on<TimeEvent>(_start);
    add(TimeEvent());
  }

  Future<void> _start(TimeEvent event, Emitter<String> emit) async {
    emit(DateTime.now().toString());
  }
}

class TimeEvent extends Equatable {
  
  List<Object?> get props => [];
}

TimeWidget

현재 시간을 노출하는 간단한 위젯이고, 해당 위젯을 각 페이지에서 동일하게 넣어주자.

class TimeWidget extends StatelessWidget {
  const TimeWidget({super.key});

  
  Widget build(BuildContext context) {
    return BlocBuilder<TimeBloc, String>(builder: (context, state) {
      return Text(state);
    });
  }
}

BlocProvider를 사용해서 TimeBloc을 등록해야 하는데, 이 때에 사용할 수 있는 방법이 BlocProvider.value로 등록하는 방법이다.

MainPage에 동일 Bloc을 3개의 Bloc으로 객체를 생성해주자.

final TimeBloc _aTime = TimeBloc();
final TimeBloc _bTime = TimeBloc();
final TimeBloc _cTime = TimeBloc();

각각의 Page에 BlocProvider.value를 사용해서 동일 Bloc을 각각의 페이지에 주입해 주면된다.

TabBarView(children: [
	BlocProvider.value(
		value: _aTime,
		child: APage(),
	),
	BlocProvider.value(
		value: _bTime,
		child: BPage(),
	),
	BlocProvider.value(
		value: _cTime,
		child: CPage(),
	),
])

마무리

Tab & Page View 에서 Bloc을 어떻게 등록하고 소비하는지에 따른 Bloc의 상태에 대해서 살펴보았다. 제가 공유한 부분이 정답은 아닐 수 있기에, 각자가 다양한 방식으로 프로젝트에 맞게 사용해 보셔야 한다. 참고용으로 봐주시면 좋을 것 같다.

더 좋은 방법이 있으면 댓글 남겨주세요 !

profile
Flutter Developer

0개의 댓글