330 likes | 347 Views
Dagger + Android contributions. Sigitas Atkočaitis. Why Dagger. private fun setUpInjection() { navigator = Navigator() locationPermissionManager = LocationPermissionManager( this ) val latLngMapper = LatLngMapperImpl() val converter = BitmapDescriptorConverter(resources)
E N D
Dagger + Android contributions Sigitas Atkočaitis
private fun setUpInjection() { navigator = Navigator() locationPermissionManager = LocationPermissionManager(this) val latLngMapper = LatLngMapperImpl() val converter = BitmapDescriptorConverter(resources) val markerGenerator = MarkerGeneratorImpl(latLngMapper, converter) val adapter = MarkerInfoWindowAdapter(activity!! .layoutInflater) val clusterIconProvider = ClusterIconProviderImpl(converter) map = PharmaciesMapHolder(markerGenerator, latLngMapper, adapter, this, clusterIconProvider) val retriever = DependencyRetriever.from(context!!) val service = retriever.getService(LoadPharmaciesService::class.java) val dbHelper = DependencyRetriever.from(context!!).databaseHelper val pharmaciesRepository = PharmaciesRepositoryImpl(dbHelper) val prefRepository = PreferenceRepository(context!!) val expireModel = PharmaciesExpireModelImpl(prefRepository) val pharmaciesModel = LoadPharmaciesModelImpl(service, pharmaciesRepository, expireModel, Schedulers.io()) locationService = DefaultLocationService(context!!) val model = PharmaciesMapModelImpl(locationService!!, pharmaciesModel) val resolver = NetworkErrorResolver( DefaultNetworkErrorMessenger(resources)) presenter = PharmaciesMapPresenterImpl(model, AndroidSchedulers.mainThread(), resolver) }
val storageFactory = PreferenceStorageFactory(); val antennaStorage = storageFactory.create(context, Antenna::class); val amplifierStorage = storageFactory.create(context, Amplifier::class); val attenuationStorage = storageFactory.create(context, Attenuation::class); val antennaRepository = AntennaRepository(antennaStorage); val amplifierRepository = AmplifierRepository(amplifierStorage); val attenuationRepository = AttenuationRepository(attenuationStorage); val bandRegionStorage = storageFactory.create(context, BandRegion::class); val colorRangeStorage = storageFactory.create(context, ColorRange::class); val colorRepository = ColorRepositoryImpl(colorRangeStorage); val audioToneStorage = storageFactory.create(context, AudioTone::class); val audioToneRepository = AudioToneRepository(audioToneStorage); val logDirRepository = SessionDirRepositoryImpl( storageFactory.create(context, String::class)); val tileDirRepository = TilesDirRepository( storageFactory.create(context, String::class)); val formStorage = storageFactory.create(context, SettingsForm::class); val bandRegionHolderStorage = storageFactory.create(context, BandRegionHolder::class); val bandListProvider = DefaultBandListProvider(); val bandRegionRepository = BandRegionListRepositoryImpl(bandRegionStorage, bandRegionHolderStorage, bandListProvider); val settingsFormRepository = SettingsFormRepositoryImpl( antennaRepository, amplifierRepository, attenuationRepository, audioToneRepository, bandRegionRepository, colorRepository, logDirRepository, tileDirRepository, formStorage ); val computationScheduler = Schedulers.computation(); val mainScheduler = AndroidSchedulers.mainThread(); val model = SettingsFragmentModel( bandRegionRepository, antennaRepository, amplifierRepository, attenuationRepository, colorRepository, audioToneRepository, formProvider, settingsFormRepository, computationScheduler ); val calculator = AutoColorRangeCalculatorImpl(); val selectedListStorage = storageFactory.create(context, BandRegionHolder::class); val bandRegionListRepository = BandRegionListRepositoryImpl(bandRegionStorage, selectedListStorage, bandListProvider); return SettingsFragmentPresenter( model, calculator, mainScheduler, updateChecker, SeewaveApplication.getRecorderManager(context).getFileManager(), bandRegionListRepository );
Why Dagger More Testable Code More Readable Code More Reusable Code Reduced Dependency Carrying
Main Dagger parts @Component @SubComponent @Module @ContributesAndroidInjector
@DaggerScope(BaseApplication::class) @Component( modules = [ AndroidSupportInjectionModule::class, AppModule::class, ActivitiesModule::class, SchedulerModule::class, NetworkModule::class, ImageModule::class ] ) interface AppComponent : AndroidInjector<BaseApplication> { @Component.Builder abstract class Builder : AndroidInjector.Builder<BaseApplication>() } open class BaseApplication : DaggerApplication() { override fun applicationInjector(): AndroidInjector<out DaggerApplication> = DaggerAppComponent.builder().create(this) }
@Module abstract class AppModule private constructor() { @Binds abstract fun bindContext(app: BaseApplication): Context @Module companion object { @JvmStatic @Provides fun provideResources(context: Context): Resources = context.resources } }
@Module abstract class SchedulerModule private constructor() { @Module companion object { @JvmStatic @Provides @MainThread fun provideMainScheduler(): Scheduler = AndroidSchedulers.mainThread() @JvmStatic @Provides @ComputationThread fun provideComputationScheduler(): Scheduler = Schedulers.computation() @JvmStatic @Provides @IoThread fun provideIoScheduler(): Scheduler = Schedulers.io() @JvmStatic @Provides @NetworkThread fun provideIoScheduler(): Scheduler = Schedulers.io() } } @Qualifier annotation class NetworkThread
@Module abstract class ImageModule private constructor() { @Binds @Reusable abstract fun bindImageLoader(loader: GlideImageLoader): ImageLoader }
@Module abstract class NetworkModule private constructor() { @Module companion object { ... @JvmStatic @Provides @DaggerScope(BaseApplication::class) fun provideRetrofitClient( @NetworkThread networkScheduler: Scheduler ): Retrofit = Retrofit.Builder() .addCallAdapterFactory( RxJava2CallAdapterFactory.createWithScheduler(networkScheduler) ) .baseUrl(BuildConfig.API_ENDPOINT) .build() } } @Scope annotation class DaggerScope(val value: KClass<*>)
@Module abstract class ActivitiesModule private constructor() { @DaggerScope(MainActivity::class) @ContributesAndroidInjector(modules = [MainActivityModule::class]) abstract fun bindMainActivity(): MainActivity } class MainActivity : DaggerAppCompatActivity(){ ... }
@dagger.Module abstract class MainActivityModule private constructor() { @DaggerScope(MainFragment::class) @ContributesAndroidInjector(modules = [MainScreen.Module::class]) abstract fun bindArticleListFragment(): MainFragment }
object MainScreen { interface MainPresenter : BasePresenter<MainScreen.View> {...} interface View {...} @dagger.Module abstract class Module private constructor() { @Binds @DaggerScope(MainFragment::class) abstract fun bindPresenter(presenter: MainPresenterImpl): MainPresenter @dagger.Module companion object { @Provides @JvmStatic @DaggerScope(MainFragment::class) fun provideService(retrofit: Retrofit) = retrofit.create<NewsService>() } } }
class MainPresenterImpl @Inject constructor( private val service: NewsService, @MainThread private val scheduler: Scheduler ) : BasePresenterImpl<MainScreen.View>(), MainScreen.MainPresenter { ... } class MainFragment : DaggerFragment(), MainScreen.View { @Inject lateinit var presenter: MainScreen.MainPresenter override fun onCreateView(...): View? { ... presenter.takeView(this) return fragmentView } ... }
Unit tests You don’t have to use Dagger for single-class unit tests If you want to write a small unit test that tests only one @Inject-annotated class, you don’t need to use Dagger to instantiate that class. If you want to write a traditional unit test, you can directly call the @Inject-annotated constructor and methods and set the @Inject-annotated fields, if any, passing fake or mock dependencies directly, just as you would if they weren’t annotated. https://google.github.io/dagger/testing
class MainPresenterImplTest { private val service = mock<NewsService>() private val view = mock<MainScreen.View>() private val scheduler = TestScheduler() private val presenter = MainPresenterImpl(service, scheduler) private val articles = listOf<Article>(mock()) private val articlesResponse = ArticlesResponse("200", 0, articles) @Test fun exampleUnitTest() { given(service.getArticleList()).willReturn(Single.just(articlesResponse)) presenter.takeView(view) presenter.onViewCreated() scheduler.triggerActions() verify(view).showArticles(articles) } }
@DaggerScope(BaseApplication::class) @Component( modules = [ AndroidSupportInjectionModule::class, InstrumentationModule::class, AppModule::class, ActivitiesModule::class, InstrumentationSchedulerModule::class, NetworkModule::class, ImageModule::class ] ) interface TestAppComponent : AndroidInjector<TestBaseApplication> { @Component.Builder abstract class Builder : AndroidInjector.Builder<TestBaseApplication>() }
open class TestApplicationRunner : AndroidJUnitRunner() { @Throws(Exception::class) override fun newApplication(loader: ClassLoader, className: String, context: Context): Application { return super.newApplication(loader, TestBaseApplication::class.java.name, context) } } class TestBaseApplication : BaseApplication() { override fun applicationInjector(): AndroidInjector<out DaggerApplication> { return DaggerTestAppComponent.builder().create(this) } }
@Module abstract class InstrumentationSchedulerModule private constructor() { @Module companion object { ... @JvmStatic @NetworkThread @Provides fun provideNetworkScheduler(executor: IdlingThreadPoolExecutor): Scheduler = Schedulers.from(executor) @JvmStatic @Provides @DaggerScope(BaseApplication::class) fun provideIdlingThreadPoolExecutor(): IdlingThreadPoolExecutor = IdlingThreadPoolExecutor(...) } }
class ExampleInstrumentedTest { @get:Rule var rule = ActivityTestRule(MainActivity::class.java) @Test fun exampleTest() { onView(withId(R.id.listRecyclerView)).check(matches(hasMinimumChildCount(1))) onView(withId(R.id.listRecyclerView)).perform(ViewActions.swipeUp()) } }
Alternative: Koin.io https://insert-koin.io/
@DaggerScope(BaseApplication::class) @Component( modules = [ AndroidSupportInjectionModule::class, AppModule::class, ActivitiesModule::class, SchedulerModule::class, NetworkModule::class, ImageModule::class ] ) interface AppComponent : AndroidInjector<BaseApplication> { @Component.Builder abstract class Builder : AndroidInjector.Builder<BaseApplication>() }
open class BaseApplication : Application() { override fun onCreate() { startKoin(KoinApplication.create().apply { androidLogger(Level.DEBUG) androidContext(this@BaseApplication) modules( PluginsModule(), AppModule(), SchedulerModule(), NetworkModule(), ImageModule(), MainScreen.Module() ) }) } }
@Module abstract class AppModule private constructor() { @Binds abstract fun bindContext(app: BaseApplication): Context @Module companion object { @JvmStatic @Provides fun provideResources(context: Context): Resources = context.resources } }
fun AppModule() = module { single { androidContext().resources } }
@dagger.Module abstract class Module private constructor() { @Binds @DaggerScope(MainFragment::class) abstract fun bindPresenter(presenter: MainPresenterImpl): MainPresenter @dagger.Module companion object { @Provides @JvmStatic @DaggerScope(MainFragment::class) fun provideService(retrofit: Retrofit) = retrofit.create<NewsService>() } }
fun AppModule()= module { single { androidContext().resources } } fun Module() = module { factory<Presenter> { MainPresenter(service = get(), scheduler = get("main")) } factory<NewsService> { get<Retrofit>().create()} }
@Module abstract class NetworkModule private constructor() { @Module companion object { @JvmStatic @Provides @DaggerScope(BaseApplication::class) fun provideRetrofitClient( @NetworkThread networkScheduler: Scheduler ): Retrofit = Retrofit.Builder() .addCallAdapterFactory( RxJava2CallAdapterFactory.createWithScheduler(networkScheduler) ) .baseUrl(BuildConfig.API_ENDPOINT) .build() } }
fun AppModule() = module { single { androidContext().resources } } fun Module() = module { factory<Presenter> { MainPresenter(service = get(), scheduler = get("main")) } factory<NewsService> { get<Retrofit>().create() } } fun NetworkModule() = module { single { Retrofit = Retrofit.Builder() .addCallAdapterFactory( RxJava2CallAdapterFactory.createWithScheduler(get("network")) ) .baseUrl(BuildConfig.API_ENDPOINT) .build() } }
Singleton scopes based only on lifecycle, no hierarchy Unstable, prone to change API