Coaspe

Flutter - Bloc # Architecutre 본문

Flutter/번역

Flutter - Bloc # Architecutre

Coaspe 2023. 2. 17. 14:02

원문: https://bloclibrary.dev/#/architecture

bloc 라이브러리를 사용하면 앱은 3개의 레이어로 나누어 집니다.

 

  • Presentation
  • Business Logic
  • Data
    • Repository
    • Data Provider

Data 레이어부터 Presentation 레이어로 훑어봅시다.

Data Layer

하나 혹은 그 이상의 소스로부터 오는 데이터를 가져오고/조작하는 레이어 입니다.

Data 레이어는 두개의 파트로 나누어집니다:

  • Repository
  • Data Provider

이 레이어는 앱의 가장 낮은 단계이고 데이터베이스, 네트워크 요청, 외에 비동기 데이터 소스들과 상호작용합니다.

Data Provider

Data Provider는 raw data를 제공합니다. Data provider는 포괄적(generic)이고 다용도(versatile)로 사용 될 수 있어야 합니다.

Data provider는 보통 CRUD 명령을 수행 할 수 있는 간단한 APIs들을 가집니다. 아마도 Data layer의 메소드로 createData, readData, updateData 그리고 deleteData 같은 메소드를 가질 것 입니다.

 

class DataProvider {
    Future<RawData> readData() async {
        // Read from DB or make network request etc...
    }
}

Repository

Repository 레이어는 Bloc 레이어와 통신하는 하나 이상의 data providers의 wrapper 입니다.
class Repository {
    final DataProviderA dataProviderA;
    final DataProviderB dataProviderB;

    Future<Data> getAllDataThatMeetsRequirements() async {
        final RawDataA dataSetA = await dataProviderA.readData();
        final RawDataB dataSetB = await dataProviderB.readData();

        final Data filteredData = _filterData(dataSetA, dataSetB);
        return filteredData;
    }
}

보시다시피, Repository 레이어는 여러 Data provider와 상호 작용하고 데이터에 대한 변환을 수행한 후 결과를 business logic layer로 전달할 수 있습니다.

 

Business Logic Layer

Business login 레이어는 presentation 레이어로 부터 오는 input을 새로운 상태와 함께 처리합니다. 이 레이어는 하나 이상의 repository에 의존하여 애플리케이션 상태를 구축하는 데 필요한 데이터를 찾을 수 있습니다.

Business login 레이어를 유저 인터페이스(presentation 레이어)와 data 레이어의 중간 다리라고 생각해도 됩니다. Business login 레이어는 presentation 레이어로부터 이벤트와 액션들을 알림받고, presentation 레이어가 사용할 새로운 상태를 빌드하기 위해 repository와 상호작용합니다.

class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
    BusinessLogicComponent(this.repository) {
        on<AppStarted>((event, emit) {
            try {
                final data = await repository.getAllDataThatMeetsRequirements();
                emit(Success(data));
            } catch (error) {
                emit(Failure(error));
            }
        });
    }

    final Repository repository;
}

 

Bloc-to-Bloc Communication

bloc는 stream을 생성하므로, bloc으로 다른 bloc을 listen하도록 하고 싶을 수 있습니다. 하지만, 그러지 마세요!  아래 코드를 사용하는 것 보다 더 좋은 대안이 있습니다.

class BadBloc extends Bloc {
  final OtherBloc otherBloc;
  late final StreamSubscription otherBlocSubscription;

  BadBloc(this.otherBloc) {
    // No matter how much you are tempted to do this, you should not do this!
    // Keep reading for better alternatives!
    otherBlocSubscription = otherBloc.stream.listen((state) {
      add(MyEvent())
    });
  }

  @override
  Future<void> close() {
    otherBlocSubscription.cancel();
    return super.close();
  }
}

위의 코드는 에러로부터 자유롭지만, 큰 문제가 하나 있습니다: 두 blocs간에 의존성이 생성됩니다.

 

일반적으로, 동일한 아키텍처 계층에서 두 엔티티 사이의 형제 의존성은 유지되기 어려운 긴밀한 결합을 만들기 때문에 어떤 대가를 치르더라도 피해야 합니다.

 

bloc는 오직 주입된 repositories와 이벤트로부터만 정보를 받아야 합니다.

만약 bloc이 다른 bloc에 응답해야하는 상황이 생긴다면, 두 개의 옵션이 있습니다. 해당 문제를 presentaion 레이어로 올려보내거나, domain 레이어로 내리면 됩니다.

 

Connecting Blocs through Presentation

한 bloc을 listen하고 싶다면, BlocListener를 사용하고, 첫 bloc이 변화 할 때마다 다른 bloc에 이벤트를 추가하세요.

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocListener<WeatherCubit, WeatherState>(
      listener: (context, state) {
        // When the first bloc's state changes, this will be called.
        //
        // Now we can add an event to the second bloc without it having
        // to know about the first bloc.
        BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
      },
      child: TextButton(
        child: const Text('Hello'),
        onPressed: () {
          BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
        },
      ),
    );
  }
}

위의 코드는 느슨한 결합을 생성하므로써 SecondBloc이 FirstBloc에 대해 알 필요가 없도록 합니다. flutter_weather 앱은 이런 테크닉을 사용해서 전달 받은 날씨 정보에 기반하여 앱의 테마를 바꿉니다.

 

경우에 따라서 presentation 레이어에서 두 blocs를 결합하길 원치 않을 수 있습니다. 그럴 때는 데이터가 변경될 때마다 두 블록이 동일한 데이터 소스를 공유하고 업데이트하는 것이 종종 합리적일 수 있습니다.

 

Connecting Blocs through Domain

두 개의 blocs가 한 repository로 부터 stream을 listen하고 repository의 data가 변경 될 때 마다 독립적으로 자신의 상태를 업데이트 할 수 있습니다. 상태를 동기화시키기 위해 반응형 repositories를 사용하는 것은 큰 규모의 상업 앱에서는 흔한 일입니다.

 

첫 번째로 Stream 데이터를 제공하는 repository를 사용하거나, 생성하세요. 예를 들면, 다음의 repository는 끝나지 않고 동일한 app ideas를 생성합니다.

class AppIdeasRepository {
  int _currentAppIdea = 0;
  final List<String> _ideas = [
    "Future prediction app that rewards you if you predict the next day's news",
    'Dating app for fish that lets your aquarium occupants find true love',
    'Social media app that pays you when your data is sold',
    'JavaScript framework gambling app that lets you bet on the next big thing',
    'Solitaire app that freezes before you can win',
  ];

  Stream<String> productIdeas() async* {
    while (true) {
      yield _ideas[_currentAppIdea++ % _ideas.length];
      await Future<void>.delayed(const Duration(minutes: 1));
    }
  }
}

동일한 repository가 각각 새로운 app ideas에 반응해야하는 bloc에 주입될 수 있습니다. 아래는 위의 repository로부터 오는 각 app idea의 상태를 생산하는 AppIdeaRankingBloc 입니다:

class AppIdeaRankingBloc
    extends Bloc<AppIdeaRankingEvent, AppIdeaRankingState> {
  AppIdeaRankingBloc({required AppIdeasRepository appIdeasRepo})
      : _appIdeasRepo = appIdeasRepo,
        super(AppIdeaInitialRankingState()) {
    on<AppIdeaStartRankingEvent>((event, emit) async {
      // When we are told to start ranking app ideas, we will listen to the
      // stream of app ideas and emit a state for each one.
      await emit.forEach(
        _appIdeasRepo.productIdeas(),
        onData: (String idea) => AppIdeaRankingIdeaState(idea: idea),
      );
    });
  }

  final AppIdeasRepository _appIdeasRepo;
}

Bloc을 stream과 사용하는 방법을 더 자세히 알고 싶다면, How to use Bloc with streams and concurrency 을 참고하세요.

 

Presentation Layer

Presentation 레이어는 하나 혹은 그 이상의 bloc 상태를 기반으로 자신을 어떻게 렌더할지 해결하는 역할을 합니다. 게다가, 유저 input과 앱의 생명주기 이벤트를 다룹니다.

대부분 앱의 흐름은 유저에게 보여줄 데이터를 앱이 패치하도록 트리거하는 AppStart 이벤트로 시작합니다.

아래 시나리오에서, presentation 레이어는 AppStart 이벤트를 추가합니다.

게다가, presentation 레이어는 bloc 레이어의 상태를 기반으로 스크린에 어떻게 렌더해야할지 해결해야 합니다.

class PresentationComponent {
    final Bloc bloc;

    PresentationComponent() {
        bloc.add(AppStarted());
    }

    build() {
        // render UI based on bloc state
    }
}

지금까지, 비록 우리가 조금의 코드들만 참고하였지만, 해당 코드들은 꽤 높은 수준입니다. 튜토리얼 섹션에서는 여러 가지 예제 앱을 만들 때 이 모든 것을 종합해 봅니다.

 

정리

  1. Bloc 구조는 Data, Bloc, Presentation 레이어로 이루어진다.
  2. Data layer는 data provider, repository로 이루어지는데, data provider는 raw data와 CRUD 명령어를 제공하고, repository는 여러 data provider를 이용해 데이터를 변환한 뒤 bloc layer와 상호작용합니다.
  3. Bloc layer는 presentation layer와 data layer의 중간 다리이고, repository에 의존하여 애플리케이션 상태를 구축하는 데 필요한 데이터를 찾으며, presentation 레이어로부터 이벤트와 액션들을 알림받고, presentation 레이어가 사용할 새로운 상태를 빌드합니다.
Comments