Dependency Injection trong Android

by Nguyễn Văn Cường
40 views

Giới thiệu

Dependency Injection là một trong những pattern giúp code tuân thủ nguyên tắc Dependency Inversion. Làm cho một class độc lập với phụ thuộc của nó mang lại nhiều ưu điểm khi phát triển, test. Android có một số thư viện giúp việc DI trở nên dễ dàng như Dagger, Dagger Hilt. Để hiểu và sử dụng được các thư viện này trong bài này ta sẽ tìm hiểu một số khái niệm cơ bản về Dependency Injection.

Các nguyên tắc cơ bản của DI

Dependency injection là gì?

Các class thường yêu cầu tham chiếu đến các class khác. Cho ví dụ, một class Car có thể cẩn một tham chiếu đến class Engine . Những class được Car tham chiếu đến như Engine được gọi là Dependencies (sự phụ thuộc), và trong ví dụ này Car là class phụ thuộc vào một instance của class Engine để chạy.

Có ba cách để một class lấy một object nó cần:

  1. Class xây dựng phần phụ thuộc mà nó cần. Trong ví dụ ở trên, Car sẽ tạo và khởi tạo một instance Engine của riêng nó.
  2. Lấy nó từ một nơi khác. Một số Android API như là Context getters và getSystemService(), cách này hoạt động.
  3. Cung cấp nó như một parameter. Ứng dụng có thể cung cấp những sự phụ thuộc này khi class được khởi tạo hoặc pass chúng vào các function mà cần từng phụ thuộc. Trong ví dụ ở trên, contructor của Car sẽ nhận Engine như một tham số.

Tùy chọn thứ ba là Dependence injection. Với cách tiếp cận này bạn nhận sự phụ thuộc của một class bằng cách cung cấp chúng thay vì việc để instance class tự lấy chúng.

Đây là một ví dụ không dependency injection, Đại diện cho một Car mà tạo ra sự phự thuộc Engine của riêng nó trong code như này:

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Screenshot from 2021-09-30 20-12-42.png

Đây không phải là một ví dụ về DI bởi vì class Car đang xây dựng Engine của riêng nó, điều này vi phạm nguyên tắc DI (Dependence inversion). Có vấn đề bởi vì:

  • CarEngine được liên kết chặc chẽ – một instance Car sử dụng một loại Engine, và không có subclass hay triển khai thay thế nào có thể dễ sử dụng. Nếu Car tạo ra Engine của riêng nó, bạn sẽ tạo hai loại Car thay vif tái sử dụng Car cho các động cơ loại Gas and Electric.
  • Việc phụ thuộc nhiều vào Engine làm cho công việc test gặp nhiều khó khăn hơn. Car sử dụng một instance của Engine, vì vậy ngăn bạn sử dụng các bản test trong các trường hợp khác nhau.

Code sử dụng Dependence injection như dưới đây:

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Screenshot from 2021-09-30 20-12-42.png

Function main sử dụng Car. bởi vì Car phụ thuộc vào Engine, ứng dung tạo một instance của Engine và sau đó sử dụng nó để xây dựng một instacne Car. Những lợi ích của phương pháp tiếp cận sự trên DI này là:

  • Khả năng tái sử dụng Car. Bạn có thẻ pass các implementation khác nhau của Engine tới Car. Cho ví dụ, bạn có thể định nghĩa một subclass mới của Engine được gọi là ElectricEngine mà bạn muốn Car sử dụng. Nếu bạn sử dụng DI, tất cả những gì bạn cần làm là pass một instance được cập nhật ElectricEngine subclass của Engine, và Car vẫn hoạt động không có bất kỳ thay đổi thêm.
  • Dễ dàng testing Car. Bạn có thể pas một test double đến test các kịch bản khác nhau của bạn. Cho ví dụ, bạn có thể tạo một test double của Engine được gọi là FakeEngine và cấu hình nó cho các bài tests khác nhau.

Có hai cách chính để thực hiện Dependence Injection trong android:

  • Contructor injection. cách này được mô tả ở trên. Bạn pass sự phụ thuộc của class vào contructor của chính nó.
  • Field Injection( họăc Setter Injection). Một số lớp Android framework như là activity and fragment kà được khỏi tạo bởi hệ thống, vì vậy constructor injection là không thể. với field injection, Các sự phụ thuộc được khởi tạo sau khi class được tạo. Code sẽ giống như này:
class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

Dependency injection tự động nhờ thư viện

Trong ví dụ trước, ta đã tạo, cung cấp và quản lý các sự phụ thuộc của những class khác. Không cần dựa vào thư viện. Điều này được gọi là DI bằng tay, hoặc manual dependency injection. Trong ví dụ Car, chỉ có một sự phụ thuộc, nhưng nhiều sự phụ thuộc và các class có thể làm cho việc tiêm phụ thuộc thủ công trở nên tẻ nhạc hơn. Việc tiêm phụ thuộc thủ công cũng xảy ra một số vấn đề:

  • Cho các ứng dụng lớn, nhận tất cả sự phụ thuộc và kết nối chúng chính xác có thể yêu cầu một số lượng lớn mã mẫu ghi sẵn. Trong một kiến trúc nhiều lớp, để tạo một đối tượng cho một top layer, bạn phải cung cấp tất cả sự phụ thuộc của các layer dưới nó. Một ví dụ cụ thể, để build một oto thật bạn có thể cần một engine, một bộ phận truyền tải, một khung xe và các bộ phận khác; và một engine lần lượt cần xilanh và bugi.
  • Khi bạn không thể khởi tạo sự phụ thuộc trước khi pass chúng vào – cho ví dụ, khi sử dụng khởi tạo lazy hoặc xác định phạm vi đối tượng cho các luồng ứng dụng của bạn, bạn cần viết và maintain một vùng chứa tùy chỉnh( hoặc graph của Dependencies ) cái mà quản lý lifetimess của các sự phụ thuộc của bạn trong memory.

Có các thư viện giải quyết vấn đề này bằng cách tự động tiến hành tạo và cung cấp các sư phụ thuộc. Chúng phù hợp với hai loại:

  • Reflection-based, giải pháp kết nối các sự phụ thuộc tại runtime.
  • Static, giải pháp tạo code để kết nối các sự phụ thuộc tại compile time.

Dragger là một thư viện DI phổ biến cho java, kotlin và Android do Google maintain. Dragger tạo điều kiện thuận lợi cho việc sử dụng DI trong ứng dụng của bạn bằng cách tạo và quản lý graph dependencies cho bạn. Nó cung cấp cá phụ thuộc hoàn toàn static và compiletime giải quyết nhiều vấn đề về phát triển và hiêu xuấn của các giải pháp sự trên phản xạ.

Hilt là một thư viện được đề xuất của Jetpack để chèn phụ thuộc trong Android.

Kết luận

Dependence injection cung cấp cho ứng dụng của bạn những ưu điểm sau:

  • Khả năng tái sử dụng của các class, việc hoán đổi các implementation dễ dàng hơn.
  • Dễ tái cấu trúc: Các phần phụ thuộc có thể kiểm tra tại thời điểm tạo đối tượng hoặc compile time thay vì bị ẩn dưới dạng chi tiết triển khai.
  • Dễ test: Một class không quản lý các phụ thuộc của nó, vì vậy ta có thể truyền các implementation khác nhau để kiểm tra các trường hợp khác nhau của mình.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.