Share
Embed
Dagger 2 on production
Presentation about Dagger 2 which I gave at Mobilization 2015 - http://2015.mobilization.pl/
Source code is available: https://github.com/frogermcs/GithubClient
More about DI/Dagger 2:
Migration from Dagger to Dagger 2 - http://frogermcs.github.io/dagger-1-to-2-migration/
Intro to DI - http://frogermcs.github.io/dependency-injection-with-dagger-2-introdution-to-di/
Dagger 2 API - http://frogermcs.github.io/dependency-injection-with-dagger-2-the-api/
Scopes in Dagger 2 - http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/
Dagger 2 - graph creation performance - http://frogermcs.github.io/dagger-graph-creation-performance/
Mirosław Stanek
October 17, 2015
Transcript
90 slides
-
How to start with Dagger 2 • The Future of
Dependency Injection with Dagger 2 (Jake Wharton) https://goo.gl/PPqT51 • DAGGER 2 - A New Type of dependency injection (Gregory Kick) https://goo.gl/UVBvaH • Bunch of posts about Dagger 2 (DI, basics, scopes, performance) http://frogermcs.github.io/ -
public class SplashActivity extends BaseActivity { private SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RestAdapter.Builder restAdapterBuilder = new RestAdapter.Builder() .setClient(new OkClient(MyOkHttpClient.getInstance())) .setEndpoint(getString(R.string.endpoint)); RestAdapter restAdapter = restAdapterBuilder.build(); GithubApiService githubApiService = restAdapter.create(GithubApiService.class); UserManager userManager = new UserManager(githubApiService); presenter = new SplashActivityPresenter(this, Validator.getInstance(), userManager); } } without DI/Dagger 2 -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } with DI/Dagger 2 -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } with DI/Dagger 2 -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } No magic happens here Initialization takes place elsewhere with DI/Dagger 2 -
Initialization/usage separation SplashActivityModule SplashActivity SplashActivityPresenter AppModule Application Validator ApiModule OkhttpClient
RestAdapter GithubApiService UserManager SplashActivtity SplashActivityPresenter @Inject SplashActivtityPresenter SplashActivity Validator UserManager @Inject Initialization Usage -
@Module public class ApiModule { @Provides @Singleton OkHttpClient provideOkHttpClient()
{ OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.setConnectTimeout(60 * 1000, TimeUnit.MILLISECONDS); okHttpClient.setReadTimeout(60 * 1000, TimeUnit.MILLISECONDS); return okHttpClient; } @Provides @Singleton RestAdapter provideRestAdapter(Application application, OkHttpClient okHttpClient) { RestAdapter.Builder builder = new RestAdapter.Builder(); builder.setClient(new OkClient(okHttpClient)) .setEndpoint(application.getString(R.string.endpoint)); return builder.build(); } @Provides @Singleton GithubApiService provideGithubApiService(RestAdapter restAdapter) { return restAdapter.create(GithubApiService.class); } } @Module Example -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } DI with Dagger 2 @Module public class SplashActivityModule { private SplashActivity splashActivity; public SplashActivityModule(SplashActivity splashActivity) { this.splashActivity = splashActivity; } @Provides @ActivityScope SplashActivity provideSplashActivity() { return splashActivity; } @Provides @ActivityScope SplashActivityPresenter provideSplashActivityPresenter(Validator validator, UserManager userManager) { return new SplashActivityPresenter(splashActivity, validator, userManager); } } ? -
SplashActivityModule SplashActivity SplashActivityPresenter SplashActivity Component SplashActivtity SplashActivityPresenter @Inject @ActivityScope @Component(
modules = SplashActivityModule.class, dependencies = AppComponent.class ) public interface SplashActivityComponent { SplashActivity inject(SplashActivity splashActivity); } -
SplashActivityComponent SplashActivtity SplashActivityPresenter @Inject @Generated("dagger.internal.codegen.ComponentProcessor") public final class DaggerSplashActivityComponent implements
SplashActivityComponent { private Provider<Validator> getValidatorProvider; private Provider<UserManager> getUserManagerProvider; private Provider<SplashActivityPresenter> provideSplashActivityPresenterProvider; private MembersInjector<SplashActivity> splashActivityMembersInjector; private void initialize(final Builder builder) { //... } @Override public SplashActivity inject(SplashActivity splashActivity) { splashActivityMembersInjector.injectMembers(splashActivity); return splashActivity; } //... } -
MembersInjector SplashActivityComponent SplashActivtity SplashActivityPresenter @Inject @Generated("dagger.internal.codegen.ComponentProcessor") public final class SplashActivity_MembersInjector
implements MembersInjector<SplashActivity> { private final MembersInjector<AppCompatActivity> supertypeInjector; private final Provider<SplashActivityPresenter> presenterProvider; //... @Override public void injectMembers(SplashActivity instance) { if (instance == null) { throw new NullPointerException(/*...*/); } supertypeInjector.injectMembers(instance); instance.presenter = presenterProvider.get(); } //... } -
Factory<SplashActivityPresenter> SplashActivity Component MembersInjector @Generated("dagger.internal.codegen.ComponentProcessor") public final class SplashActivityModule_SplashActivityPresenterFactory implements
Factory<SplashActivityPresenter> { private final SplashActivityModule module; private final Provider<Validator> validatorProvider; private final Provider<UserManager> userManagerProvider; //... @Override public SplashActivityPresenter get() { SplashActivityPresenter provided = module.splashActivityPresenter( validatorProvider.get(), userManagerProvider.get() ); if (provided == null) { throw new NullPointerException(/*...*/); } return provided; } //... } -
Alternative for scope instances or singletons ScopedProvider <SplashActivityPresenter> Factory<SplashActivityPresenter> SplashActivityModule
SplashActivity SplashActivityPresenter SplashActivity Component MembersInjector SplashActivtity SplashActivityPresenter @Inject ScopedProvider <SplashActivityPresenter> SplashActivity Component MembersInjector SplashActivtity SplashActivityPresenter @Inject SplashActivityPresenter single instance 1st inject 2nd inject -
SplashActivityModule SplashActivity SplashActivityPresenter SplashActivity Component SplashActivtity SplashActivityPresenter @Inject @ActivityScope @Component(
modules = SplashActivityModule.class, dependencies = AppComponent.class ) public interface SplashActivityComponent { SplashActivity inject(SplashActivity splashActivity); } -
@Singleton @Component(modules = { AzimoAppModule.class, //... }) public interface AzimoAppComponent
{ void inject(AzimoApplication app); void inject(IntentUriHandlerActivity intentUriHandlerActivity); void inject(MyContactsActivity myContactsActivity); void inject(SelectItemActivity selectItemActivity); void inject(ProgressFragment progressFragment); void inject(GcmRegistrationService gcmRegistrationService); void inject(SyncConfigService syncConfigService); void inject(GcmMessagingService gcmMessagingService); void inject(LocaleChangeReceiver localeChangeReceiver); void inject(ReferrerReceiver referrerReceiver); void inject(ReminderReceiver reminderReceiver); void inject(RateSessionExpiredDialog rateSessionExpiredDialog); void inject(LeaveReviewDialog leaveReviewDialog); void inject(EnjoyingAppDialog enjoyingAppDialog); void inject(MakeSuggestionsDialog makeSuggestionsDialog); //… } Production app example -
What about base class injections? @Singleton @Component(modules = { AzimoAppModule.class,
//... }) public interface AzimoAppComponent { void inject(Application app); void inject(BaseActivity baseActivity); void inject(BaseFragment baseFragment); void inject(Service service); void inject(BroadcastReceiver broadcastReceiver); void inject(BaseDialogFragment baseDialogFragment); } -
public class BaseActivity extends AppCompatActivity { @Inject AnalyticsTracker analyticsTracker;
} public class SplashActivity extends BaseActivity { @Inject Validator validator; } SplashActivtity SplashActivityPresenter @Inject BaseActivtity AnalyticsTracker @Inject -
public class BaseActivity extends AppCompatActivity { @Inject AnalyticsTracker analyticsTracker;
} public class SplashActivity extends BaseActivity { @Inject Validator validator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getComponent().inject(this); } } Subclass injection -
@Generated("dagger.internal.codegen.ComponentProcessor") public final class SplashActivity_MembersInjector implements MembersInjector<SplashActivity> { private final
MembersInjector<AppCompatActivity> supertypeInjector; private final Provider<SplashActivityPresenter> presenterProvider; //... @Override public void injectMembers(SplashActivity instance) { if (instance == null) { throw new NullPointerException(/*...*/); } supertypeInjector.injectMembers(instance); instance.presenter = presenterProvider.get(); } //... } -
public class BaseActivity extends AppCompatActivity { @Inject AnalyticsTracker analyticsTracker;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getComponent().inject(this); } } public class SplashActivity extends BaseActivity { @Inject Validator validator; } Super class injection -
Super class injection • ComponentReflectionInjector<T> https://gist.github.com/konmik/ 6ac725fa7134402539c4 • Uses
Reflection • Performance drawback is about 0.013 ms per injection on slow device -
• 0.013 ms per injection (50-methods component, tested on Samsung
Galaxy S) • 60 FPS = 16ms per frame • => ~100 reflected injections per frame • (1000 direct injections per frame) ComponentReflectionInjector -
ComponentReflectionInjector public class BaseActivity extends AppCompatActivity { @Inject AnalyticsTracker
analyticsTracker; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); componentReflectionInjector.inject(this); } } public class SplashActivity extends BaseActivity { @Inject Validator validator; } Super class injection -
But still no… @Singleton @Component(modules = { AzimoAppModule.class, //... })
public interface AzimoAppComponent { void inject(Application app); void inject(BaseActivity baseActivity); void inject(BaseFragment baseFragment); void inject(Service service); void inject(BroadcastReceiver broadcastReceiver); void inject(BaseDialogFragment baseDialogFragment); } -
Direct injection public class SplashActivity { @Inject Validator validator;
//... } SplashActivtity Validator -
Lazy injection public class SplashActivity { @Inject Lazy<Validator> lazyValidator;
//... } SplashActivtity Validator Lazy<Validator> 1st get() call 2nd get() call get() SplashActivtity Dependencies Graph Validator Lazy<Validator> -
Lazy injection public class SplashActivity { @Inject Lazy<Validator> lazyValidator;
//... } SplashActivtity Validator Lazy<Validator> 1st get() call nth get() call get() SplashActivtity Dependencies Graph Validator Lazy<Validator> Same instance like in 1st call … -
Launch time without lazy loading: 650ms SplashActivity Crashlytics 200ms Mixpanel
Google Analytics 100ms Rest Client 150ms 200ms SplashActivity Crashlytics 200ms Mixpanel Google Analytics 100ms Rest Client 150ms 200ms Launch time with lazy loading: 100ms Those will be Lazy-loaded Lazy injection app launch time optimizations More optimization hints -> http://frogermcs.github.io/dagger-graph-creation-performance/ -
Provider injection public class SplashActivity { @Inject Provider<Validator> validatorProvider;
//... } 1st call 2nd call get() (instance 1) SplashActivtity Dependencies Graph Validator Provider<Validator> Validator Validator Provider<Validator> get() (instance 2) SplashActivtity Dependencies Graph Validator -
Provider injection 1st get() call nth get() call … get()
(instance 1) SplashActivtity Dependencies Graph Validator Provider<Validator> Validator Validator Provider<Validator> get() (instance n) SplashActivtity Dependencies Graph Validator New instance every time public class SplashActivity { @Inject Provider<Validator> validatorProvider; //... } -
@Generated("dagger.internal.codegen.ComponentProcessor") public final class SplashActivityModule_SplashActivityPresenterFactory implements Factory<SplashActivityPresenter> { private final
SplashActivityModule module; private final Provider<Validator> validatorProvider; private final Provider<UserManager> userManagerProvider; //... private void initialize(final Builder builder) { //… this.validatorProvider = Validator_Factory.create(/*...*/) this.userManagerProvider = ScopedProvider.create( UserManager_Factory.create(/*...*/) ); //... } //... } -
@Generated("dagger.internal.codegen.ComponentProcessor") public final class SplashActivityModule_SplashActivityPresenterFactory implements Factory<SplashActivityPresenter> { private final
SplashActivityModule module; private final Provider<Validator> validatorProvider; private final Provider<UserManager> userManagerProvider; //... private void initialize(final Builder builder) { //… this.validatorProvider = Validator_Factory.create(/*...*/) this.userManagerProvider = ScopedProvider.create( UserManager_Factory.create(/*...*/) ); //... } //... } -
ScopedProvider public final class ScopedProvider<T> implements Provider<T> { private static
final Object UNINITIALIZED = new Object(); private final Factory<T> factory; private volatile Object instance = UNINITIALIZED; @Override public T get() { // double-check idiom from EJ2: Item 71 Object result = instance; if (result == UNINITIALIZED) { synchronized (this) { result = instance; if (result == UNINITIALIZED) { instance = result = factory.get(); } } } return (T) result; } //… } -
ScopedProvider public final class ScopedProvider<T> implements Provider<T> { private static
final Object UNINITIALIZED = new Object(); private final Factory<T> factory; private volatile Object instance = UNINITIALIZED; @Override public T get() { // double-check idiom from EJ2: Item 71 Object result = instance; if (result == UNINITIALIZED) { synchronized (this) { result = instance; if (result == UNINITIALIZED) { instance = result = factory.get(); } } } return (T) result; } //… } -
@ApplicationScope @UserScope @ActivityScope @Lorem @Ipsum @Dolor = It’s just a
name. Not a difference at all. -
@UserScope @Subcomponent(modules = UserModule.class) public interface UserComponent { }
Scope usage @Module public class UserModule { @Provides @UserScope RepositoriesManager provideRepositoriesManager(User user, GithubApiService apiService) { return new RepositoriesManager(user, apiService); } } -
@Singleton @Component( modules = { AppModule.class, GithubApiModule.class } ) public
interface AppComponent { } Singleton (Application scope) AppComponent - AppModule - GithubApiModule Creating scopes with subcomponents -
@Singleton @Component( modules = { AppModule.class, GithubApiModule.class } ) public
interface AppComponent { UserComponent plus(UserModule userModule); } UserScope UserComponent - UserModule Singleton (Application scope) AppComponent - AppModule - GithubApiModule @UserScope @Subcomponent( modules = { UserModule.class } ) public interface UserComponent { } -
@UserScope @Subcomponent( modules = { UserModule.class } ) public interface
UserComponent { SplashActivityComponent plus( SplashActivityModule module ); } ActivityScope ActivityComponent - ...ActivityModules UserScope UserComponent - UserModule Singleton (Application scope) AppComponent - AppModule - GithubApiModule … @ActivityScope @Subcomponent( modules = SplashActivityModule.class ) public interface SplashActivityComponent { } -
Creating scopes with components dependencies UserScope UserComponent - UserModule Singleton
(Application scope) AppComponent - AppModule - GithubApiModule -
Creating scopes with components dependencies @Singleton @Component( modules = {
AppModule.class, GithubApiModule.class } ) public interface AppComponent { Validator Validator(); UserManager getUserManager(); AnalyticsManager getAnalyticsManager(); GithubApiService getGithubApiService(); } @UserScope @Component( modules = { UserModule.class }, dependencies = AppComponent.class ) public interface UserComponent { } -
Creating scopes with components dependencies @Singleton @Component( modules = {
AppModule.class, GithubApiModule.class } ) public interface AppComponent { Validator Validator(); UserManager getUserManager(); AnalyticsManager getAnalyticsManager(); GithubApiService getGithubApiService(); } @UserScope @Component( modules = { UserModule.class }, dependencies = AppComponent.class ) public interface UserComponent { } All dependencies used in dependent components have to be exposed -
Unit tests • Dagger 2 doesn't make unit testing easier
• …but DI itself makes project ready for testing • (Still) no official way from Dagger 2 team • Dagger 2 + Robolectric 3 -
Swapping modules public class TestGithubClientApplication extends GithubClientApplication { AppModule
appModule; @Override public AppModule getAppModule() { if (appModule == null) { return super.getAppModule(); } return appModule; } public void setAppModule(AppModule appModule) { this.appModule = appModule; initAppComponent(); } } Dagger 2 + Robolectric 3 public class GithubClientApplication extends Application { private AppComponent appComponent; @Override public void onCreate() { super.onCreate(); initAppComponent(); } protected void initAppComponent() { appComponent = DaggerAppComponent.builder() .appModule(getAppModule()) .build(); } @VisibleForTesting public AppModule getAppModule() { return new AppModule(this); } } App Tests -
Swapping modules @Module public class AppModule { private Application application;
public AppModule(Application application) { this.application = application; } @Provides @Singleton public Application provideApplication() { return application; } @Provides @Singleton AnalyticsManager provideAnalyticsManager() { return new AnalyticsManager(application); } } Dagger 2 + Robolectric 3 public class MockAppModule extends AppModule { @Mock AnalyticsManager analyticsManagerMock; public MockAppModule(Application application) { super(application); MockitoAnnotations.initMocks(this); } @Override AnalyticsManager provideAnalyticsManager() { return analyticsManagerMock; } } App Tests -
Swapping modules @RunWith(RobolectricGradleTestRunner.class) @Config( sdk = 18, constants = BuildConfig.class,
application = TestGithubClientApplication.class ) public class SplashActivityTests { @Before public void setup() { TestGithubClientApplication app = (TestGithubClientApplication) RuntimeEnvironment.application; MockAppModule mockAppModule = new MockAppModule(app); app.setAppModule(mockAppModule); } @Test public void testName() throws Exception { SplashActivity activity = Robolectric.setupActivity(SplashActivity.class); verify(activity.analyticsManager).logScreenView(anyString()); } } Dagger 2 + Robolectric 3 -
Injecting without Dagger public class TestGithubClientApplication extends GithubClientApplication {
private AppComponent appComponent; private SplashActivityComponent splashActivityComponent; @Override public AppComponent getAppComponent() { if (appComponent == null) { appComponent = mock(AppComponent.class); when(appComponent.plus(any(SplashActivityModule.class))) .thenReturn(splashActivityComponent); } return appComponent; } public void setSplashActivityComponent(SplashActivityComponent component) { this.splashActivityComponent = component; } } -
@RunWith(RobolectricGradleTestRunner.class) @Config( sdk = 18, constants = BuildConfig.class, application =
TestGithubClientApplication.class ) public class SplashActivityTests { @Mock SplashActivityComponent splashActivityComponentMock; @Mock AnalyticsManager analyticsManagerMock; @Before public void setup() { MockitoAnnotations.initMocks(this); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) { SplashActivity activity = (SplashActivity) invocation.getArguments()[0]; activity.analyticsManager = analyticsManagerMock; return null; } }).when(splashActivityComponentMock).inject(any(SplashActivity.class)); TestGithubClientApplication app = (TestGithubClientApplication) RuntimeEnvironment.application; app.setSplashActivityComponent(splashActivityComponentMock); } @Test public void testName() throws Exception { SplashActivity activity = Robolectric.setupActivity(SplashActivity.class); verify(activity.analyticsManager).logScreenView(anyString()); } } -
@RunWith(RobolectricGradleTestRunner.class) @Config( sdk = 18, constants = BuildConfig.class, application =
TestGithubClientApplication.class ) public class SplashActivityTests { @Mock SplashActivityComponent splashActivityComponentMock; @Mock AnalyticsManager analyticsManagerMock; @Before public void setup() { MockitoAnnotations.initMocks(this); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) { SplashActivity activity = (SplashActivity) invocation.getArguments()[0]; activity.analyticsManager = analyticsManagerMock; return null; } }).when(splashActivityComponentMock).inject(any(SplashActivity.class)); TestGithubClientApplication app = (TestGithubClientApplication) RuntimeEnvironment.application; app.setSplashActivityComponent(splashActivityComponentMock); } @Test public void testName() throws Exception { SplashActivity activity = Robolectric.setupActivity(SplashActivity.class); verify(activity.analyticsManager).logScreenView(anyString()); } } -
Instrumentation tests mocking public class ApplicationMock extends GithubClientApplication {
private AppComponent appComponent; private GithubApiModule githubApiModuleMock; public void setGithubApiModuleMock(GithubApiModule githubApiModuleMock) { this.githubApiModuleMock = githubApiModuleMock; setupMockAppComponent(); } public void setupMockAppComponent() { appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .githubApiModule(githubApiModuleMock) .build(); } @Override public AppComponent getAppComponent() { return appComponent == null ? super.getAppComponent() : appComponent; } } -
Instrumentation tests mocking @RunWith(AndroidJUnit4.class) public class SplashActivityUITests { @Mock
UserManager userManagerMock; @Rule public ActivityTestRule<SplashActivity> activityRule = new ActivityTestRule<>( SplashActivity.class, true, false ); @Before public void setUp() { MockitoAnnotations.initMocks(this); GithubApiModuleMock githubApiModuleMock = new GithubApiModuleMock(userManagerMock); ApplicationMock app = (ApplicationMock) InstrumentationRegistry.getTargetContext().getApplicationContext(); app.setGithubApiModuleMock(githubApiModuleMock); activityRule.launchActivity(new Intent()); } @Test public void checkLoadingError() { Mockito.when(userManagerMock.getUser(anyString())) .thenReturn(Observable.<User>error(new RuntimeException("test"))); onView(withId(R.id.etUsername)).perform(typeText("frogermcs")); onView(withId(R.id.btnShowRepositories)).perform(click()); onView(withId(R.id.etUsername)).check(matches(hasErrorText("Validation error"))); } } -
Instrumentation tests mocking @RunWith(AndroidJUnit4.class) public class SplashActivityUITests { @Mock
UserManager userManagerMock; @Rule public ActivityTestRule<SplashActivity> activityRule = new ActivityTestRule<>( SplashActivity.class, true, false ); @Before public void setUp() { MockitoAnnotations.initMocks(this); GithubApiModuleMock githubApiModuleMock = new GithubApiModuleMock(userManagerMock); ApplicationMock app = (ApplicationMock) InstrumentationRegistry.getTargetContext().getApplicationContext(); app.setGithubApiModuleMock(githubApiModuleMock); activityRule.launchActivity(new Intent()); } @Test public void checkLoadingError() { Mockito.when(userManagerMock.getUser(anyString())) .thenReturn(Observable.<User>error(new RuntimeException("test"))); onView(withId(R.id.etUsername)).perform(typeText("frogermcs")); onView(withId(R.id.btnShowRepositories)).perform(click()); onView(withId(R.id.etUsername)).check(matches(hasErrorText("Validation error"))); } } -
http://blog.codinghorror.com/learn-to-read-the-source-luke/ „If you can't understand the platform below you, how
can you understand your own software?”