FLUTTER CLEAN ARCHITECTURE (part 10) – Bloc & Input Conversion

Trần Đình Quý

Updated on:

Flutter Clean Architecture

Presentation layer bao gồm giao diện người dùng dưới dạng các widget và các presentation logic holders, có thể được triển khai dưới dạng ChangeNotifier, Bloc, Reducer, ViewModel, MobX Store, v.v. Trong trường hợp ứng dụng Number Trivia của chúng ta, chúng ta sẽ sử dụng package flutter_bloc để hỗ trợ triển khai mẫu BLoC.

Cài đặt IDE

Trước khi tạo các tệp và lớp cần thiết cho Bloc, bạn nên giao công việc lặp đi lặp lại cho tiện ích mở rộng VS Code hoặc plugin IntelliJ. Mặc dù bạn hoàn toàn có thể tự tạo tất cả các tệp, nhưng tốt hơn hết là chỉ cần nhấp vào một nút và để tiện ích mở rộng thực hiện công việc cho bạn.

Ngoài việc cung cấp một cách thức đơn giản để tạo các tệp Bloc, tiện ích mở rộng/plugin này còn bổ sung các đoạn code tiện dụng để sử dụng từ các widget khi xây dựng giao diện người dùng.

Event, State, Bloc and hơn nữa

Bloc, hay BLoC, là viết tắt của Business Logic Component (Thành phần Logic Nghiệp vụ). Theo Clean Architecture, nó nên được gọi là PLoC (Thành phần Logic Trình bày) nhưng tôi nghĩ vẫn nên giữ nguyên tên gọi ban đầu ?. Dù sao thì toàn bộ logic nghiệp vụ đều nằm trong tầng domain.

Nói tóm lại, Bloc là một mô hình quản lý trạng thái phản ứng, trong đó dữ liệu chỉ theo một hướng. Nó có thể được chia thành ba bước cốt lõi:

  • Event (chẳng hạn như “lấy thông tin thú vị về số cụ thể”) được gửi đi từ các Widget giao diện người dùng.
  • Bloc nhận các sự kiện và thực thi logic nghiệp vụ thích hợp (gọi các Use Case, trong trường hợp của Kiến trúc Sạch).
  • State (chứa một thể hiện NumberTrivia, chẳng hạn) được phát ra từ Bloc trở lại các Widget giao diện người dùng, các Widget này sẽ hiển thị dữ liệu mới đến.

Tạo các file cần thiết

Trong VS Code và với tiện ích mở rộng đã được cài đặt, nhấp chuột phải vào thư mục bloc và chọn “Bloc > New Bloc” từ menu.

Đặt tên các file với tiền tố “number_trivia”.

Cuối cùng, chọn sử dụng Equatable để các Event và State được tạo có tính bình đẳng giá trị ngay từ đầu.

Tiện ích mở rộng bây giờ sẽ tạo ra 3 file bình thường, mỗi file chứa một class cơ bản cho Bloc, EventState tương ứng. File thứ 4 được gọi đơn giản là bloc.dart là cái gọi là “file barrel” chỉ xuất tất cả các tệp khác. Điều này giúp việc import dễ dàng hơn trong các phần khác của lớp trình bày.

Events

Hiện tại, tệp number_trivia_event.dart chỉ chứa một base class abstract, từ đó tất cả các Event tùy chỉnh của chúng ta sẽ kế thừa. Các Widget sẽ có thể gửi các loại Event nào đến Bloc? Nhìn vào giao diện người dùng, chỉ có 2 nút – một nút để hiển thị thông tin thú vị cho một số cụ thể và một nút khác cho một số ngẫu nhiên.

Thế nên, nên có hai sự kiện. GetTriviaForConcreteNumberGetTriviaForRandomNumber. Nhưng đừng lo lắng, bởi vì bây giờ sẽ có một chút khác biệt trong cách chúng ta xử lý các sự kiện đó bên trong Bloc. Bạn sẽ thấy lý do tại sao trong giây lát nữa.

Event random sẽ chỉ là một lớp rỗng. Còn event concrete thì phải chứa một field cho số. Kiểu dữ liệu của field đó nên là gì? Có thể một số bạn sẽ ngạc nhiên, nhưng kiểu dữ liệu của field number sẽ là một String.

number_trivia_event.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class NumberTriviaEvent extends Equatable {
  NumberTriviaEvent([List props = const <dynamic>[]]) : super(props);
}

class GetTriviaForConcreteNumber extends NumberTriviaEvent {
  final String numberString;

  GetTriviaForConcreteNumber(this.numberString) : super([numberString]);
}

class GetTriviaForRandomNumber extends NumberTriviaEvent {}

Event được gửi đi từ các Widget. Widget mà người dùng nhập số vào sẽ là một TextField. Giá trị được lưu trữ bên trong một TextField luôn là một String.

Việc chuyển đổi một Chuỗi thành một số nguyên trực tiếp trong giao diện người dùng hoặc thậm chí bên trong class Event sẽ đi ngược lại những gì chúng ta đang cố gắng đạt được với Clean architecture từ trước đến nay – khả năng bảo trì, khả năng đọc và khả năng kiểm thử. Và chúng ta cũng sẽ vi phạm nguyên tắc đầu tiên của SOLID về sự tách biệt mối quan tâm.

InputConverter

Chúng ta sẽ phá vỡ truyền thống một chút và tạo một class để thực hiện chuyển đổi, một InputConverter, mà không cần tạo class abstract trước. Cá nhân tôi cảm thấy rằng việc tạo contract cho các class tiện ích đơn giản như lớp này là không cần thiết. Thêm vào đó, vì mọi class trong Dart đều có thể được triển khai như một giao diện, việc mô phỏng InputConverter trong khi kiểm thử Bloc trong phần tiếp theo sẽ vẫn dễ dàng như mô phỏng một lớp abstract.

Nó sẽ nằm bên trong presentation layer giống như NumberTriviaModel nằm bên trong data layer. Mục đích của trình chuyển đổi sẽ giống như mục đích của mô hình – không để domain layer bị vướng vào thế giới bên ngoài. Số không phải là chuỗi, giống như NumberTrivia không phải là JSON.

Chúng ta sẽ tạo một file cho nó trong một thư mục mới core/util. Nếu bạn muốn tách biệt code thành các lớp rất nghiêm ngặt, tất nhiên bạn có thể đặt nó trong core/presentation/util.

Lớp InputConverter sẽ có một phương thức duy nhất được gọi là stringToUnsignedInteger. Điều này là do ngoài việc parse string, nó cũng sẽ đảm bảo rằng số nhập vào không âm.

input_converter.dart

import 'package:dartz/dartz.dart';

import '../error/failure.dart';

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

class InvalidInputFailure extends Failure {}

Và sau khi implement nó thì sẽ trông như sau

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

Công việc của InputConverter chỉ như vậy là đủ cho mục đích hiện tại của ứng dụng. Trong phần tiếp theo, chúng ta sẽ thấy cách sử dụng InputConverter bên trong Bloc để xử lý việc chuyển đổi và tiếp tục với logic nghiệp vụ cho các event.

State

Các state được phát ra bởi Bloc là thứ điều khiển giao diện người dùng. Đã có một class cụ thể được tạo trong tệp number_trivia_state.dart. Chỉ cần đổi tên nó thành Empty.

Trong trường hợp của chúng ta, sẽ có bốn trạng thái – Empty, Loading, LoadedError. Tương tự như cách các event mang dữ liệu từ giao diện người dùng đến Bloc, các trạng thái mang dữ liệu từ Bloc đến giao diện người dùng. Trạng thái Loaded sẽ chứa một instance NumberTrivia để hiển thị dữ liệu từ đó và trạng thái Error sẽ chứa thông báo lỗi.

number_trivia_state.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

import '../../domain/entities/number_trivia.dart';

@immutable
abstract class NumberTriviaState extends Equatable {
  NumberTriviaState([List props = const <dynamic>[]]) : super(props);
}

class Empty extends NumberTriviaState {}

class Loading extends NumberTriviaState {}

class Loaded extends NumberTriviaState {
  final NumberTrivia trivia;

  Loaded({@required this.trivia}) : super([trivia]);
}

class Error extends NumberTriviaState {
  final String message;

  Error({@required this.message}) : super([message]);
}

Tiếp theo

Chúng ta đã hoàn thành những bước quan trọng trong việc thiết lập Bloc cho ứng dụng Number Trivia, chúng ta đã sẵn sàng để bắt đầu triển khai logic nghiệp vụ bên trong Bloc.

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