FLUTTER CLEAN ARCHITECTURE (PART 13) – Dependency Injection

Trần Đình Quý

Flutter Clean Architecture

Chúng ta đã có tất cả các thành phần riêng lẻ của kiến trúc ứng dụng. Tuy nhiên, trước khi có thể sử dụng chúng bằng cách xây dựng giao diện người dùng (UI), chúng ta cần kết nối chúng lại với nhau. Vì mỗi class được tách rời khỏi các phụ thuộc của nó bằng cách nhận chúng thông qua constructor, chúng ta phải tìm cách đưa chúng vào bằng cách nào đó.

Injecting Dependencies

Gần như mọi class chúng ta tạo ra trong series này cho đến nay đều có một số phụ thuộc. Ngay cả trong một ứng dụng nhỏ như Number Trivia App mà chúng ta đang xây dựng, cũng có khá nhiều thứ cần cấu hình. Có nhiều package sử dụng DI khác nhau phù hợp với Flutter và như bạn có thể nhớ từ phần 2, chúng ta đang sử dụng get_it.

Bây giờ, chúng ta sẽ xem xét tất cả các lớp theo thứ tự flow từ trên xuống dưới. Trong trường hợp này, nghĩa là bắt đầu từ NumberTriviaBloc và kết thúc với các phụ thuộc bên ngoài như SharedPreferences.

Hãy thiết lập mọi thứ trong một tệp mới có tên là inject_container.dart nằm trực tiếp bên trong thư mục gốc lib. Về cơ bản, chúng ta sẽ điền vào các constructor với các đối số thích hợp. Cấu trúc cơ bản của file như sau:

injection_container.dart

final sl = GetIt.instance;

void init() {
  //! Features - Number Trivia

  //! Core

  //! External

}

Hàm init() sẽ được gọi ngay lập tức khi ứng dụng khởi động từ tệp main.dart. Bên trong function này, tất cả các class và contract sẽ được đăng ký và sau đó được “tiêm” (injected) sử dụng thể hiện singleton của GetIt được lưu trữ bên trong sl (viết tắt của service locator – trình định vị dịch vụ).

Gói get_it hỗ trợ tạo các đối tượng singleton và các factory. Vì chúng ta không lưu trữ bất kỳ trạng thái nào bên trong bất kỳ class nào, chúng ta sẽ đăng ký mọi thứ dưới dạng singleton, nghĩa là chỉ một instance của một class sẽ được tạo ra trong suốt thời gian tồn tại của ứng dụng. Sẽ chỉ có một ngoại lệ cho quy tắc này – đó là NumberTriviaBloc, theo flow, chúng ta sẽ đăng ký trước tiên.

Đăng ký Factory

Quá trình đăng ký rất đơn giản. Chỉ cần khởi tạo lớp như bình thường và truyền sl() vào mọi tham số của constructor. Như bạn có thể thấy, class GetIt có phương thức call() để tạo cú pháp dễ dàng hơn, rất giống với cách các use case của chúng ta cũng có phương thức call().

injection_container.dart

//! Features - Number Trivia
//Bloc
sl.registerFactory(
  () => NumberTriviaBloc(
    concrete: sl(),
    random: sl(),
    inputConverter: sl(),
  ),
);

Các trình presentation logic holders như Bloc không nên được đăng ký dưới dạng singleton. Chúng rất gần với giao diện người dùng (UI) và nếu ứng dụng của bạn có nhiều trang mà bạn điều hướng qua lại, bạn có thể sẽ muốn thực hiện một số thao tác dọn dẹp (như đóng các Stream của Bloc) từ phương thức dispose() của một StatefulWidget.

Việc sử dụng singleton cho các class có kiểu dọn dẹp này sẽ dẫn đến việc cố gắng sử dụng một trình giữ logic trình bày (như Bloc) với các Stream đã đóng, thay vì tạo một thể hiện mới với các Stream đang mở mỗi khi bạn cố gắng lấy một đối tượng của kiểu đó từ GetIt.

Sử dụng nội suy kiểu, lời gọi đến sl() sẽ xác định đối tượng nào nó nên truyền làm đối số hàm tạo đã cho. Tất nhiên, điều này chỉ khả thi khi kiểu được đề cập cũng đã được đăng ký. Rõ ràng là bây giờ chúng ta cần đăng ký các use case GetConcreteNumberTriviaGetRandomNumberTrivia cũng như InputConverter. Chúng sẽ không được đăng ký dưới dạng factory, thay vào đó, chúng sẽ là singleton.

Đăng ký Singleton

GetIt cung cấp cho chúng ta hai tùy chọn về singleton, đó là registerSingleton và registerLazySingleton. Sự khác biệt duy nhất giữa chúng là:

  • registerSingleton: Đăng ký một singleton ngay lập tức sau khi ứng dụng khởi động.
  • registerLazySingleton: Đăng ký một singleton chỉ khi nó được yêu cầu làm phụ thuộc cho một lớp khác.

Trong trường hợp của chúng ta, lựa chọn giữa đăng ký lazy và thường không tạo ra sự khác biệt vì Ứng dụng Number Trivia chỉ có một trang với một Bloc và một “cây phụ thuộc”. Điều này có nghĩa là ngay cả các lazy singleton cũng sẽ được đăng ký ngay lập tức. Chúng ta sẽ chọn sử dụng registerLazySingleton.

Bloc

Để giữ cho việc đăng ký được sắp xếp có tổ chức, mọi thứ sẽ được đặt dưới một comment. Việc tạo các function riêng biệt cũng có thể thực hiện được, nhưng theo quan điểm của tôi thì điều đó có thể làm giảm khả năng đọc của code nếu bạn chỉ có một vài sự đăng ký như trong trường hợp này.

injection_container.dart

//! Features - Number Trivia
...
// Use cases
sl.registerLazySingleton(() => GetConcreteNumberTrivia(sl()));
sl.registerLazySingleton(() => GetRandomNumberTrivia(sl()));

//! Core
sl.registerLazySingleton(() => InputConverter());

Repository

Trong khi InputConverter là một class độc lập, cả hai use case đều yêu cầu NumberTriviaRepository. Lưu ý rằng các use case phụ thuộc vào contract chứ không phải triển khai cụ thể. Tuy nhiên, chúng ta không thể khởi tạo một contract. Thay vào đó, chúng ta phải khởi tạo việc triển khai của repository. Điều này có thể thực hiện được bằng cách chỉ định một kiểu tham số trên phương thức registerLazySingleton.

injection_container.dart

//! Features - Number Trivia
...
// Repository
sl.registerLazySingleton<NumberTriviaRepository>(
  () => NumberTriviaRepositoryImpl(
    remoteDataSource: sl(),
    localDataSource: sl(),
    networkInfo: sl(),
  ),
);

Đoạn code này minh họa tuyệt vời tính hữu ích của sự liên kết lỏng lẻo (loose coupling). Việc phụ thuộc vào các abstraction (trừu tượng) thay vì các triển khai cụ thể không chỉ cho phép kiểm thử, mà còn cho phép thay đổi dễ dàng việc triển khai cơ bản của NumberTriviaRepository thành một triển khai khác mà không cần thay đổi các lớp phụ thuộc.

Data Sources & Network Info

Repository cũng phụ thuộc vào các contract, vì vậy chúng ta sẽ lại chỉ định một kiểu tham số theo cách thủ công.

injection_container.dart

//! Features - Number Trivia
...
// Data sources
sl.registerLazySingleton<NumberTriviaRemoteDataSource>(
  () => NumberTriviaRemoteDataSourceImpl(client: sl()),
);

sl.registerLazySingleton<NumberTriviaLocalDataSource>(
  () => NumberTriviaLocalDataSourceImpl(sharedPreferences: sl()),
);

//! Core
...
sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl(sl()));

External

Chúng ta đã di chuyển đến cuối chuỗi gọi (call chain) vào lĩnh vực của các thư viện bên thứ ba. Chúng ta cần đăng ký http.Client, DataConnectionChecker và cả SharedPreferences. Đoạn cuối cùng này hơi phức tạp một chút.

Khác với tất cả các lớp khác, SharedPreferences không thể được khởi tạo đơn giản bằng một lệnh gọi hàm tạo thông thường. Thay vào đó, chúng ta phải gọi SharedPreferences.getInstance() là một phương thức bất đồng bộ (asynchronous)! Bạn có thể nghĩ rằng chúng ta có thể làm điều này đơn giản như sau:
sl.registerLazySingleton(() async => await SharedPreferences.getInstance());
Tuy nhiên, hàm bậc cao trong trường hợp này sẽ trả về một Future, đó không phải là điều chúng ta muốn. Thay vào đó, chúng ta muốn đăng ký một thể hiện đơn giản của SharedPreferences.

Để làm điều đó, chúng ta cần chờ (await) lệnh gọi getInstance() bên ngoài quá trình đăng ký. Điều này sẽ yêu cầu chúng ta thay đổi chữ ký của phương thức init().

injection_container.dart

Future<void> init() async {
  ...
  //! External
  final sharedPreferences = await SharedPreferences.getInstance();
  sl.registerLazySingleton(() => sharedPreferences);
  sl.registerLazySingleton(() => http.Client());
  sl.registerLazySingleton(() => DataConnectionChecker());
}

Khởi chạy

Phương thức init() sẽ không tự động được gọi. Trách nhiệm của chúng ta là phải khởi tạo nó và vị trí tốt nhất để thực hiện việc khởi tạo trình định vị dịch vụ (service locator) là bên trong hàm main().

main.dart

import 'injection_container.dart' as di;

void main() async {
  await di.init();
  runApp(MyApp());
}

Tiếp theo

Dependency injection là mảnh ghép còn thiếu giữa code productioncode test, nơi mà chúng ta đã tạm thời inject các phụ thuộc một cách thủ công bằng các lớp mock. Giờ đây, khi đã triển khai trình định vị dịch vụ (service locator), chúng ta có thể bắt đầu viết các widget Flutter để sử dụng tất cả code đã viết và hiển thị một giao diện người dùng đầy đủ chức năng cho người dùng.

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