Dependency Injection trong flutter (part 1)

Trần Đình Quý

Trong quá trình làm việc với các app mobile thì mình cảm thấy việc sử dụng Dependency Injection là không thể thiếu đối với mình. Vậy thì Dependency Injection là gì và vì sao mình phải sử dụng nó?

Dependency Injection (DI)

Dependency injection là một kỹ thuật theo đó một đối tượng (hoặc static method) cung cấp các phụ thuộc của đối tượng khác. Một phụ thuộc là một đối tượng có thể được sử dụng.

Dependency Injection

Lợi ích khi sử dụng DI

  • Dễ test và viết Unit Test: Dễ hiểu là khi ta có thể Inject các dependency vào trong một class thì ta cũng dễ dàng “tiêm” các mock object vào class (được test) đó.
  • Dễ dàng thấy quan hệ giữa các object: Dependency Injection sẽ inject các object phụ thuộc vào các interface thành phần của object bị phụ thuộc nên ta dễ dàng thấy được các dependency của một object.
  • Dễ dàng hơn trong việc mở rộng các ứng dụng hay tính năng.
  • Giảm sự kết dính giữa các thành phần, tránh ảnh hưởng quá nhiều khi có thay đổi nào đó.

Vậy trong thực tế với Flutter người ta áp dụng DI như thế nào?

Việc này trên thực tể Flutter đã cung cấp sẵn cho chúng ta một giải pháp của DI đó là InheritedWidget. Tuy nhiên, InheritedWidget có một hạn chế là phải nhúng nó trực tiếp vào UI, vì vậy nếu như chúng ta khai báo nhiều dependency thì việc nesting là không thể tránh khỏi. Và lại, khi muốn lấy một instance, bạn phải bắt buộc cần có context, thứ mà không phải ở đâu cũng có mà chỉ có trong các widget.

Từ những vấn đề đó, Get It ra đời như một giải pháp thay thế tốt hơn, dễ sử dụng hơn và tách biệt phần khai báo dependency với UI.

Như các thư viện khác việc cài đặt của Get It khá dễ dàng:
https://pub.dev/packages/get_it

flutter pub add get_it

Trong dự án, tạo 1 file mới injection_container.dart

final sl = GetIt.instance;

void init() {
// Thêm các phụ thuộc ở đây
}

Function init() sẽ được gọi ngay khi ứng dụng bắt đầu chạy từ ​main.dart​. Nó sẽ nằm bên trong function đó, nơi tất cả các class và contracts sẽ được đăng ký và sau đó cũng được inject bằng cách sử dụng singleton của GetIt được lưu trữ bên trong sl (viết tắt của service locator​).

Cách đăng ký

Bây giờ chúng ta sẽ đăng ký các phụ thuộc, có 2 loại cơ bản mà chúng ta cần lưu ý:

Factory

Factory được hiểu như một nhà máy sản xuất object. Mỗi khi bạn gọi đến để lấy object thì sẽ có một instance mới được tạo ra và trả về cho bạn.
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à chuyển sl() vào mọi tham số hàm tạo. Như bạn có thể thấy, lớp GetIt có phương thức call() để tạo cú pháp dễ dàng hơn, giống như các trường hợp sử dụng của chúng ta cũng có phương thức call().

sl.registerFactory(
  () => NumberBloc(
    concrete: sl(),
    random: sl(),
    inputConverter: sl(),
  ),
);

Sử dụng nội suy kiểu​, lệnh sl() sẽ xác định đối tượng nào sẽ truyền dưới dạng đối số cho constructor. Tất nhiên, điều này chỉ có thể thực hiện được khi phụ thuộc được đề cập cũng được đăng ký. Rõ ràng là bây giờ chúng ta cần đăng ký các trường hợp sử dụng GetConcrete và GetRandom cũng như InputConverter. Chúng sẽ không được đăng ký như là ​factory​, thay vào đó, chúng sẽ là ​các singleton.

Singleton

Singleton trái ngược với factory, chỉ tạo ra một instance duy nhất kể từ khi app khởi động, sau đó nếu bất kì chỗ nào có dùng dến thì sẽ chỉ trả về instance đã tạo trước đó. Do đó xuyên suốt app, bạn sẽ chỉ sử dụng một instance của object đó mà thôi.

sl.registerSingleton<RemoteDataSource>(
  () => RemoteDataSourceImpl(sl()),
);

⛔️ Lưu ý: Presentation logic holders chẳng hạn như Bloc không nên được đăng ký là singletons. Chúng rất gần với giao diện người dùng và nếu ứng dụng của bạn có nhiều trang mà bạn điều hướng giữa chúng thì bạn phải thực hiện một số thao tác dọn dẹp (như đóng Stream của một Bloc) từ phương thức dispose() của StatefulWidget.

Có một singleton cho các class với kiểu xử lý này sẽ dẫn đến việc cố gắng sử dụng trình giữ presentation logic (chẳng hạn như Bloc) với các Stream đã đóng, thay vì tạo một instance mới với các Stream mở bất cứ khi nào bạn cố lấy một đối tượng của Bloc đó từ Get It.

Lazy-singleton

Là một biên thể của singleton, lazy-singleton thì giống như singleton, chỉ khác là nó sẽ được khởi tạo vào lần gọi lấy instance đầu tiên, chứ không phải khi app khởi động. Sử dụng nó nếu như việc tạo instance này mất thời gian, bạn không muốn app dừng ở màn hình splash quá lâu để chờ khởi tạo instance, dẫn đến việc UX của app không tốt. Chuyển cái RemoteDataSource  thành Lazy-singleton luôn nào:

sl.registerLazySingleton<RemoteDataSource>(
  () => RemoteDataSourceImpl(client: sl()),
);
sl.registerLazySingleton<Repository>(
  () => RepositoryImpl(
    remoteDataSource: sl(),
    localDataSource: sl(),
    networkInfo: sl(),
  ),
);

Có nhiều khi chúng ta phải cung cấp async vậy thì phải sử dụng như thế nào với Get It?
Ví dụ sau đây cho SharedPreferences

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

Ngoài các đăng kí như trên thì Get It còn cung cấp các phương thức register cho async function như: registerFactoryAsync, registerSingletonAsync

Khởi tạo

function init() không thể tự gọi cho chính mình được. Nhiệm vụ của chúng ta là gọi nó và nơi tốt nhất sẽ là main().

import 'injection_container.dart' as di;

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

Tạm kết

Bên trên mình đã tổng kết lại một số kiến thức về Dependency Injection của flutter qua những thông tin mình tìm kiếm được, hy vọng bài viết giúp ích được phần nào cho mọi người. Nếu có sai sót, xin hãy để lại bình luận phía bên dưới, mình sẽ cập nhật nhanh nhất có thể.

Sự ủng hộ của mọi người là động lực để mình tiếp tục cho ra những bài viết tiếp theo. Cảm ơn mọi người!

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