Flutter Clean Architecture (Part 5) – Contracts trong Data Sources

Trần Đình Quý

Flutter Clean Architecture

Repository là bộ não của data layer trong app. Nó xử lý dữ liệu từ remote và local Data Source, quyết định Data Source nào nên được sử dụng, và đó cũng là nơi cache data theo chính sách cache đã được quy định.

Trong phần trước, chúng ta đã tìm hiểu cấu trúc cơ bản của data layer và hôm nay, đã đến lúc bắt đầu triển khai data layer ngay từ cốt lõi của nó – từ NumberTriviaRepository, đồng thời tạo contract cho các phần phụ thuộc của nó.

Thiết lập Contract

Interface, abstract class, hay bất kì thứ gì tương tự. Mọi ngôn ngữ đều có một cách vận hành có thể giống nhau có thể khác nhau xoay quanh nó. Điều quan trọng là chúng đã có 1 contract mà Repository đã thực hiện triển khai. Bằng cách này, Use case giao tiếp với Repository không cần phải biết gì về cách nó hoạt động như thế nào.

Chúng ta sẽ tạo một file mới trong data/repository cho tính năng number_trivia, file này sẽ chứa một class cụ thể NumberTriviaRepositoryImpl.

number_trivia_repository_impl.dart

import 'package:dartz/dartz.dart';

import '../../../../core/error/failure.dart';
import '../../domain/entities/number_trivia.dart';
import '../../domain/repositories/number_trivia_repository.dart';

class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
  @override
  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
    // TODO: implement getConcreteNumberTrivia
    return null;
  }

  @override
  Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
    // TODO: implement getRandomNumberTrivia
    return null;
  }
}

Repository cần Data Source ở cấp thấp hơn để lấy dữ liệu thực tế từ đó.

Những thứ phụ thuộc vào Repository

Trong phần này, chúng ta sẽ chỉ tạo contract cho tất cả các phần phụ thuộc của Repository. Điều này sẽ cho phép chúng ta mô phỏng chúng một cách dễ dàng mà không cần phải bận tâm đến việc triển khai chúng – điều đó sẽ có trong các phần tiếp theo.

Nếu mà chúng ta chỉ có Data Source có đủ không? Sau tất cả, chúng ta sẽ lưu trữ cache NumberTrivia mới nhất để đảm bảo người dùng nhìn thấy nội dung nào đó ngay cả khi họ ngoại tuyến. Điều này có nghĩa là chúng ta cũng cần có cách để tìm hiểu về trạng thái hiện tại của kết nối mạng. Vì tôi muốn giữ code của mình độc lập với thế giới bên ngoài nhất có thể nên chúng ta sẽ không đưa bất kỳ thư viện bên thứ 3 nào vào để kết nối trực tiếp vào Kho lưu trữ. Thay vào đó, chúng ta sẽ tạo một class NetworkInfo.

Network Info

network_info.dart

abstract class NetworkInfo {
  Future<bool> get isConnected;
}

Remote Data Source

Interface chung của NumberTriviaRemote DataSource sẽ gần giống với Interface của Repository – nó sẽ có các phương thức getConcreteNumberTrivia và getRandomNumberTrivia. Tuy nhiên, như chúng ta đã thảo luận ở phần trước, kiểu trả về sẽ khác.

Chúng ta đang ở biên giữa thế giới bên ngoài và ứng dụng của chúng ta, vì vậy chúng ta muốn giữ điều này đơn giản. Sẽ không có Either<Failure, NumberTrivia> mà thay vào đó, chúng ta sẽ chỉ trả về một NumberTriviaModel đơn giản (được chuyển đổi từ JSON). Lỗi sẽ được xử lý bằng cách ném Exception. Việc xử lý dữ liệu “ngu ngốc” này và chuyển đổi nó sang loại Either sẽ là trách nhiệm của Repository.

number_trivia_remote_data_source.dart

import '../models/number_trivia_model.dart';

abstract class NumberTriviaRemoteDataSource {
  /// Calls the http://numbersapi.com/{number} endpoint.
  ///
  /// Throws a [ServerException] for all error codes.
  Future<NumberTriviaModel> getConcreteNumberTrivia(int number);

  /// Calls the http://numbersapi.com/random endpoint.
  ///
  /// Throws a [ServerException] for all error codes.
  Future<NumberTriviaModel> getRandomNumberTrivia();
}

Exceptions and Failures

Cả hai function này sẽ đưa ra ServerException khi response không có mã 200 OK, nhưng chẳng hạn như mã 404 KHÔNG TÌM THẤY. Vấn đề là hiện tại chúng ta không có bất kỳ ServerException nào, vì vậy hãy tạo nó để sử dụng nó.

ServerException có thể được chia sẻ trên nhiều tính năng, vì vậy chúng ta sẽ đặt nó bên trong file core/error/exception.dart. Trong khi thực hiện việc đó, chúng ta cũng sẽ tạo một CacheException sẽ được local data source bắn ra.

exception.dart

class ServerException implements Exception {}

class CacheException implements Exception {}

Vì chúng ta đang xử lý các Exception, nên chúng ta cũng hãy tạo các Failure. Hãy nhớ rằng Repository sẽ nắm bắt các Exception và trả về chúng bằng cách sử dụng loại Either là Failure. Vì lý do này, các loại Failure thường ánh xạ chính xác tới các loại Exception.

Failure.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'failure.freezed.dart';

@freezed
class Failure with _$Failure {
  const factory Failure.serverFailure() = ServerFailure;

  const factory Failure.cacheFailure() = CacheFailure; 
}

Local Data Source

Cho đến nay, các phương pháp chúng ta tạo luôn là lấy dữ liệu, cho dù là Entity hay Model. Họ cũng được chia thành các Trivia Number cụ thể hoặc số ngẫu nhiên. Chúng ta sẽ phá vỡ mô hình này bằng Number Trivia Local Data Source.

Ở đây, chúng ta cũng sẽ cần đưa dữ liệu vào cache và chúng ta cũng sẽ không quan tâm liệu chúng ta đang xử lý các Trivia Number về số cụ thể hay ngẫu nhiên. Đó là vì chính sách bộ nhớ đệm (được triển khai bên trong Repository) sẽ đơn giản – luôn lưu vào bộ nhớ đệm và truy xuất thông tin Number Trivia cuối cùng mà nhận được từ Remote Data Source.

number_trivia_local_data_source.dart

import '../models/number_trivia_model.dart';

abstract class NumberTriviaLocalDataSource {
  /// Gets the cached [NumberTriviaModel] which was gotten the last time
  /// the user had an internet connection.
  ///
  /// Throws [NoLocalDataException] if no cached data is present.
  Future<NumberTriviaModel> getLastNumberTrivia();

  Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache);
}

Cài đặt Repository

Hãy tiếp tục và thêm tất cả các field cần thiết và tham số constructor vào NumberTriviaRepositoryImpl.

number_trivia_repository_impl.dart

import 'package:dartz/dartz.dart';

import '../../../../core/error/failure.dart';
import '../../../../core/platform/network_info.dart';
import '../../domain/entities/number_trivia.dart';
import '../../domain/repositories/number_trivia_repository.dart';
import '../datasources/number_trivia_local_data_source.dart';
import '../datasources/number_trivia_remote_data_source.dart';

class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
  final NumberTriviaRemoteDataSource remoteDataSource;
  final NumberTriviaLocalDataSource localDataSource;
  final NetworkInfo networkInfo;

  NumberTriviaRepositoryImpl({
    @required this.remoteDataSource,
    @required this.localDataSource,
    @required this.networkInfo,
  });

  @override
  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
    // TODO: implement getConcreteNumberTrivaia
    return null;
  }

  @override
  Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
    // TODO: implement getRandomNumberTrivia
    return null;
  }
}

Tiếp theo

Đã có khá nhiều thứ đã được hoàn thành cho đến phần này. Chúng ta đã tạo 3 contract cho các phần phụ thuộc của Repository. Bởi vì chúng ta luôn triển khai mọi thứ “từ trong ra ngoài”, phần tiếp theo sẽ nói về việc triển khai Repository thực hiện công việc của nó.

Phần trước: https://dev.tora-tech.com/flutter-ca-tong-quan-data-layer-model/

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