FLUTTER CLEAN ARCHITECTURE (PART 8) – Local Data Source

Trần Đình Quý

Flutter Clean Architecture

Kho lưu trữ (Repository) có một phụ thuộc tiếp theo là Nguồn dữ liệu cục bộ (local Data Source) được sử dụng để lưu vào bộ nhớ đệm dữ liệu lấy từ API từ xa. Chúng ta sẽ triển khai nó bằng cách sử dụng shared_preferences.

Khi nói đến lưu trữ dữ liệu local, chúng ta có rất nhiều lựa chọn. Chúng ta chọn sử dụng shared_preferences bởi vì chúng ta không lưu trữ nhiều dữ liệu, chỉ một NumberTrivia duy nhất được chuyển đổi thành JSON.

Chúng ta sẽ tạo lớp triển khai (implementation class) bên dưới lớp trừu tượng (abstract class).

number_trivia_local_data_source.dart

class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource {
  final SharedPreferences sharedPreferences;

  NumberTriviaLocalDataSourceImpl({@required this.sharedPreferences});

  @override
  Future<NumberTriviaModel> getLastNumberTrivia() {
    // TODO: implement getLastNumberTrivia
    return null;
  }

  @override
  Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache) {
    // TODO: implement cacheNumberTrivia
    return null;
  }
}

Hãy tập trung vào phương thức getLastNumberTrivia trước. Khi gọi nó, nó sẽ trả về NumberTriviaModel đã được lưu vào bộ nhớ đệm từ shared preferences, tất nhiên là nếu nó đã được lưu trước đó. Tuy nhiên, mô hình này sẽ được lưu trữ như thế nào bên trong preferences? Và làm thế nào chúng ta có thể kiểm tra tất cả những điều này?

Object và Shared Preferences

Việc lưu trữ các đối tượng phức tạp như NumberTrivia bên trong shared preferences chỉ có thể thực hiện được ở định dạng chuỗi (string). Do đó, giá trị được trả về từ đối tượng SharedPreferences được mô phỏng sẽ là một chuỗi JSON, giống như phản hồi API là một chuỗi JSON. Từ các phần trước, bạn đã biết cách tốt nhất để làm việc với JSON trong các thử nghiệm là sử dụng các bộ dữ liệu cố định (fixtures)!

Chúng ta có thể sử dụng các bộ dữ liệu cố định (fixtures) đã tạo trước đó không? Vâng, có thể. Sử dụng chúng chắc chắn sẽ không phá vỡ bất cứ điều gì, nhưng điều đó không nhất thiết áp dụng cho các ứng dụng phức tạp hơn. Tại sao? Các tệp trivia.json và trivia_double.json chứa một số trường và giá trị chỉ có trong phản hồi API.

trivia.json

{
  "text": "Test Text",
  "number": 1,
  "found": true,
  "type": "trivia"
}

Khi NumberTriviaModel được chuyển đổi thành JSON (điều này cũng sẽ xảy ra trong nguồn local data source), phương thức toJson sẽ được chạy để tạo ra một Map chỉ chứa các field text và number. Map được mã hóa JSON này sẽ được lưu trong shared preferences.

number_trivia_model.dart

Map<String, dynamic> toJson() {
  return {
    'text': text,
    'number': number,
  };
}

Vì vậy, hãy tạo một fixture khác để mô phỏng NumberTriviaModel đã được lưu trữ. File mới sẽ được đặt trong thư mục test/fixtures với tên là trivia_cached.json.

trivia_cached.json

{
  "text": "Test Text",
  "number": 1
}

getLastNumberTrivia

Vì kiểu trả về của phương thức là Future và SharedPreferences có lẽ là thư viện lưu trữ cục bộ đồng bộ duy nhất hiện có, chúng ta sẽ sử dụng Future.value để trả về một Future đã hoàn thành.

number_trivia_local_data_source.dart

@override
Future<NumberTriviaModel> getLastNumberTrivia() {
  final jsonString = sharedPreferences.getString('CACHED_NUMBER_TRIVIA');
  // Future which is immediately completed
  return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString)));
}

Hãy đi ngay vào giai đoạn refactor (tái cấu trúc mã). Tôi không thích việc sử dụng những chuỗi “magic”, như ‘CACHED_NUMBER_TRIVIA’, vì vậy hãy tạo một hằng số với cùng tên đó và sử dụng nó trong cả tệp production (tệp mã sản phẩm) và test (tệp mã kiểm thử).

number_trivia_local_data_source.dart

const CACHED_NUMBER_TRIVIA = 'CACHED_NUMBER_TRIVIA';

class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource {
...
}

Chúng ta không thể chỉ dựa vào việc luôn có sẵn một phiên bản được lưu trong bộ nhớ đệm của NumberTrivia cuối cùng. Điều gì sẽ xảy ra nếu người dùng khởi chạy ứng dụng lần đầu tiên mà không có kết nối Internet? Trong trường hợp đó, Repository sẽ ngay lập tức chuyển sang SharedPreferences và chúng sẽ trả về null.

Chào đón người dùng đầu tiên với sự cố ứng dụng chắc chắn sẽ không phải là một trải nghiệm tốt! Để ngăn chặn bất kỳ sự cố nào xảy ra, chúng ta sẽ ném ra một CacheException có kiểm soát. Nếu bạn nhớ từ phần trước, ngoại lệ này được bắt trong Repository và trả về Left(CacheFailure()).

implementation.dart

@override
Future<NumberTriviaModel> getLastNumberTrivia() {
  final jsonString = sharedPreferences.getString('CACHED_NUMBER_TRIVIA');
  if (jsonString != null) {
    return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString)));
  } else {
    throw CacheException();
  }
}

cacheNumberTrivia

implementation.dart

@override
Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache) {
  return sharedPreferences.setString(
    CACHED_NUMBER_TRIVIA,
    json.encode(triviaToCache.toJson()),
  );
}

Tiếp theo

Trong phần này, chúng ta đã triển khai lớp NumberTriviaLocalDataSource bằng cách sử dụng Test-Driven Development (TDD – Phát triển hướng kiểm thử). Phần còn lại cuối cùng của tầng dữ liệu là nguồn dữ liệu từ xa (remote Data Source), mà chúng ta sẽ triển khai tiếp theo.

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