Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dependency Injection with Dagger 2

Dependency Injection with Dagger 2

Now, even Daggier.

Ilya Tsymbal

January 10, 2017
Tweet

Other Decks in Programming

Transcript

  1. Dependency Injection with Dagger 2 now, even Daggier. ILYA TSYMBAL

    Orange Penguin, Inc. @ilyatsymbal +ilyatsymbal
  2. Goals • Understand DI pattern and benefits • Understand Dagger

    2 benefits • Be able to apply Dagger 2 to existing code 3
  3. Dependency Injection • Software design pattern • SOLID (single responsibility,

    open-closed, Liskov substitution, interface segregation and dependency inversion) • Separate dependency resolution from logic • Code reuse (DRY) • Evolve logic separately from dependencies • Test business logic separately
  4. Before DI 62 public User fetchUserSync(String username) throws IOException {

    63 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); 64 loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 65 // create OkHttp client 66 OkHttpClient client = new OkHttpClient.Builder() 67 .addInterceptor(loggingInterceptor) 68 .build(); 69 70 // create Moshi converter 71 Moshi moshi = new Moshi.Builder() 72 .add(ApiMoshiAdapterFactory.create()) 73 .build(); 74 MoshiConverterFactory moshiConverterFactory = 75 MoshiConverterFactory.create(moshi); 76 // create gitHubClient 77 GitHubClient gitHubClient = new Retrofit.Builder() 78 .client(client) 79 .addCallAdapterFactory(RxJavaCallAdapterFactory 80 .createWithScheduler(Schedulers.io())) 81 .addConverterFactory(moshiConverterFactory) 82 .baseUrl("https://api.github.com") 83 .build() 84 .create(GitHubClient.class); 85 86 Call<ApiUser> call = gitHubClient.callUser(username); 87 ApiUser apiUser = call.execute().body(); 88 return User.fromApiUser(apiUser).build(); 89 }
  5. After DI 62 public User fetchUserSync(String username) throws IOException {

    63 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); 64 loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 65 // create OkHttp client 66 OkHttpClient client = new OkHttpClient.Builder() 67 .addInterceptor(loggingInterceptor) 68 .build(); 69 70 // create Moshi converter 71 Moshi moshi = new Moshi.Builder() 72 .add(ApiMoshiAdapterFactory.create()) 73 .build(); 74 MoshiConverterFactory moshiConverterFactory = 75 MoshiConverterFactory.create(moshi); 76 // create gitHubClient 77 GitHubClient gitHubClient = new Retrofit.Builder() 78 .client(client) 79 .addCallAdapterFactory(RxJavaCallAdapterFactory 80 .createWithScheduler(Schedulers.io())) 81 .addConverterFactory(moshiConverterFactory) 82 .baseUrl("https://api.github.com") 83 .build() 84 .create(GitHubClient.class); 85 86 return fetchUserSyncInternal(username, gitHubClient); 87 } 88 89 public User fetchUserSyncInternal(String username, GitHubClient gitHubClient) 90 throws IOException { 91 Call<ApiUser> call = gitHubClient.callUser(username); 92 ApiUser apiUser = call.execute().body(); 93 return User.fromApiUser(apiUser).build(); 94 }
  6. Extract dependency resolution public User fetchUserSync(String username) throws IOException {


    GitHubClient gitHubClient = RepositoryUtil.getGitHubClient();
 
 return fetchUserSyncInternal(username, gitHubClient);
 }
 
 public User fetchUserSyncInternal(String username,
 GitHubClient gitHubClient)
 throws IOException {
 ApiUser apiUser = gitHubClient
 .callUser(username)
 .execute()
 .body();
 return User.fromApiUser(apiUser).build();
 }
  7. Profit! @RunWith(MockitoJUnitRunner.class)
 public class UserRepositoryTest1 {
 
 UserRepository userRepository;
 @Mock

    GitHubClient mockGitHubClient;
 
 @Before
 public void setUp() {
 userRepository = new UserRepository();
 } }
  8. Profit! @Test
 public void fetchUserSyncInternal_ShouldReturnUser() throws Exception {
 // arrange


    ApiUser stubbedUserResponse = TestFixtures.createApiUser();
 User expectedUserObject = TestFixtures.createExpectedUser();
 Mockito.when(mockGitHubClient.callUser("itsymbal").execute().body())
 .thenReturn(stubbedUserResponse);
 // act
 User user = userRepository.fetchUserSyncInternal("itsymbal", mockGitHubClient);
 
 // assert
 Mockito.verify(mockGitHubClient.callUser("itsymbal"));
 Assert.assertEquals(expectedUserObject, user);
 }

  9. Dagger 2 • Dependency Injection library, created by Google •

    Inspired by Dagger 1 library, created by Square • Uses annotation processing with code generation • No runtime reflection at all • Compile time safety • Generates readable, debuggable code
  10. Central elements • @Module - provides dependencies. • @Component -

    combines one or more modules and injects your code
  11. OkHttp client Retrofit client Repository Module Repository Component Example D2

    setup for Repository layer User Repository Dagger elements Your code Dependencies
  12. Module @Module
 public class RepositoryModule {
 
 @Provides
 OkHttpClient provideOkHttpClient()

    { // … set up OkHttpClient return client;
 }
 
 @Provides 
 public GitHubClient provideGitHubClient(OkHttpClient okHttpClient) { // … set up REST API client return gitHubClient;
 }
 }
  13. Your code 
 public class UserRepository {
 
 @Inject GitHubClient

    gitHubClient;
 
 public UserRepository() { RepositoryComponent component; // create or obtain component, somehow… component.inject(this);
 } public User fetchUserSyncInternal(String username)
 throws IOException {
 ApiUser apiUser = gitHubClient
 .callUser(username)
 .execute()
 .body();
 return User.fromApiUser(apiUser).build();
 }
 }
  14. Creating components // your class that has dependencies public UserRepository()

    // constructor { RepositoryComponent component; // … create or obtain component, somehow… component.inject(this);
 } RepositoryComponent repositoryComponent =
 DaggerRepositoryComponent
 .builder()
 .repositoryModule(new RepositoryModule())
 .build();
  15. OkHttp client Retrofit client Repository Module Repository Component More accurate

    User Repository Dagger elements Your code Dependencies DaggerRepository Component
  16. Where to put Component - Static Injector pattern public class

    Injector {
 
 private static RepositoryComponent repositoryComponent;
 
 public static RepositoryComponent getRepositoryComponent() {
 // repository component is a singleton. Once created it is never recreated
 if (repositoryComponent == null) {
 repositoryComponent =
 DaggerRepositoryComponent
 .builder()
 .repositoryModule(new RepositoryModule())
 .build();
 }
 return repositoryComponent;
 }
 }
  17. OkHttp client Retrofit client Repository Module Repository Component User Repository

    Dagger elements Your code Dependencies DaggerRepository Component Injector
  18. Your code - Injection public class UserRepository {
 
 @Inject

    GitHubClient gitHubClient;
 
 public UserRepository() {
 Injector.getRepositoryComponent().inject(this);
 } public User fetchUserSyncInternal(String username)
 throws IOException {
 ApiUser apiUser = gitHubClient
 .callUser(username)
 .execute()
 .body();
 return User.fromApiUser(apiUser).build();
 }
 }
  19. Dependency 1 Class 1 Class 2 Dependency 2 Dependency N

    Class N Module 1 Module 2 Dependency 3 Dependency 4 Dependency 5 Dependency 6 Component 1 Component 2 Module 3 Class 3 Class 4
  20. Dependency 1 Activity 1 Dependency 2 Dependency N Dependency 3

    Dependency 4 Dependency 5 Dependency 6 Fragment 1A Fragment 1B Presenter 1 Use Case 1 Use Case 2 Repository 1 Service 1 Class 25 Activity 2 Module 1 Component 1
  21. Dependency 1 Activity 1 Dependency 2 Dependency N Dependency 3

    Dependency 4 Dependency 5 Dependency 6 Fragment 1A Fragment 1B Presenter 1 Use Case 1 Use Case 2 Repository 1 Service 1 Activity 2 Component 1 Component 2 Component 3 Module 1 Module 2 Module 3 Component N Module N Class N
  22. Activity 1 Fragment 1A Fragment 1B Presenter 1 Use Case

    1 Use Case 2 Repository 1 Repository 2 Repository N View component Presenter component Use Case Component View Module Presenter Module Repository Component Presenter 1 Presenter 2 Use Case 1 Use Case 2 Use Case Module Repository 1 Repository 2 Repository Module Rest Service 1 Rest Service 2 Endpoint URL 1 Presenter N Use Case N
  23. Post-Login Component App Component User module App Module • Reuse

    of common dependencies • Allow subcomponents to have a lifecycle different from parent User
  24. Defining subcomponent - version 2 @Component(dependencies = ApplicationComponent.class, modules =

    {CatalogModule.class})
 public interface CatalogComponent {
 void inject(CatalogActivity activity);
 }
  25. Creating subcomponent // Injector.java public static ActivityComponent getActivityComponent() {
 return

    Injector.getApplicationComponent().plus(new ActivityModule());
 }
  26. Your code - Injection public class UserRepository {
 
 @Inject

    GitHubClient gitHubClient;
 
 public UserRepository() {
 Injector.getRepositoryComponent().inject(this);
 } public Call<ApiUser> callUser(String username)
 throws IOException {
 Call<ApiUser> apiUser = gitHubClient.callUser(username);
 return apiUser;
 } }
  27. Test code @RunWith(MockitoJUnitRunner.class)
 public class UserRepositoryTest2 {
 UserRepository userRepository;
 @Mock

    GitHubClient mockGitHubClient;
 @Before
 public void setUp() throws Exception {
 // get Injector to use mockGitHubClient, somehow...
 userRepository = new UserRepository();
 }
 @Test
 public void callUser_ShouldReturnCall()throws Exception{ // act
 userRepository.callUser("itsymbal");
 // assert
 Mockito.verify(mockGitHubClient).callUser("itsymbal");
 } }
  28. Update Injector.java public class Injector {
 
 private static RepositoryComponent

    repositoryComponent;
 
 public static RepositoryComponent getRepositoryComponent() {
 // repository component is a singleton. Once created it is never recreated
 if (repositoryComponent == null) {
 repositoryComponent =
 DaggerRepositoryComponent
 .builder()
 .repositoryModule(new RepositoryModule())
 .build();
 }
 return repositoryComponent;
 }
 }
  29. Modify Injector code // Injector.java // setters allow setting a

    mock component from test code
 public static void setRepositoryComponent(RepositoryComponent repositoryComponent) {
 Injector.repositoryComponent = repositoryComponent;
 }
  30. Repository DI configuration User Repository Repository Component Repository Module GitHubClient

    Test Repository Component Test Repository Module mock GitHubClient
  31. Production and test Components @Singleton
 @Component(modules = {RepositoryModule.class})
 public interface

    RepositoryComponent {
 void inject(UserRepository userRepository);
 } @Singleton
 @Component(modules = {TestRepositoryModule2.class})
 public interface TestRepositoryComponent2 extends RepositoryComponent {
 }
  32. Production module @Module
 public class RepositoryModule {
 @Provides
 @Singleton
 OkHttpClient

    provideOkHttpClient() {
 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
 loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
 OkHttpClient client = new OkHttpClient.Builder()
 .addInterceptor(loggingInterceptor)
 .build();
 return client;
 }
 
 @Provides
 @Singleton
 public GitHubClient provideGitHubClient(OkHttpClient okHttpClient) {
 
 Moshi moshi = new Moshi.Builder()
 .add(ApiMoshiAdapterFactory.create())
 .build();
 
 return new Retrofit.Builder()
 .client(okHttpClient)
 .addCallAdapterFactory(RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io()))
 .addConverterFactory(MoshiConverterFactory.create(moshi))
 .baseUrl(getGitHubUrl())
 .build()
 .create(GitHubClient.class);
 }
  33. Test module @Module
 public final class TestRepositoryModule2 {
 
 GitHubClient

    gitHubClient;
 
 public void setGitHubClient(GitHubClient gitHubClient) {
 this.gitHubClient = gitHubClient;
 }
 
 @Provides
 @Singleton
 public GitHubClient provideGitHubClient() {
 return gitHubClient;
 }
 }
  34. Set up mock dependencies @RunWith(MockitoJUnitRunner.class)
 public class UserRepositoryTest2 {
 UserRepository

    userRepository;
 
 @Mock GitHubClient mockGitHubClient;
 @Before
 public void setUp() throws Exception {
 // get Injector to use mockGitHubClient, somehow...
 TestRepositoryModule2 testRepositoryModule = new TestRepositoryModule2();
 testRepositoryModule.setGitHubClient(mockGitHubClient);
 
 TestRepositoryComponent2 testRepositoryComponent =
 DaggerTestRepositoryComponent2
 .builder()
 .testRepositoryModule2(testRepositoryModule)
 .build();
 
 Injector.setRepositoryComponent(testRepositoryComponent);
 
 userRepository = new UserRepository();
 }
  35. Summary of this approach • Technically correct - will work.

    Very flexible (new Test Module in every test, can configure as needed) • Very verbose. Dozens of lines of code - in EACH test class. Gets worse - each test class has to create a Module that provides all dependencies that ANY Repository uses (superset of all dependencies)
  36. Refactoring - ComponentUtil.java public static TestRepositoryModule2 setUpTestRepositoryModule2() {
 TestRepositoryModule2 testRepositoryModule

    = new TestRepositoryModule2();
 
 TestRepositoryComponent2 testRepositoryComponent =
 DaggerTestRepositoryComponent2
 .builder()
 .testRepositoryModule2(testRepositoryModule)
 .build();
 
 Injector.setRepositoryComponent(testRepositoryComponent);
 return testRepositoryModule;
 }
  37. Refactoring - updated Test module @Module
 public final class TestRepositoryModule2

    {
 
 GitHubClient gitHubClient = Mockito.mock(GitHubClient.class);
 @Provides
 @Singleton
 public GitHubClient provideGitHubClient() {
 return gitHubClient;
 }
 }
  38. Refactoring - updated Test Class @Before
 public void setUp() throws

    Exception {
 // TestRepositoryModule2 testRepositoryModule = new TestRepositoryModule2();
 // testRepositoryModule.setGitHubClient(mockGitHubClient);
 //
 // TestRepositoryComponent2 testRepositoryComponent =
 // DaggerTestRepositoryComponent2
 // .builder()
 // .testRepositoryModule2(testRepositoryModule)
 // .build();
 //
 // Injector.setRepositoryComponent(testRepositoryComponent);
 TestRepositoryModule2 testRepositoryModule2 = ComponentUtil.setUpTestRepositoryModule2();
 mockGitHubClient = testRepositoryModule2.gitHubClient;
 
 // configure mock object to return stub responses as needed
 // Mockito.when(mockGitHubClient.callUser("itsymbal")).thenReturn(....);
 
 userRepository = new UserRepository();
 }
  39. Scope annotations @Module
 public class RepositoryModule {
 
 @Provides
 @Singleton


    OkHttpClient provideOkHttpClient() {} } @Singleton
 @Component(modules = {RepositoryModule.class})
 public interface RepositoryComponent {
 void inject(UserRepository userRepository);
 }
  40. Custom annotations @Module
 public class RepositoryModule {
 @Provides
 @PerActivity
 Navigator

    provideNavigator() { } } @PerApplication
 @Component(modules={AppModule.class})
 public interface AppComponent {} // PerActivity.java @Scope
 @Retention(RetentionPolicy.RUNTIME)
 public @interface PerActivity {}

  41. MockWebServer / Plus public class UserRepositoryTest {
 
 @Rule public

    MockWebServerPlus server = new MockWebServerPlus();
 UserRepositoryDi userRepository;
 
 @Before
 public void setUp() {
 String serverBaseUrl = server.url("/");
 TestRepositoryModule testRepositoryModule = ComponentUtil.setUpTestRepositoryModule();
 testRepositoryModule.setGitHubUrl(serverBaseUrl);
 userRepository = new UserRepositoryDi();
 }
 
 @Test
 public void testFetchUserSync() throws Exception {
 server.enqueue("github_user");
 User expectedUserObject = TestFixtures.createExpectedUser();
 User user = userRepository.fetchUserSync("itsymbal");
 Assert.assertEquals(expectedUserObject, user); }
 public User fetchUserSync(String username) throws IOException {
 Call<ApiUser> call = gitHubClient.callUser(username);
 ApiUser apiUser = call.execute().body();
 User user = User.fromApiUser(apiUser).build();
 return user;
 }
  42. github_user.yaml statusCode : 200
 delay: 0
 headers:
 - 'Auth:auth'
 -

    'key:value'
 body: 'github/user.json' user.json {
 "login": "itsymbal",
 "id": 1466908,
 "avatar_url": "https://avatars.githubusercontent.com/u/1466908?v=3",
 "gravatar_id": "",
 "url": "https://api.github.com/users/itsymbal",
 "html_url": "https://github.com/itsymbal",
 "followers_url": "https://api.github.com/users/itsymbal/followers",
 "following_url": "https://api.github.com/users/itsymbal/following{/other_user}",
  43. References This presentation https://speakerdeck.com/itsymbal/dependency-injection-with-dagger-2 This presentation https://goo.gl/tP71WP Dagger2 https://github.com/google/dagger Boilerplate

    project https://github.com/itsymbal/op-boilerplate MockWebServer https://github.com/square/okhttp/tree/master/mockwebserver MockWebServerPlus https://github.com/orhanobut/mockwebserverplus