Dagger

Biblioteki

  |   14 min czytania
(Dagger 2.21)

Wstęp

Proces budowania większości aplikacji niezależnie od środowiska polega na tworzeniu różnego typu obiektów, które często wymagają innych zależności i nierzadko mogą być współdzielone. Ręczna inicjalizacja wszystkich zależności bywa kosztowna, czasochłonna oraz może zwiększać nadmiarowość powtarzającego się kodu (boilerplate). Wzorzec Wstrzykiwanie zależności (Dependency Injection) jest realizacją paradygmatu Odwrócenia sterowania (Inversion of Control) i przejmuje odpowiedzialność inicjalizowania obiektów poprzez wstrzykiwanie oczekiwanych zależności w odpowiednie miejsca. Na podstawie zdefiniowanych komponentów i modułów Dagger dokonuje analizy zależności oraz generuje kod odpowiedzalny za wiązanie obiektów, a jego użycie opiera się wyłącznie na wykorzystaniu adnotacji i kontroli kompilacji w celu analizy i weryfikacji zależności. Dzięki temu proces testowania, refaktoryzacji i dostęp do współużytkowanych instancji jest uproszczony.

Wstrzykiwanie

Obiekt oznaczony adnotacją @Inject oczekuje inicjalizacji poprzez wstrzyknięcie zależności z poziomu komponentu. Jeśli kilka instancji tego samego typu ma zostać wstrzyknięte wówczas należy dokonać rozróżnienia za pomocą adnotacji @Named o wartości zgodnej z deklaracją w module. Istnieje także możliwość definicji własnego kwalifikatora przy użyciu adnotacji @Qualifier w definicji adnotacji.

public class InjectClass {

    //just inject
    @Inject 
    String dependency;

    //the same type so must be recognized by some qualifiers
    @Inject @Named("triangle") 
    Dependency dependencyRed;

    @Inject @Named("quarter") 
    Dependency dependencyBlue;

    @Inject @Type("oval") 
    Dependency dependencyOrange;
}

@Qualifier
@Retention(RUNTIME)
public @interface Type {

    //qualifier annotations to use like @Named, for example:
    //@Inject @Type("type") 
    SimpleDependency dependency
}

Moduł

Klasa oznaczona jako @Module staje się modułem, którego zadaniem jest deklaracja i konfiguracja dostarczanych przez niego zależności co odbywa się przy użyciu metod oznaczonych adnotacją @Provides. To jaka zależność ma zostać wstrzyknięta rozpoznawane jest przez komponent na podstawie zgodności typów i kwalifikatora dlatego nazwa metody nie ma znaczenia. Jednak dobrą praktyką jest przyjęcie stałego schematu nazewnictwa. Dodatkowo adnotacja @Singleton zapewnia, że instancja obiektu będzie singletonem w obrębie zakresu.

//create main app module to provide Application and Context
@Module
public class AppModule {

    private Application application;

    public AppModule(Application application) {
        this.application = application;
    }

    @Provides @Singleton
    Application providesApplication() {
        return application;
    }
}

//some manager module
@Module
public class ManagerModule {

    private String url;

    public ManagerModule(String name) {
        this.url = name;
    }

    @Provides @Named("red")
    SimpleDependency provideSimpleDependencyRed() {
        SimpleDependency.Builder builder = new SimpleDependency.Builder();
        builder.setColor("red").setShape("oval");
        return builder.build();
    }

    @Provides @Named("blue")
    SimpleDependency provideSimpleDependencyBlue() {
        SimpleDependency.Builder builder = new SimpleDependency.Builder();
        builder.setColor("blue").setShape("triangle");
        return builder.build();
    }

    @Provides @Singleton
    ComplexDependency provideComplexDependency(NetworkManager networkDependency) {
        return new ComplexDependency(networkDependency);
    }

    @Provides @Singleton
    NetworkManager provideNetworkDependency() {
        return new NetworkManager(url, 80);
    }

    @Provides @Singleton
    SharedPreferences providesSharedPreferences(Application application) {
        return PreferenceManager.getDefaultSharedPreferences(application);
    }
}

Komponent

Komponent buduje zależności zadeklarowane w modułach oraz umożliwia ich dostarczenie przez zainteresowane klasy. Aby stworzyć komponent należy oznaczyć klasę adnotacją @Component, wskazać źródła zależności w postaci listy modułów oraz stworzyć metodę przyjmującą jako argument klasę do której mogą zostać wstrzyknięte zależności. Zachodzi tutaj silne typowanie w związku z czym nie możliwe jest deklaracja klasy bazowej jako parametu. Każda aplikacja wymaga posiadania komponentu bazowego, który najlepiej stworzyć w głównej klasie aplikacji. Jeśli któryś z modułów komponentu posiada konstruktor argumentowy wówczas inicjalizacja komponentu zachodzi przy użyciu budowniczego builder, a w przeciwnym wypadku wystarczy wywołanie metody create.

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

    void inject(MainActivity activity);
    //add more inject methods for activity, fragment or service
    //Dagger relies on strongly typed classes so do not pass base class
}

//initialize component
public class App extends Application {

    private AppComponent appComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        //DaggerAppComponent is generated class with name based on Dagger prefix and component name
        appComponent = DaggerAppComponent.builder()
                //set list of modules that are included in component
                .appModule(new AppModule(this)) //builder method name correspond to module name
                .managerModule(new ManagerModule("androidcode.pl"))
                .build();

        //if modules of component doesn't have any constructor with arguments than this build could be
        //appComponent = DaggerAppComponent.create()
    }

    public AppComponent getAppComponent() {
        return appComponent;
    }
}

Wstrzykiwanie zależności oznaczonych jako @Inject w klasie docelowej zachodzi poprzez wywołanie metody komponentu (jeśli został on wcześniej zainicjalizowany).

public class MainActivity extends AppCompatActivity {

    @Inject @Named("red") 
    SimpleDependency simpleDependencyRed;

    @Inject @Named("blue") 
    SimpleDependency simpleDependencyBlue;

    @Inject 
    ComplexDependency complexDependency;

    @Inject 
    SharedPreferences sharedPreferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //MainActivity shouldn't know anything about how it is injected
        //so main depedency injection principle is broken
        ((App) getApplication()).getAppComponent()
                .inject(this);

        //do something with dependencies
        simpleDependencyRed.getInfo();
        simpleDependencyBlue.getInfo();
        complexDependency.getInfo();
        sharedPreferences.contains("KEY");
    }
}

Zakres

Komponenty i moduły w Dagger działają w podanym zakresie (domyślnie w obszarze całej aplikacji) w obrębie którego dostępne są zależności. Zakres jest swego rodzaju etykietą, któego rolą jest informowanie programisty o przeznaczeniu i cyklu życia danej zależności czy komponentu oraz kompilatora w celu weryfikacji zgodności. Aby stworzyć zakres należy zdefiniować adnotację oznaczoną jako @Scope.

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

    //informs about custom lifecycle time
    //use this annotations in cases like @Singleton
}

Komponent zależny

Rozszerzenie funckjonalności komponentu może zostać zrealizowane za pomocą wskazania zależności w postaci komponentu bazowego w komponencie pochodnym. Innymi słowy komponent rozszerzający jest swego rodzaju delegatem i poza modułami przyjmuje także zależność komponentu, który jest dla niego dostawcą. Komponent bazowy dostarcza API w postaci metod zwracających dany oczekiwany typ zależności (podobnie jak robi to moduł).

@Module
public class UserModule {

    private final UserActivity activity;

    public UserModule(UserActivity activity) {
        this.activity = activity;
    }

    @Provides @UserScope
    public UserManager providesUserDependency() {
        return new UserManager("global");
    }
}

@UserScope
@Component(dependencies = AppComponent.class, modules = UserModule.class)
public interface UserComponent {

    void inject(UserActivity userActivity);
}

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

    //remove injection methods if child performs this and no need to use only base component
    void inject(MainActivity activity);

    //methods return dependencies
    @Named("red") 
    SimpleDependency simpleDependencyRed();

    @Named("blue") 
    SimpleDependency simpleDependencyBlue();

    Application application();

    ComplexDependency complexDependency();

    NetworkManager networkManager();

    SharedPreferences sharedPreferences();
}

public class UserActivity extends AppCompatActivity {

    @Inject 
    UserManager manager; //from dependent component

    @Inject 
    SharedPreferences sharedPreferences; //from parent component

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);
        
        //get parent component
        AppComponent appComponent = ((App) getApplication()).getAppComponent();
        
        //create child component
        UserComponent userComponent = DaggerUserComponent.builder()
                .appComponent(appComponent)
                .userModule(new UserModule(this))
                .build();
        
        //inject
        userComponent.inject(this);

        //do something with dependencies
        manager.getConfiguration();
        sharedPreferences.contains("KEY");
    }
}

Podkomponent

Rozwinięcie grafu drzewa może odbywać się przy użyciu podkomponentów, które rozszerzają zależności komponentu bazowego co sprawia, że komponenty te są ze sobą powiązane. Aby stworzyć komponent, który będzie podkomponentem należy oznaczyć go adnotacją @Subcomponent, dodać fabrykę w komponencie bazowym zwracającą podkomponent oraz dokonać inicjalizacji we właściwym zakresie.

@UserScope
@Subcomponent(modules = UserModule.class)
public interface UserComponent {

    void inject(UserActivity userActivity);
}

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

    //inject methods
    void inject(MainActivity activity);
    
    //for subcomponent
    UserComponent userSubcomponent(UserModule module);
}

public class UserActivity extends AppCompatActivity {

    @Inject 
    UserManager manager; //from subcomponent

    @Inject 
    SharedPreferences sharedPreferences; //from parent component

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        ((App) getApplication()).getAppComponent()
                .userSubcomponent(new UserModule(this))
                .inject(this);

        //do something with dependencies
        manager.getConfiguration();
        sharedPreferences.contains("KEY");
    }
}

Fabryka

Jedną z trudności pisania aplikacji z użyciem Dagger w środowisku Android jest to, że klasy komponentów (Activity, Fragment, Service itp) są inicjalizowane przez system operacyjny co wymusza wstrzykiwanie w metodach cyklu życia. Aby zredukować powtarzający się w ten sposób kod odpowiedzialny za pobieranie komponentu i wstrzykiwanie instancji pól oraz wyeliminować problem łamania zasady znajomości sposobu działania konterera zależności przez klasę wywołującą można posłużyć się klasami z pakietu dagger.android. W tym celu należy dostarczyć do podstawowego komponentu aplikacji moduł zawierający fabrykę abstrakcyjną wiążącą podkomponenty z klasami wywołań i wstrzyknąć zbudowane zależności w głównej klasie aplikacji.

//create subcomponent for each calling class
@Subcomponent(modules = ManagerModule.class)
public interface MainComponent extends AndroidInjector<MainActivity> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<MainActivity> {
        //some additional config
    }
}

@UserScope
@Subcomponent(modules = UserModule.class)
public interface UserComponent extends AndroidInjector<UserActivity> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<UserActivity> {
        
        public abstract Builder userModule(UserModule module);

        @Override
        public void seedInstance(UserActivity instance) {
            //additional config like specify module constructor
            userModule(new UserModule()); 
        }
    }
}

//create factory module for subcomponents, if needed create one module for each component
@Module(subcomponents = {MainComponent.class, UserComponent.class})
public abstract class BinderComponentsModule {

    @Binds @IntoMap @ClassKey(MainActivity.class)
    abstract AndroidInjector.Factory<?> bindMainActivityInjectorFactory(MainComponent.Builder builder);

    @Binds @IntoMap @ClassKey(UserActivity.class)
    abstract AndroidInjector.Factory<?> bindUserActivityInjectorFactory(UserComponent.Builder builder);
}

//install AndroidInjectionModule from library and add own BinderComponentsModule
@Singleton
@Component(modules = {AndroidInjectionModule.class, AppModule.class, ManagerModule.class, BinderComponentsModule.class})
public interface AppComponent {

    void inject(App app);
}

//implement HasActivityInjector and inject DispatchingAndroidInjector
//for other Android components do in similar same way, e.g. for Fragment use HasFragmentInjector
public class App extends Application implements HasActivityInjector {

    @Inject
    DispatchingAndroidInjector<Activity> dispatchingActivityInjector;

    @Override
    public void onCreate() {
        super.onCreate();
        DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                .managerModule(new ManagerModule("androidcode.pl"))
                .build()
                .inject(this);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return dispatchingActivityInjector;
    }
}

//now just call AndroidInjection.inject(this) to perform injection
public class UserActivity extends AppCompatActivity {

    @Inject 
    UserManager manager;

    @Inject 
    SharedPreferences sharedPreferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this); //must be call before super.onCreate
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

         //do something with dependencies
    }
}

Jeśli podkomponenty i ich klasy budowniczych nie wymagają dodatkowej konfiguracji wówczas wykorzystanie adnotacji @ContributesAndroidInjector w metodach modułu zwracających klasę wywołującą spowoduje automatyczne ich utworzenie.

@Module
public abstract class BinderActivityModule {

    @ContributesAndroidInjector
    abstract MainActivity mainActivity();

    @ContributesAndroidInjector(modules = UserModule.class)
    abstract UserActivity userActivity();
}

//install AndroidInjectionModule from library and add own BinderActivityModule
@Component(modules = {AndroidInjectionModule.class, AppModule.class, BinderActivityModule.class})
public interface AppComponent {

    void inject(App app);
}

Ponadto Dagger dostarcza rozszerzone klasy komponentów DaggerApplication, DaggerActivity, DaggerFragment, DaggerService, DaggerBroadcastReceiver, DaggerContentProvider, które jeszcze bardziej upraszczają tworzenie aplikacji.