Flutter Clean Architecture (PART 2) – Entity & Use Case

Trần Đình Quý

Updated on:

Flutter Clean Architecture

Trong phần trước chúng ta đã được xem qua concept về clean architecture trong Flutter. Chúng ta cũng đã tạo ra 1 đống các thư mục rỗng cho presentation, domain và data layer trong app mà chúng ta đang xây dựng. Bây giờ là lúc bắt đầu thêm code vào trong các thư mục đó ^^

Bắt đầu từ đâu?

Trước khi chúng ta bắt đầu xây dựng 1 app nào thì việc design UI và UX là chuyện đầu tiên. Tôi đã thiết kế nó trong phần 1 và bạn có thể dựa vào nó để làm theo.

Phần coding thực sự sẽ bắt đầu từ phía bên trong, layer ổn định nhất. Ý tôi sẽ là domain layer, bắt đầu từ Entity.

À mà trước tiên thì chúng ta cần sử dụng 1 vài package như sau ở toàn bộ dự án nên bạn cần add nó vào pubspec.yaml trước. Các package sẽ có thể thay đổi phiên bản liên tục nên có thể bạn nên sử dụng các phiên bản mới nhất.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

  # Service locator
  get_it: ^7.6.0
  # Bloc for state management
  flutter_bloc: ^8.1.3
  # Value equality
  equatable: ^2.0.5
  # Functional programming thingies
  dartz: ^0.10.1
  # Remote API
  connectivity_plus: ^4.0.2
  http: ^1.1.0
  # Local cache
  shared_preferences: ^2.2.0
  # Freezed
  freezed: ^2.4.2
  freezed_annotation: ^2.4.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0
  build_runner: ^2.4.6

Entity

Loại data nào mà App của chúng ta tiến hành tương tác? Number Entities, tất nhiên. Để tìm hiểu xem field nào mà class này cần có thì chúng ta hãy xem response từ Numbers API. App của chúng ta sẽ hoạt động với response từ 1 số cụ thể hoặc random.
Ví dụ: http://numbersapi.com/42?json

Response:

{
  "text": "42 is the answer to the Ultimate Question of Life, the Universe, and Everything.",
  "number": 42,
  "found": true,
  "type": "trivia"
}

Chúng ta chỉ cần quan tâm tới field text và number, vì tôi nhận ra thì found luôn luôn trả về true và type luôn luôn là trivia. Nếu mà số không được tìm thấy thì chúng ta sẽ có được response dưới, nó vẩn ổn khi hiển thị lên app.

{
  "text": "123456 is an unremarkable number.",
  "number": 123456,
  "found": false,
  "type": "trivia"
}

Vì vậy chúng ta sẽ có class NumberTrivia như sau:

number_trivia.dart

import 'package:equatable/equatable.dart';

class NumberTrivia extends Equatable {
  const NumberTrivia({
    required this.text,
    required this.number,
  }) : super();

  final String text;
  final int number;

  @override
  List<Object?> get props => <Object>[text, number];
}

Use Case

Use case là nơi business logic được thực hiện. Và chắc chắn rằng sẽ không có nhiều business logic trong app của chúng ta đâu. Tất các các use case sẽ chỉ lấy dữ liệu từ repository. Và chúng ta sẽ có 2 use case là GetConcreteNumberTrivia và GetRandomNumverTrivia.

How usecase receives data

Data Flow & Error Handling

Chúng ta biết rằng Use case sẽ nhận NumberTrivia entity từ Repository và chúng sẽ truyền các entity này cho presentation layer.

Vì vậy kiểu trả về của Use case sẽ phải là Future<NumberTrivia> để xử lấy bất đồng bộ.

Rồi còn xử lý lỗi thì sao? Lựa chon tốt nhất là để exception truyền 1 cách tự do? Và sẽ catch exception một nơi nào đó trong code? Thì tôi không nghĩ như vậy.
Thay vì đó chúng ta sẽ muốn catch exception càng sớm càng tốt (trong repository) và rồi trả về Failure từ phương thức trong câu hỏi trên.

Rồi, tổng kết lại thì. Repository và Use case sẽ trả về cả NumberTrivia và Failure từ các phương thức của chúng. Vậy thì làm sao việc này có thể làm nhỉ?

Either Type

May cho chúng ta là đã có 1 package đã hỗ trợ việc này – dartz.
Tất cả những gì chúng tôi quan tâm nhằm mục đích xử lý lỗi tốt hơn là loại Either<L, R>.

Kiểu này có thể dùng để thể hiện tất cả các kiểu trả về chứa 2 kiểu cùng 1 lúc. Và nó vừa vặn cho trường hợp của chúng ta, trong đó L là Failure và R là NumberTrivia. Bằng cách này, Failure không cần phải có 1 luông riêng để xử lý flow lỗi như là exception làm. Chúng sẽ được xử lý như bất kỳ dữ liệu nào mà không cần try/catch. Hãy để lại chi tiết về cách làm việc với Either khi chúng ta cần nó trong các phần tiếp theo của khóa này.

Defining Failures

Trước khi có thể tiếp tục viết Use case, trước tiên chúng ta phải xác định Failure, vì chúng sẽ là một phần của kiểu trả về Either. Failure sẽ được sử dụng trên nhiều tính năng và layer, vì vậy hãy tạo chúng trong thư mục core và trong thư mục con là error.

Error folder structure

Sẽ có một lớp abstract Failure cơ bản mà từ đó mọi Failure cụ thể sẽ kế thừa, giống như với các Exception thông thường và class Base Exception.

Một cách để implement cái này là dùng freeze.

failure.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'failure.freezed.dart';

@freezed
class Failure with _$Failure {
	// Các lớp kế thừa Failure sẽ được implemnent ở đây
}

Viết một hợp đồng của Repository, đối với Dart là một abstract class, sẽ cho phép chúng ta viết các bài test cho các UseCase mà không cần triển khai Repository thực tế.

Thế thì, 1 hợp đồng trông như thế nào? Nó sẽ có hai phương thức – một để lấy những Trivia Number cụ thể, một phương thức khác để lấy những Number Trivia ngẫu nhiên và kiểu trả về của những phương thức này là Future<Either<Failure, NumberTrivia>>, đảm bảo rằng việc xử lý lỗi sẽ diễn ra dễ dàng!

number_trivia_repository.dart

import 'package:dartz/dartz.dart';

import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';

abstract class NumberTriviaRepository {
  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
  Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}

Get Concrete Number Trivia

Chúng ta sẽ tạo nên sườn của GetConcreteNumberTrivia.

import '../repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {

  GetConcreteNumberTrivia(this.repository);
  final NumberTriviaRepository repository;

  Future<Either<Failure, NumberTrivia>> execute(int number) {
    return repository.getConcreteNumberTrivia(number);
  }
}

Trong phần tiếp theo, chúng ta sẽ refactor code ở trên, tạo base class UseCase để giúp ứng dụng có thể mở rộng dễ dàng và thêm trường hợp sử dụng GetRandomNumberTrivia.

Phần trước: https://dev.tora-tech.com/flutter-clean-architecture-part-1/
Phần tiếp theo: https://dev.tora-tech.com/flutter-ca-tai-cau-truc-domain-layer/

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