An Introduction to Dagger 2 (Android DI) — Part 3

In the previous two articles, we went through an introduction to Dagger 2 with simple examples and learned how to use Android Build variants with Dagger 2 to have different implementations that are switched automatically when the app is in the debug mode or release mode.

This article discusses using Dagger 2 with an MVP (Model View Presenter) pattern, and unit testing our simple Dagger 2 app using Espresso.

MVP is a derivation of the MVC (Model View Controller) design pattern, and its main purpose is to improve the separation of concerns in the presentation layer. In MVP, Model represents “Data”, Presenter talks with the model to get data and formats the data to be displayed by the view, and View displays the formatted data and delegate event handling to Presenter.

We will modify the app from the first two articles to allow the end user to enter a city name and their name to display a greeting message that contains the current weather information of the entered city.

App Structure

The following interaction diagram shows the main app flow in an abstract way.

As shown in the diagram, in order to get weather information for a user, MainActivity talks with MainPresenter. Then MainPresenter talks with MainInteractor. Finally, MainInteractor abstracts the calls to the app services (HelloService and WeatherService) and it returns the result to the MainPresenter that displays data on the MainActivity (which represents the View).

The next diagram shows the app structure, which is a self-explanatory.

Dagger 2 for Dependency Injection

Now, let’s see some of the important details of the Dagger 2 components and subcomponents in our App. We mainly have a single Component (AppComponent), which is responsible for theAppModule and ServiceModule modules. The AppComponent class code is shown below:

@Singleton @Component(modules = {AppModule.class, ServiceModule.class}) public interface AppComponent { MainActivityComponent plus(MainActivityModule module); Application application(); }

The Dagger subcomponent is defined using @Subcomponent annotation on an interface. The main difference between a Dagger component and subcomponent is that a subcomponent cannot be standalone, has to be defined as a method on an interface marked as @Component, and that method should return the interface marked as subcomponent. This is why the MainActivityComponent plus(MainActivityModule module) method is defined in AppComponent since a subcomponent will be defined for every activity in our app.

The next code snippet shows the AppModule class code, which provides instances of Application and Resources classes.

@Module public class AppModule { DaggerApplication app; public AppModule(DaggerApplication application) { app = application; } @Provides @Singleton protected Application provideApplication() { return app; } @Provides @Singleton protected Resources provideResources() { return app.getResources(); } }

The next code snippet shows the ServiceModule class code, which provides instances of HelloService and WeatherService.

@Module public class ServiceModule { @Provides @Singleton HelloService provideHelloService() { return new HelloServiceDebugManager(); } @Provides @Singleton WeatherService provideWeatherService() { return new WeatherServiceManager(); } }

@ActivityScope custom scope is defined for every activity subcomponent. The following code snippet shows MainActivityComponent code:

@ActivityScope @Subcomponent( modules = {MainActivityModule.class} ) public interface MainActivityComponent { void inject(MainActivity activity); }

MainActivityModule will be responsible for providing instances of the MainActivity (which implements the MainView interface), and also instances of MainInteractor interface.

@Module public class MainActivityModule { public final MainView view; public MainActivityModule(MainView view) { this.view = view; } @Provides @ActivityScope MainView provideMainView() { return this.view; } @Provides @ActivityScope MainInteractor provideMainInteractor(MainInteractorImpl interactor) { return interactor; } @Provides @ActivityScope MainPresenter provideMainPresenter(MainPresenterImpl presenter) { return presenter; } }

Finally, the next code snippet shows the custom Application class code, which is used for initializing the AppComponent graph.

public class DaggerApplication extends Application { private static AppComponent appComponent; private static DaggerApplication instance; @Override public void onCreate() { super.onCreate(); instance = this; initAppComponents(); } public static DaggerApplication get(Context context) { return (DaggerApplication) context.getApplicationContext(); } public AppComponent getAppComponent() { return appComponent; } public void initAppComponents() { appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); } /** * Visible only for testing purposes. */ public void setTestComponent(AppComponent testingComponent) { appComponent = testingComponent; } }

Note that setTestComponent(AppComponent testingComponent) method will be used for testing purposes only to provide a mock implementation of the AppComponent class to test the app flow.

App Details

Now, let’s see some of the app details. The next code snippet shows MainView interface that defines the view methods of the MainActivity (MainView is implemented by MainActivity and extends OnInfoCompletedListener interface).

public interface MainView extends OnInfoCompletedListener { public String getUserNameText(); public String getCityText(); public void showUserNameError(int messageId); public void showCityNameError(int messageId); public void showBusyIndicator(); public void hideBusyIndicator(); public void showResult(String result); }

OnInfoCompletedListener interface defines the information retrieval operation’s callback methods since this operation is asynchronous.

public interface OnInfoCompletedListener { public void onUserNameValidationError(int messageID); public void onCityValidationError(int messageID); public void onSuccess(String data); public void onFailure(String errorMessage); }

The following code snippet shows the MainActivity class. Note that ButterKnife provides @InjectView annotation. ButterKnife is a lightweight library that can be used for injecting User interface elements instead of doing this every time manually using findViewById() method of View class.

public class MainActivity extends AppCompatActivity implements MainView, View.OnClickListener { @Inject MainPresenter presenter; @InjectView(R.id.userNameText) EditText userNameText; @InjectView(R.id.cityText) EditText cityText; @InjectView(R.id.btnShowInfo) Button showInfoButton; @InjectView(R.id.resultView) TextView resultView; @InjectView(R.id.progress) ProgressBar progressBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); DaggerApplication.get(this) .getAppComponent() .plus(new MainActivityModule(this)) .inject(this); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); showInfoButton.setOnClickListener(this); } // … @Override public void onClick(View v) { if (v.getId() == R.id.btnShowInfo) { presenter.requestInformation(); } } @Override public String getUserNameText() { return userNameText.getText().toString(); } @Override public String getCityText() { return cityText.getText().toString(); } @Override public void showUserNameError(int messageId) { userNameText.setError(getString(messageId)); } @Override public void showCityNameError(int messageId) { cityText.setError(getString(messageId)); } @Override public void showBusyIndicator() { progressBar.setVisibility(View.VISIBLE); } @Override public void hideBusyIndicator() { progressBar.setVisibility(View.GONE); } @Override public void showResult(final String result) { resultView.setText(result); } @Override public void onUserNameValidationError(final int messageID) { runOnUiThread(new Runnable() { @Override public void run() { hideBusyIndicator(); showUserNameError(messageID); } }); } @Override public void onCityValidationError(final int messageID) { runOnUiThread(new Runnable() { @Override public void run() { hideBusyIndicator(); showCityNameError(messageID); } }); } @Override public void onSuccess(final String data) { runOnUiThread(new Runnable() { @Override public void run() { hideBusyIndicator(); showResult(data); } }); } @Override public void onFailure(final String errorMessage) { runOnUiThread(new Runnable() { @Override public void run() { hideBusyIndicator(); showResult(errorMessage); } }); } }

One important thing to note here is that in the activity’s onCreate() method, MainActivityComponent is initiated using DaggerApplication.get(this).getAppComponent().plus(new MainActivityModule(this)).inject(this). When the show information button is clicked, requestInformation() of MainPresenter is called.

The next code snippet shows MainPresenter interface which has only one method.

public interface MainPresenter { public void requestInformation(); }

MainPresenterImpl class implements MainPresenter as follows.

public class MainPresenterImpl implements MainPresenter { private MainView mainView; private MainInteractor mainInteractor; @Inject public MainPresenterImpl(MainView mainView, MainInteractor mainInteractor) { this.mainView = mainView; this.mainInteractor = mainInteractor; } @Override public void requestInformation() { mainView.showBusyIndicator(); mainInteractor.getInformation(mainView.getUserNameText(), mainView.getCityText(), mainView); } }

As shown in the MainPresenterImpl‘s requestInformation() method, it calls mainInteractor‘s getInformation() method passing user name, city name, and the class instance (mainView) which implements OnInfoCompletedListener interface.

The next code snippet shows MainInteractor interface:

public interface MainInteractor { public void getInformation(String userName, String cityName, final OnInfoCompletedListener listener); }

The next code snippet shows MainInteractorImpl class, which interacts with HelloService and WeatherService interfaces:

public class MainInteractorImpl implements MainInteractor { private static final String TAG = MainInteractorImpl.class.getName(); @Inject HelloService helloService; @Inject WeatherService weatherService; @Inject public MainInteractorImpl() { } @Override public void getInformation(final String userName, final String cityName, final OnInfoCompletedListener listener) { final String greeting = helloService.greet(userName) + "\n"; if (TextUtils.isEmpty(userName)) { listener.onUserNameValidationError(R.string.username_invalid_message); return; } if (TextUtils.isEmpty(cityName)) { listener.onUserNameValidationError(R.string.city_invalid_message); return; } Thread thread = new Thread(new Runnable() { @Override public void run() { try { int temperature = weatherService.getWeatherInfo(cityName); String temp = "Current weather in " + cityName + " is " + temperature + "°F"; listener.onSuccess(greeting + temp); } catch (InvalidCityException ex) { listener.onFailure(ex.getMessage()); Log.e(TAG, ex.getMessage(), ex); } catch (Exception ex) { listener.onFailure("Unable to get weather information"); Log.e(TAG, ex.getMessage(), ex); } } }); thread.start(); } }

When information is retrieved, getInformation() method calls the onSuccess() method of the OnInfoCompletedListener interface if the operation succeeds. If getInformation() method fails, it calls onFailure() method of the OnInfoCompletedListener interface.

Now, let’s look into the services part of the app. The next code snippet shows HelloService interface:

public interface HelloService { public String greet(String userName); }

HelloServiceDebugImpl implements HelloService (Note that this implementation is active only in app debug mode. For the app release mode, HelloServiceReleaseManager will be the implementation of HelloService interface):

public class HelloServiceDebugManager implements HelloService { @Override public String greet(String userName) { return "[Debug] Hello " + userName + "!"; } }

The next code snippet shows WeatherService interface:

public interface WeatherService { public int getWeatherInfo(String city) throws InvalidCityException; }

WeatherServiceImpl class implements WeatherService interface as follows:

public class WeatherServiceManager implements WeatherService { private static final String TAG = WeatherServiceManager.class.getName(); @Override public int getWeatherInfo(String city) throws InvalidCityException { int temperature = 0; if (city == null) { throw new RuntimeException(ErrorMessages.CITY_REQUIRED); } try { city = URLEncoder.encode(city, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(ErrorMessages.INVALID_CITY_PROVIDED); } try { URL url = new URL("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22" + city + "%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setInstanceFollowRedirects(true); httpURLConnection.setRequestMethod("GET"); httpURLConnection.setRequestProperty("User-Agent", "Mozilla/5.0"); // Receive response ... int responseCode = httpURLConnection.getResponseCode(); InputStream is = httpURLConnection.getInputStream(); InputStreamReader reader = new InputStreamReader(is); BufferedReader bufferedReader = new BufferedReader(reader); String line = ""; StringBuffer sb = new StringBuffer(); while ((line = bufferedReader.readLine()) != null) { sb.append(line); } bufferedReader.close(); String result = sb.toString(); int startIndex = result.indexOf("\"temp\":"); if (startIndex == -1) { throw new InvalidCityException(ErrorMessages.INVALID_CITY_PROVIDED); } int endIndex = result.indexOf(",", startIndex); temperature = Integer.parseInt(result.substring(startIndex + 8, endIndex - 1)); } catch (InvalidCityException ex) { throw ex; } catch (Exception ex) { Log.e(TAG, ex.getMessage(), ex); } return temperature; } }

WeatherServiceImpl gets the weather information using the Yahoo weather API.

Unit Testing App Using Espresso

Now, let’s see how to unit test our application using Espresso. The next code snippet shows how we create our own test custom Dagger component and use it instead of the original app component to test the app flow.

@RunWith(AndroidJUnit4.class) public class MainActivityTest { private static String GREET_PREFIX = "[Test] Hello "; private static int MOCK_TEMPERATURE = 65; private static String MOCK_NAME = "Hazem"; private static String MOCK_PLACE = "Cairo, Egypt"; private static String MOCK_GREETING_MSG = GREET_PREFIX + MOCK_NAME; private static String MOCK_WEATHER_MSG = "\nCurrent weather in " + MOCK_PLACE + " is " + MOCK_TEMPERATURE + "°F"; private static String MOCK_RESPONSE_MESSAGE = MOCK_GREETING_MSG + MOCK_WEATHER_MSG; private static String TAG = MainActivityTest.class.getName(); @Rule public ActivityTestRule<MainActivity> mActivityRule = new DaggerActivityTestRule<>(MainActivity.class, new DaggerActivityTestRule.OnBeforeActivityLaunchedListener<MainActivity>() { @Override public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) { DaggerApplication app = (DaggerApplication) application; AppComponent mTestAppComponent = DaggerMainActivityTest_TestAppComponent.builder() .appModule(new AppModule(app)) .build(); app.setTestComponent(mTestAppComponent); } }); @Singleton @Component(modules = {TestServiceModule.class, AppModule.class}) interface TestAppComponent extends AppComponent { } @Module static class TestServiceModule { @Provides @Singleton HelloService provideHelloService() { return new HelloService() { @Override public String greet(String userName) { return GREET_PREFIX + userName; } }; } @Provides @Singleton WeatherService provideWeatherService() { return new WeatherService() { @Override public int getWeatherInfo(String city) throws InvalidCityException { return 65; } }; } } @Test public void greetButtonClicked() { onView(withId(R.id.userNameText)) .perform(typeText(MOCK_NAME), closeSoftKeyboard()); onView(withId(R.id.cityText)).perform(clearText(), typeText(MOCK_PLACE), closeSoftKeyboard()); onView(withId(R.id.btnShowInfo)).perform(click()); onView(withId(R.id.resultView)).check(matches(withText(MOCK_RESPONSE_MESSAGE))); } }

In the test method greetButtonClicked(), a simulation for entering a username and a city is performed and then the show information button is clicked and finally the returned result is being tested against the mock message.

Check the app source code

All the source code of this Dagger 2 app can be found in:
https://github.com/hazems/Dagger-Sample/tree/dagger2-mvp-espresso1

Feel free to use and let me know if you have comments.

Finally, I hope that these three articles can provide a useful introduction to the cool Dagger2 DI framework.


Originally published at dzone.com.