Dependency injection với Dagger 2 - Thiết lập phạm vi (scope)

Bài viết này là một phần của loạt bài tìm hiểu về Dependency injection với Dagger 2 framework trong Android. Nó sẽ trình bày về thiết lập phạm vi (scope) - chức năng này có thể khó khăn một chút đối với những người mới tìm hiểu về Dependency injection.

Scope (phạm vi) - Những gì mà nó sẽ cung cấp?

Hầu hết các project sử dụng singleton - cho API client, database helper, analytics manager... Nhưng vì chúng ta không quan tâm đến việc khởi tạo các thể nghiệm (dependency injection framework phụ trách), nên chúng ta không cần nghĩ về việc làm thế nào để có các đối tượng khi code. Thay vào đó, anotation @Inject cung cấp cho chúng ta các thể nghiệm cần thiết.

Trong Dagger 2, cơ chế scope quan tâm về việc giữ lại các thể nghiệm duy nhất của class trong suốt phạm vi nó tồn tại. Trong thực tế, nó có nghĩa là các thể nghiệm có phạm vi trong @ApplicationScope sẽ có vòng đời sống như đối tượng Application. @ActivityScope giữ các tham chiếu trong suốt vòng đời Activity tồn tại (Chẳng hạn, chúng ta có thể chia sẻ thể nghiệm duy nhất của bất kỳ class nào giữa tất cả các fragment được gắn vào Activity này).

Tóm lại, Scope cung cấp cho chúng ta các "local singleton" mà sống lâu như phạm vi của chính nó.

Lưu ý: không có các anotation @ApplicationScope hay @ActivityScope được cung cấp mặc định trong Dagger 2. Nó phải được đặc tả khai báo trước khi sử dụng trong việc thiết lập phạm vi. Chỉ có phạm vi @Singleton là có sẵn mặc định (được cung cấp bởi chính Java).

Scope - Ví dụ thực tế

Để hiểu rõ ràng hơn về scope trong Dagger 2, chúng ta sẽ tìm hiểu các ví dụ thực tế. Chúng ta sẽ implement một phạm vi phức tạp hơn một chút so với phạm vi Application và Activity. Ứng dụng của chúng ta sẽ có 3 phạm vi:

  • @Singleton - phạm vi toàn ứng dụng
  • @UserScope - phạm vi cho các thể nghiệm của class kết hợp với việc chọn user (trong ứng dụng thực tế, nó có thể được dùng cho việc login user)
  • @ActivityScope - phạm vi cho các thể nghiệm mà sống lâu như các Activity (là các presenter trong ví dụ này)

@UserScope là điểm giới thiệu mới trong bài viết lần này. Từ góc nhìn kiến trúc, nó giúp chúng ta cung cấp một thể nghiệm user mà không cần truyền đi như một tham số intent. Ngoài ra, các class mà yêu cầu dữ liệu user trong tham số của phương thức (RepositoriesManager) có thể lấy thể nghiệm user như là một tham số của contructor (sẽ được cung cấp từ depedency graph) và có thể được khởi tạo theo nhu cầu thay vì tạo ra nó trong thời gian khởi động ứng dụng. Có nghĩa là RepositoriesManager sẽ được tạo sau khi chúng ta lấy user từ Github API.

Dưới đây là hình ảnh hình dung về scope và các component được sử dụng trong app:

Singleton (Application scope) là phạm vi sống lâu nhất (cùng thời gian sống của ứng dụng). UserScope như là một phạm vi con của ApplicationScope, có quyền truy cập đến các đối tượng của nó (chúng ta luôn có thể lấy được các đối tượng từ phạm vi cha). Tương tự với phạm vi ActivityScope (thời gian sống cùng với đối tượng Activity) - có thể lấy các đối tượng từ phạm vi UserScope và ApplicationScope.

Ví dụ vòng đời của Scope

Dưới đây là ví dụ vòng đời của Scope trong ứng dụng:

Singleton sống toàn thời gian bắt đầu từ lúc khởi chạy ứng dụng. Trong khi UserScope được tạo khi chúng ta get thực thể user từ Github API (trong các ứng dụng thực tế là khi người dùng login) và UserScope bị huỷ khi back lại SplashActivity (trong các ứng dụng thực tế là khi người dùng logout). Với mỗi user mới được chọn, một UserScope khác sẽ được tạo ra. Mỗi ActivityScope tồn tại trong suốt vòng đời đối tượng Activity của nó.

Implementation

Chúng ta có 2 cách để implement - sử dụng anotation @Subcomponent hoặc sử dụng Component depedencies. Sự khác biệt chính giữa chúng là chia sẻ object graph. Subcomponent có thể truy cập vào toàn bộ object graph từ cha của nó, trong khi Component depedency chỉ cho phép truy cập những đối tượng mà được exposed trong Component interface.

Chúng ta sẽ sử dụng cách đầu tiên với anotation Subcomponent. Nếu sử dụng Dagger 1, nó gần như tương tự việc tạo ra một subgraph từ ObjecGraph. Hơn nữa, chúng ta sẽ sử dụng quy ước đặt tên tương tự cho các method mà tạo ra một subgraph (nhưng không bắt buộc).

Hãy cùng bắt đầu từ việc implement AppComponent:

@Singleton
@Component(
        modules = {
                AppModule.class,
                GithubApiModule.class
        }
)
public interface AppComponent {

    UserComponent plus(UserModule userModule);

    SplashActivityComponent plus(SplashActivityModule splashActivityModule);

}

Nó sẽ là gốc cho các subcomponent khác: UserComponent và các ActivityComponent. Chúng ta có thể nhận thấy, tất cả các public method mà return các đối tượng từ depedency graph đều biến mất. Bởi vì chúng ta sử dụng subcomponent nên không cần expose các phụ thuộc một cách công khai - subgraph đã có quyền truy cập vào tất cả chúng.

Thay vào đó, chúng ta đã thêm vào 2 method:

  • UserComponent plus(UserModule userModule);
  • SplashActivityComponent plus(SplashActivityModule splashActivityModule);

Có nghĩa là từ AppComponent chúng ta có thể tạo ra 2 subcomponent: UserComponentSplashActivityComponent. Khi chúng là các subcomponent của AppComponent, cả hai sẽ có quyền truy cập đến các thể nghiệm được cung cấp bởi AppModuleGithubApiModule.

Lưu ý: Quy tắc đặt tên cho method này là: kiểu trả về là một subcomponent class, tên method là tuỳ ý, các paramater là những module được yêu cầu trong subcomponent này.

Như ta thấy, UserComponent cần một module khác (được truyền vào như một paramater của phương thức plus()). Với việc làm này, chúng ta đang mở rộng graph của AppComponent bằng cách thêm các đối tượng được cung cấp bởi module mới. Class UserComponent như sau:

@UserScope
@Subcomponent(
        modules = {
                UserModule.class
        }
)
public interface UserComponent {
    RepositoriesListActivityComponent plus(RepositoriesListActivityModule repositoriesListActivityModule);

    RepositoryDetailsActivityComponent plus(RepositoryDetailsActivityModule repositoryDetailsActivityModule);
}

Dĩ nhiên, anotation UserScope phải được tạo ra trước như sau:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface UserScope {
}

Từ UserComponent, chúng ta sẽ có thể tạo ra 2 subcomponent khác là : RepositoriesListActivityComponentRepositoryDetailsActivityComponent:

@UserScope
@Subcomponent(
        modules = {
                UserModule.class
        }
)
public interface UserComponent {
    RepositoriesListActivityComponent plus(RepositoriesListActivityModule repositoriesListActivityModule);

    RepositoryDetailsActivityComponent plus(RepositoryDetailsActivityModule repositoryDetailsActivityModule);
}

Điều quan trọng là, tất cả các thể nghiệm lấy từ AppComponent mà thừa kế ở AppComponent vẫn là singleton (trong Application scope). Nhưng những đối tượng được cung cấp bởi UserModule (một phần của UserComponent) sẽ là "local singleton" mà thời gian sống cùng thể nghiệm của UserComponent này. Vì vậy, mỗi khi chúng ta tạo ra một thể nghiệm UserComponent khác, phải gọi: UserComponent appComponent = appComponent.plus(new UserModule(user))

các đối tượng UserModule truyền vào là các thể nghiệm khác nhau.

Nhưng quan trọng ở đây, chúng ta phải chịu trách nhiệm về vòng đời của UserComponent. Vì vậy, chúng ta cần quan tâm đến việc khởi tạo và release nó. Trong ví dụ này, chúng ta tạo thêm 2 phương thức là việc đó:

public class GithubClientApplication extends Application {

    private AppComponent appComponent;
    private UserComponent userComponent;

    //...

    public UserComponent createUserComponent(User user) {
        userComponent = appComponent.plus(new UserModule(user));
        return userComponent;
    }

    public void releaseUserComponent() {
        userComponent = null;
    }

    //...
}

createUserComponent() đươc gọi khi chúng ta get đối tượng User từ Github API (trong SplashActivity). Và releaseUserComponent() được gọi khi chúng ta đến từ RepositoriesListActivity (tại thời điểm này, chúng ta không cần UserScope nữa).

Kết luận

Qua bài viết này, hy vọng có thể cung cấp tới các bạn cách sử dụng, thiết lập các phạm vi (Scope) trong Dagger 2.