Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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

DZone's Guide to

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

How to use Dagger to create a simple Android weather app and how to unit test it.

· Mobile Zone
Free Resource

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.

Interaction Diagram of Sample Dagger App Flow

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.

Image title

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.

Topics:
android development ,dagger ,dependecy injection ,unit testing ,mvp ,espresso

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}