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.