FLUTTER CLEAN ARCHITECTURE (PART 12) – cài đặt Bloc 2/2

Trần Đình Quý

Flutter Clean Architecture

Trong phần trước chúng ta đã bắt đầu triển khai NumberTriviaBloc.

Tiếp tục

Chúng ta đã triển khai một phần quan trọng của logic chạy bất cứ khi nào event GetTriviaForConcreteNumber đến Bloc – conversion. Tuy nhiên, sau khi chúng ta có số nguyên đã được chuyển đổi và xác thực thành công mà người dùng muốn xem một số thông tin thú vị về nó, hiện tại chúng ta chỉ ném một UnimplementedException. Hãy thay đổi điều đó!

implementation.dart

  Future<void> _onGetTriviaForConcreteNumber(
      GetTriviaForConcreteNumber event, Emitter<NumberTriviaState> emit) async {
    final Either<Failure, int> inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    await inputEither.fold(
      (Failure l) async => emit(Error(message: invalidInputFailureMessage)),
      (int r) async {
        final Either<Failure, NumberTrivia> failureOrTrivia =
            await getConcreteNumberTrivia(
          Params(number: r),
        );
      },
    );
  }

Tất nhiên, chỉ đơn giản là thực thi use case là không đủ. Chúng ta cần cho giao diện người dùng biết điều gì đang xảy ra để nó có thể hiển thị nội dung cho người dùng. Những trạng thái nào mà Bloc nên phát ra khi use case thực thi trơn tru?

Trước khi gọi use case, nên hiển thị một số phản hồi trực quan cho người dùng. Trong khi việc hiển thị CircularProgressIndicator là công việc của giao diện người dùng, chúng ta phải thông báo cho giao diện người dùng hiển thị nó bằng cách phát ra trạng thái Loading.

Sau khi use case trả về phía Right của Either (có nghĩa là thành công), Bloc nên chuyển NumberTrivia đã nhận được đến giao diện người dùng bên trong một trạng thái Loaded.

  Future<void> _onGetTriviaForConcreteNumber(
      GetTriviaForConcreteNumber event, Emitter<NumberTriviaState> emit) async {
    final Either<Failure, int> inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    await inputEither.fold(
      (Failure l) async => emit(Error(message: invalidInputFailureMessage)),
      (int r) async {
        emit(Loading());
        final Either<Failure, NumberTrivia> failureOrTrivia =
            await getConcreteNumberTrivia(
          Params(number: r),
        );
        failureOrTrivia.fold(
          (Failure failure) { throw UnimplementedError() },
          (NumberTrivia trivia) => emit(Loaded(trivia: trivia)),
        );
      },
    );
  }

Kiểu Either buộc chúng ta phải xử lý trường hợp Failure, nhưng hiện tại, chúng ta chỉ đơn giản là ném ra một UnimplementedError khi điều này xảy ra.

implementation.dart

(int r) async {
  emit(Loading());
  final Either<Failure, NumberTrivia> failureOrTrivia =
      await getConcreteNumberTrivia(
    Params(number: r),
  );
  failureOrTrivia.fold(
    (Failure failure) => emit(Error(message: SERVER_FAILURE_MESSAGE)),
    (NumberTrivia trivia) => emit(Loaded(trivia: trivia)),
  );
},

Đúng vậy, rất đơn giản! Kiểm thử trường hợp ServerFailure là một bước quan trọng, nhưng bạn hoàn toàn chính xác khi nói rằng CacheFailure cũng cần được xử lý.

Chúng ta sắp sửa gặp một đoạn mã trông rất xấu xí. Tuy nhiên, đừng lo lắng vì chúng ta sẽ refactor nó chỉ trong chốc lát.

implementation.dart

(int r) async {
  emit(Loading());
  final Either<Failure, NumberTrivia> failureOrTrivia =
      await getConcreteNumberTrivia(
    Params(number: r),
  );
  failureOrTrivia.fold(
    (Failure failure) => emit(
      Error(message: failure is ServerFailure
        ? SERVER_FAILURE_MESSAGE
        : CACHE_FAILURE_MESSAGE,
        )
      ),
    (NumberTrivia trivia) => emit(Loaded(trivia: trivia)),
  );
},

Cách chúng ta quyết định thông báo của trạng thái Error còn nhiều hạn chế. Toán tử ba ngôi thực sự là một lựa chọn không an toàn – làm sao nếu có một số loại Failure phụ khác mà chúng ta không biết đến? Chắc chắn, điều đó không nên xảy ra với Kiến trúc Ngăn nắp, nhưng… Dù là loại Failure thực sự nào, trừ khi đó là ServerFailure, bây giờ nó sẽ luôn được gán với CACHE_FAILURE_MESSAGE.

Hãy tạo một phương thức trợ giúp _mapFailureToMessage riêng biệt, nơi chúng ta sẽ cố gắng xử lý các thông báo một cách phù hợp hơn. Chúng ta vẫn đang trong giới hạn của Dart vanilla, vì vậy chúng ta vẫn cần xử lý loại Failure bất ngờ (để đề phòng).

Toàn bộ phương thức mapEventToState bây giờ trông như thế này:

implementation.dart

  Future<void> _onGetTriviaForConcreteNumber(
      GetTriviaForConcreteNumber event, Emitter<NumberTriviaState> emit) async {
    final Either<Failure, int> inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    await inputEither.fold(
      (Failure l) async => emit(Error(message: invalidInputFailureMessage)),
      (int r) async {
        emit(Loading());
        final Either<Failure, NumberTrivia> failureOrTrivia =
            await getConcreteNumberTrivia(
          Params(number: r),
        );
        failureOrTrivia.fold(
          (Failure failure) => emit(
            Error(message: _mapFailureToMessage(failure)
            ),
          (NumberTrivia trivia) => emit(Loaded(trivia: trivia)),
        );
      },
    );
  }

  String _mapFailureToMessage(Failure failure) {
    switch (failure) {
      case ServerFailure _:
        return serverFailureMessage;
      case CacheFailure _:
        return cacheFailureMessage;
      default:
        return 'Unexpected error';
    }
  }

Xử lý random trivia

Chỉ có hai thay đổi cần thực hiện trong đoạn code được sao chép này:

  1. Gọi use case GetRandomNumberTrivia: Thay vì gọi GetConcreteNumberTrivia như trước, hãy gọi GetRandomNumberTrivia để lấy thông tin thú vị về một số ngẫu nhiên.
  2. Truyền NoParams(): Vì use case GetRandomNumberTrivia không chấp nhận bất kỳ tham số nào, nên hãy truyền đối tượng NoParams() vào nó. NoParams() là một đối tượng trống đặc biệt được sử dụng để biểu thị việc không có tham số nào được truyền.

implementation.dart

  Future<void> _onGetTriviaForRandomNumber(
      GetTriviaForRandomNumber event, Emitter<NumberTriviaState> emit) async {
    emit(Loading());
    final Either<Failure, NumberTrivia> failureOrTrivia =
        await getRandomNumberTrivia(
      NoParams(),
    );
    failureOrTrivia.fold(
      (Failure failure) => emit(
        Error(message: _mapFailureToMessage(failure)
        ),
      (NumberTrivia trivia) => emit(Loaded(trivia: trivia)),
    );
  }

Thực ra không có quá nhiều đoạn mã trùng lặp cần loại bỏ, nhưng chúng ta vẫn có thể làm cho mã của mình ngắn gọn hơn một chút bằng cách trích xuất việc phát ra các trạng thái Error và Loaded.

implementation.dart

class NumberTriviaBloc extends Bloc<NumberTriviaEvent, NumberTriviaState> {
  NumberTriviaBloc({
    required GetConcreteNumberTrivia concrete,
    required GetRandomNumberTrivia random,
    required this.inputConverter,
  })  : getConcreteNumberTrivia = concrete,
        getRandomNumberTrivia = random,
        super(Empty()) {
    on<GetTriviaForConcreteNumber>(_onGetTriviaForConcreteNumber);
    on<GetTriviaForRandomNumber>(_onGetTriviaForRandomNumber);
  }

  final GetConcreteNumberTrivia getConcreteNumberTrivia;
  final GetRandomNumberTrivia getRandomNumberTrivia;
  final InputConverter inputConverter;

  Future<void> _onGetTriviaForConcreteNumber(
      GetTriviaForConcreteNumber event, Emitter<NumberTriviaState> emit) async {
    final Either<Failure, int> inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    await inputEither.fold(
      (Failure l) async => emit(Error(message: invalidInputFailureMessage)),
      (int r) async {
        emit(Loading());
        final Either<Failure, NumberTrivia> failureOrTrivia =
            await getConcreteNumberTrivia(
          Params(number: r),
        );
        emit(_eitherLoadedOrErrorState(failureOrTrivia));
      },
    );
  }

  Future<void> _onGetTriviaForRandomNumber(
      GetTriviaForRandomNumber event, Emitter<NumberTriviaState> emit) async {
    emit(Loading());
    final Either<Failure, NumberTrivia> failureOrTrivia =
        await getRandomNumberTrivia(
      NoParams(),
    );
    emit(_eitherLoadedOrErrorState(failureOrTrivia));
  }

  NumberTriviaState _eitherLoadedOrErrorState(
    Either<Failure, NumberTrivia> failureOrTrivia,
  ) {
    return failureOrTrivia.fold(
      (Failure failure) => Error(message: _mapFailureToMessage(failure)),
      (NumberTrivia trivia) => Loaded(trivia: trivia),
    );
  }

  String _mapFailureToMessage(Failure failure) {
    switch (failure) {
      case ServerFailure _:
        return serverFailureMessage;
      case CacheFailure _:
        return cacheFailureMessage;
      default:
        return 'Unexpected error';
    }
  }
}

Và với lần tái cấu trúc này, chúng ta vừa hoàn thành một phần khác của Ứng dụng Number Trivia – bộ chứa logic trình bày (tôi có nên nói là P​LoC không?)​​ dưới dạng ​B​LoC.

Tiếp theo

Chúng ta đang tiến gần đến đích. Bây giờ chúng ta đã triển khai từng chút logic theo đúng nghĩa đen, đã đến lúc đặt tất cả các phần lại với nhau bằng tính năng chèn phụ thuộc và sau đó… Chúng ta sẽ không còn gì để làm ngoài việc tạo giao diện người dùng!

Viết một bình luận