Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
Lightweight dependency injection: a Dagger használata Android alatt
Az Android SDK-ja - bár Java alapú, illetve most már sokkal inkább a Kotlin az Android natív nyelve - a rendszer szükségleteinek megfelelően lett kialakítva, így számos olyan package hiányzik belőle (például a java.beans csak részlegesen található meg), melyre a Spring alapvetően támaszkodik. Így tehát komoly problémákba fogunk ütközni az alkalmazás fordítása/indítása során. A Spring-nek egyébként is csak egy pici része a DI, így ha másra nincs szükségünk, érdemes más eszköz után néznünk. Szerencsére a Dagger nevű DI framework fejlesztői gondoltak az Android fejlesztőkre is, így a Dagger teljes Android támogatással és nyílt forráskóddal érhető el, maga a Google szárnyai alatt.
dependencies {
// ...
implementation "com.google.dagger:dagger:2.16"
implementation "com.google.dagger:dagger-android:2.16"
implementation "com.google.dagger:dagger-android-support:2.16"
// ...
annotationProcessor "com.google.dagger:dagger-compiler:2.16"
annotationProcessor "com.google.dagger:dagger-android-processor:2.16"
}
A Dagger (illetve a jelenleg is aktív fejlesztés alatt álló Dagger 2) kisméretű, fordítás-idejű dependency injection framework. Mit is jelent ez számunkra? Nos, a Spring-en (de főleg Spring Boot-on) nevelkedett kollégáknak eleinte néhány ősz hajszálat, kisebb memóriafoglalást, gyors alkalmazás-kontextus indulást és már a fordításnál kibukó kontextus konfigurációs hibákat. Mindezt azért, mert míg a Spring futásidőben, reflection használatával építi fel az alkalmazás kontextusát, addig a Dagger előzetes annotáció-feldolgozás útján generál különböző factory és egyéb osztályokat, amelyeket aztán az alkalmazás maga fogja a belépési ponton példányosítani. Röviden tehát nincs reflection okozta overhead, nincs “reflection-magic”, egyszerű példányosítások történnek az alkalmazás indulásakor. Az ősz hajszálak a Dagger által is használt, annotáció alapú konfigurálásból, illetve az általa okozott hamis nyugalom érzetből erednek, miszerint az annotációk elhelyezésével gyakorlatilag végeztünk is, kész a kontextus.
Építőkövek
Ez egyrészt már csak azért sem igaz, mert Spring alatt is szükség van valamilyen konfigurációra, másrészt meg, bár az elv ugyanaz, a megvalósítás némiképp különbözik egymástól a két fronton. Az eltérés már csak a két framework eltérő működési elvéből fakadóan is nyilvánvalónak tűnik - párhuzamot vonni azonban lehet közöttük: a Dagger @Component
-jei nagyjából a Spring ApplicationContext-jeinek (a DI container-ek) felelnek meg, a “bean”-eket Dagger-ben @Module
osztályokban definiáljuk, ahol az azokat létrehozó factory metódusokat @Provides
annotációval jelöljük meg. Felhívnám a figyelmet a @Component
annotáció Dagger-beli szerepére, mely éles ellentétben áll a Spring-ben megszokott használatával - a @Component
annotáció nem hoz létre bean-eket Dagger alatt, hanem egy olyan komponenst generál, amivel a kontextus elindítható. Az injektálás és a bean-ek “inline” megjelölése egyaránt az @Inject
annotációval történik az osztály konstruktorán, ami a Spring után elég szokatlan lehet, továbbá a Spring-gel ellentétben a Dagger alapértelmezetten prototype scope-on példányosít, amit a @Singleton
annotációval akadályozhatunk meg. Az @Inject
annotációval megjelölt bean-ek felkutatása automatikusan történik, extra konfigurációt (Spring-ben @ComponentScan
) nem igényel, de az azonos típusú bean-ek listába való összegyűjtését automatikusan nem tudja megoldani - kézzel persze meg lehet, erről majd később. Csak hogy a fentiek tiszták legyenek, alább néhány példa látható, összehasonlítva a Spring és a Dagger megoldását.
Inline singleton bean létrehozás Springben
@Component
public class CachingEntryNetworkRequestAdapter implements EntryAdapter {
private EntryAdapter entryNetworkRequestAdapter;
private EntrySummaryPageCacheWriteHelper entrySummaryPageCacheWriteHelper;
private EntryDAO entryDAO;
@Autowired
public CachingEntryNetworkRequestAdapter(
@Qualifier("entryNetworkRequestAdapter") EntryAdapter entryNetworkRequestAdapter,
EntrySummaryPageCacheWriteHelper entrySummaryPageCacheWriteHelper,
LeafletLocalCacheDatabase leafletLocalCacheDatabase) {
this.entryNetworkRequestAdapter = entryNetworkRequestAdapter;
this.entrySummaryPageCacheWriteHelper = entrySummaryPageCacheWriteHelper;
this.entryDAO = leafletLocalCacheDatabase.entryDAO();
}
// ...
}
Ugyanez Daggerben
@Singleton // prototype-hoz hagyjuk el ezt az annotációt
public class CachingEntryNetworkRequestAdapter implements EntryAdapter {
private EntryAdapter entryNetworkRequestAdapter;
private EntrySummaryPageCacheWriteHelper entrySummaryPageCacheWriteHelper;
private EntryDAO entryDAO;
// az Inject egyben hordozza a Spring Component/Service annotációjának
// és az Autowired annotációnak funkcionalitását
@Inject
public CachingEntryNetworkRequestAdapter(
@Named("entryNetworkRequestAdapter") EntryAdapter entryNetworkRequestAdapter,
EntrySummaryPageCacheWriteHelper entrySummaryPageCacheWriteHelper,
LeafletLocalCacheDatabase leafletLocalCacheDatabase) {
this.entryNetworkRequestAdapter = entryNetworkRequestAdapter;
this.entrySummaryPageCacheWriteHelper = entrySummaryPageCacheWriteHelper;
this.entryDAO = leafletLocalCacheDatabase.entryDAO();
}
// ...
}
Központosított konfiguráció - Spring
@Configuration
public class ApplicationModule {
private static final String DATABASE_NAME = "leaflet_local_cache";
@Bean
public LeafletLocalCacheDatabase providesLeafletLocalCacheDatabase() {
return Room.databaseBuilder(application, LeafletLocalCacheDatabase.class, DATABASE_NAME)
.build();
}
}
Központosított konfiguráció - Dagger
@Module
public class ApplicationModule {
private static final String DATABASE_NAME = "leaflet_local_cache";
// ...
@Provides
@Singleton
public LeafletLocalCacheDatabase providesLeafletLocalCacheDatabase() {
return Room.databaseBuilder(application, LeafletLocalCacheDatabase.class, DATABASE_NAME)
.build();
}
}
Mint említettem, listába összegyűjteni az azonos interface-hez tartozó beaneket a Dagger nem tudja, de rá lehet venni, hogy megtegye. Ehhez a @Module
osztályban vegyünk fel egy új metódust, tegyük rá a @Provides
annotációt, opcionálisan nevezzük át a @Named
annotációval, a metódus paramétereibe pedig vegyük fel az implementációkat, amiket egy listába szeretnénk rendezni. A metódus visszatérése természetesen List
@Module
public class ViewModelFactoryModule {
private static final String REPOSITORIES = "repositories";
@Provides
@Singleton
@Named(REPOSITORIES)
public List<Repository> provideRepositories(
CategoryRepository categoryRepository, DocumentRepository documentRepository,
EntryRepository entryRepository, SupportRepository supportRepository) {
return Arrays.asList(categoryRepository, documentRepository,
entryRepository, supportRepository);
}
// ...
}
További érdekesség, de fontos megemlíteni, hogy a Dagger nem tud azzal mit kezdeni, ha egy interfészbe próbálunk injektálni egy konkrét implementációt. Megoldás persze van rá: egy abstract @Module
osztályra lesz szükségünk, benne kizárólag @Binds
annotációval ellátott abstract metódusokkal. Minden metódus egy-egy összerendelést fog definiálni az interface és az implementáció között, méghozzá az alábbi módon:
@Module
public abstract class RepositoryModule {
@Binds
public abstract CategoryRepository bindCategoryRepository(
CategoryRepositoryImpl categoryRepository);
// ...
}
A kontextus felépítése
A kontextus létrehozásához először is szükségünk lesz a konfigurációs osztályokra, amiket @Module
annotációval jelölünk meg. Bár ilyen osztályokra nem feltétlenül van szükség (ha inline hozunk létre minden beant az @Inject
annotációval), de mivel most Androidhoz készítünk konfigurációt, egy @Module
osztályra mindenképp szükségünk lesz. Ebben a modulban kell megadnunk azokat az Activity (és ha használunk, akkor Fragment) osztályokat, amik számára biztosítani szeretnénk a dependency injectiont. Az osztály ez esetben abstract lesz, a tényleges implementációt a Dagger fogja létrehozni a fordítás során. Minden Activity és Fragment osztályhoz létre kell hoznunk egy-egy abstract metódust az alább látható módon, és ellátni őket a @ContributesAndroidInjector
annotációval. Ha a központosított konfiguráció mellett döntöttünk, vegyül fel a további @Module
osztályokat, és a @Provides
annotáció használatával hozzuk létre a beaneket.
@Module
public abstract class LeafletMobileApplicationModule {
@ContributesAndroidInjector
abstract MainActivity mainActivity();
// ...
@ContributesAndroidInjector
abstract EntryListFragment entryListFragment();
}
A következő lépés a @Component
interface létrehozása lesz, mely gyakorlatilag a kontextus containereként fog szolgálni. Fontos kihangsúlyozni, hogy megint csupán egy interface-re lesz szükségünk, az implementációt ismét a Dagger biztosítja. A @Component
annotáció attribútumaként meg kell adnunk a dependency injectionben résztvevő modul osztályokat - nem Android alkalmazás esetén ezen a ponton már majdnem meg is állhatunk. A @Component
annotált interface-ünkhöz még adjunk hozzá néhány metódust, melyek lehetőséget adnak majd a @Component
által bekötött beanekhez való hozzáférésre (bár ez jelen esetben nem annyira szükséges). Ha ezzel megvagyunk, fordítsuk le az alkalmazást, egyelőre a @Component
interface bekötése nélkül - ezzel le fog generálódni az implementációja, amivel aztán a belépési ponton létre tudjuk hozni a kontextust. Ha a @Module
osztályok egyike sem vár konstruktoron keresztül függést, akkor a Dagger a @Component
interface-ből generált implementációban elhelyez egy .create() metódust, amivel azonnal létrehozható a container. Ellenkező esetben a .builder() metódus meghívásával egy builder példányt kapunk, metódusaival a @Module
osztályok egy-egy példányát tudjuk a kontextusban elhelyezni - ha valamelyiknek nincs függése, azt automatikusan tudja ez esetben is példányosítani. A végén a .build() metódussal készítjük el a kontextust.
@Singleton
@Component(modules = {
ApplicationModule.class,
BridgeClientConfigurationModule.class,
BridgeServiceConfigurationModule.class,
LeafletMobileApplicationModule.class,
RepositoryModule.class,
SpannableConfigurationModule.class,
ViewModelFactoryModule.class})
public interface LeafletMobileApplicationComponent
// erre az extendre a következő szekcióban visszatérünk
extends AndroidInjector<LeafletMobileApplication> {
}
További lépések Android alatt
Android alkalmazás esetén egy picit még tovább kell mennünk. A korábban említett @Component
interface-ünket alakítsuk át, hogy az kiterjessze a generikus AndroidInjector interface-t - típusparamétere egy android.app.Application implementáció kell, legyen. Egy ilyen Application minden Android alkalmazásban példányosítva van alapból, ez indítja a main activity-t. A saját implementációra azért lesz szükség, mert azzal fogjuk a Dagger-t utasítani a container példányosítására - a fentebb említett @Component
implementáció meghívásával. Ha csak Activity osztályaink vannak, még implementáljuk ezzel az osztállyal a HasActivityInjector interface-t is, továbbá adjunk hozzá az osztályhoz egy DispatchingAndroidInjector mezőt is, majd jelöljük meg az @Inject
annotációval. Az interface egy activityInjector() nevű metódus implementálását várja majd tőlünk, itt szimplán adjuk vissza a fentebb létrehozott mezőt. Amennyiben support fragment osztályokat is használunk (és persze azok számára is biztosítani akarjuk a dependency injectiont), még implementálnunk kell itt a HasSupportFragmentInjector interface-t is, hasonló módon, mint az Activity osztályok kapcsán jártunk el. Hogy ebben az osztályban is megtörténjen az injektálás, a komponens .build() vagy .create() hívása után hívjunk egy .inject(this)-t is. Fontos még megjegyezni, hogy az Application implementációt be kell jegyezni az AndroidManifest.xml file-ban: az application node android:name attribútuma értékének adjuk meg az osztály nevét. Ha az osztályt ugyanabba a package-be tettük, ami a manifest node package attribútumában szerepel, akkor az érték .Osztálynév lesz, ellenkező esetben az említett package-hez viszonyított relatív package hivatkozás (szintén ponttal kezdve), majd végül szintén .osztálynév.
public class LeafletMobileApplication extends Application
implements HasActivityInjector, HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Activity> dispatchingAndroidActivityInjector;
@Inject
DispatchingAndroidInjector<Fragment> dispatchingAndroidFragmentInjector;
@Override
public void onCreate() {
super.onCreate();
DaggerLeafletMobileApplicationComponent.builder()
.applicationModule(new ApplicationModule(this))
.bridgeClientConfigurationModule(new BridgeClientConfigurationModule())
.bridgeServiceConfigurationModule(new BridgeServiceConfigurationModule())
.spannableConfigurationModule(new SpannableConfigurationModule(this))
.viewModelFactoryModule(new ViewModelFactoryModule())
.build()
.inject(this);
}
@Override
public AndroidInjector<Activity> activityInjector() {
return dispatchingAndroidActivityInjector;
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingAndroidFragmentInjector;
}
}
Ha eddig mindent jól csináltunk, az alkalmazás már le kell forduljon. Ahogy azt említettem is már, a Dagger fordítási időben “köti be” az alkalmazást, így ha a fordítás sikertelen, ellenőrizzük a Gradle logokat: valahol ott fog figyelni a Dagger annotáció feldolgozójától származó üzenet, miszerint a konfiguráció érvénytelen. Egy-két esetben az üzenet nem feltétlenül teljesen egyértelmű, így komolyabb elbaltázás eredménye több órányi Google/SoF túrás lehet - a legjobb, ha nagyon odafigyelünk a konfiguráció felépítése során és akkor nem nem lesz gond.
Mindenesetre még nem értünk a bekötés végére, mivel ugyan kontextusunk már van, de azt még nem érjük el az Activity és Fragment osztályokban - így tehát ez még hátravan. Két apróságra lesz szükségünk: egyrészt az adott Activity vagy Fragment osztályban hozzunk létre egy-egy fieldet, melynek típusa természetesen a szükséges függés típusa. Ha a konfigurációban létrehoztuk a megfelelő bindolásokat, interface-eket is megadhatunk. Fontos, hogy a field nem lehet private, ez esetben a Dagger nem fogja tudni elvégezni az injektálást. Szerencsére nem kell public láthatóságra sem állítanunk, illetve fölösleges setterekre sem lesz szükség, package-private már megfelel. A fieldet annotáljuk az @Inject
annotációval, ezzel jelezve a Dagger számára, hogy ide majd függést kell beszúrnia. Végül az onCreate() callback metódusban hívjuk meg Activity osztály esetén az AndroidInjection, “support” Fragment esetén az AndroidSupportInjection osztály statikus inject() metódusát - paramétere mindkét esetben this lesz. Ezzel sikeresen a végére értünk a Dagger bekötésének.
public class MainActivity extends AppCompatActivity {
// ...
@Inject
LocalCacheUpdaterService localCacheUpdaterService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AndroidInjection.inject(this);
// ...
}
// ...
}
Dagger referencia implementáció (Leaflet Mobile)
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.