[Flutter] : TDD - 10. Bloc Scaffolding & Input Conversion


드디어 Bloc을 구현할 시간이 되었다.

Bloc은 Business Logic Component 의 약자이다.

여기서 VsCode Extension Bloc을 설치하고

presentation 폴더 내부에 new bloc을 생성해보자. 이름은 number_trivia로 , advancde로 진행한다.

그럼 아래와 같이 폴더가 생긴다.

이미지

  1. Event : UI로 부터 이벤트를 받아온다.
  2. Bloc : 받아온 이벤트를 비즈니스 로직으로 처리한다. (여기서는 Usecase를 호출한다.)
  3. State : 그렇게 처리된 Bloc의 데이터를 UI로 다시 보내준다.

이미지

1. Events


자동으로 생성된 파일을 봐보자.

number_trivia_event.dart

part of 'number_trivia_bloc.dart';

abstract class NumberTriviaEvent extends Equatable {
  const NumberTriviaEvent();

  @override
  List<Object> get props => [];
}

추상 클래스가 하나 존재한다. 이것을 상속해서 우리 클래스를 구현하면서 진행할 것이다.

먼저 어떤 UI 이벤트들이 여기로 들어오게 될까?

우리가 만들 화면 예시를 봐보자.

이미지

화면에는 2개의 버튼이 있다.

하나는 ConcreteNumber 하나는 RandomNumber 버튼이다.

그러므로 우리는 2개의 이벤트가 들어온다.

GetTriviaForConcreteNumber

GetTriviaForRandomNumber

ConcreteNumber는 숫자 필드를 포함해야하고

RandomNumber는 비어있다.

작성해보자.

number_trivia_event.dart

abstract class NumberTriviaEvent extends Equatable {
  const NumberTriviaEvent();

  @override
  List<Object> get props => [];
}

class GetTriviaForConcreteNumber extends NumberTriviaEvent {
  final String numberString;

  GetTriviaForConcreteNumber(this.numberString);

  @override
  List<Object> get props => [numberString];
}

class GetTriviaForRandomNumber extends NumberTriviaEvent {}

GetTriviaForConcreteNumber는 TextField에서 숫자를 받아올 것이다. 그 숫자는 String 타입이다.

여기서 받아온 String Number를 Int로 변경해야하는데 그걸 UI 혹은 event에서 처리하는 건 SOLID 원칙 ( Clean Architecture) 에 위배된다.

이 컨버터를 만들어보자.

컨버터는 core폴더아래 util이라는 폴더를 새로 만들고 input_converter.dart 파일을 생성해주자.

core/util/input_converter.dart

import 'package:dartz/dartz.dart';
import 'package:number_trivia/core/error/failures.dart';

class InputConverter {
  Either<Failure, int> stringToUnsignedInteger(String str) {
    // TODO
  }
}

class InvalidInputFailure extends Failure {}

input_converter도 테스트하기 위해 먼저 테스트 코드를 작성해주자.

input_converter_test.dart

void main() {
  InputConverter inputConverter;

  setUp(() {
    inputConverter = InputConverter();
  });

  group('stringToUnsignedInt', () {
    test(
        'should return an integer when the string represents an unsigned integer',
        () async {
      // arrange
      final str = '123';
      // act
      final result = inputConverter.stringToUnsignedInteger(str);
      // assert
      expect(result, Right(123));
    });
  });
}

impl.dart

class InputConverter {
  Either<Failure, int> stringToUnsignedInteger(String str) {
    return Right(int.parse(str));
  }
}

자 이번에는 숫자가 아니라 문자가 들어 올 때 Failure 처리를 해보자

test.dart

test('should return a Failure when the string is not an integer', () async {
      // arrange
      final str = 'abc';
      // act
      final result = inputConverter.stringToUnsignedInteger(str);
      // assert
      expect(result, Left(InvalidInputFailure()));
    });

impl.dart

class InputConverter {
  Either<Failure, int> stringToUnsignedInteger(String str) {
    try {
      return Right(int.parse(str));
    } on FormatException {
      return Left(InvalidInputFailure());
    }
  }
}

자 여기서 하나 더

우리는 양수 처리만 해준다.

그러니 음수에 대한 예외처리를 해주자.

test.dart

test('should return a failure when the string is a negative integer',
        () async {
      // arrange
      final str = '-123';
      // act
      final result = inputConverter.stringToUnsignedInteger(str);
      // assert
      expect(result, Left(InvalidInputFailure()));
    });

impl.dart

class InputConverter {
  Either<Failure, int> stringToUnsignedInteger(String str) {
    try {
      final integer = int.parse(str);
      if (integer < 0) throw FormatException();
      return Right(int.parse(str));
    } on FormatException {
      return Left(InvalidInputFailure());
    }
  }
}

자 이제 Converter는 끝이 났다.

이제 State를 봐보자

2. States


State는 UI에 따른 Bloc에 대한 결과 상태이다.

여기서(NumberTrivia)는 4가지 상태가 있다.

  • Empty
  • Loading
  • Loaded
  • Error

하나씩 만들어보자.

number_trivia_state.dart

abstract class NumberTriviaState extends Equatable {
  const NumberTriviaState();

  @override
  List<Object> get props => [];
}

class Empty extends NumberTriviaState {}

class Loading extends NumberTriviaState {}

class Loaded extends NumberTriviaState {
  final NumberTrivia trivia;

  Loaded({@required this.trivia});

  @override
  List<Object> get props => [trivia];
}

class Error extends NumberTriviaState {
  final String message;

  Error({@required this.message});

  @override
  List<Object> get props => [message];
}

자 이렇게 event와 state의 기본적인 코딩이 끝이 났다.

다음에는 Bloc을 TDD 기반으로 개발 해보도록 하자.