Android: Interactors with Retrofit and RxJava
In a previous article I described how to structure your application under the Model-View-Presenter architecture applying dependency injection with Dagger. I covered how to setup a View and its Presenter. Today I would like to share with you how to use an Interactor to communicate the Presenter with the Model.
Setup
For this example, I am going to introduce Retrofit, a networking library that makes it easy to define endpoints from which to retrieve data from the network. Also, I will make use of RxJava (and RxAndroid) to handle asynchronous requests. Let’s begin by updating the dependencies in our application’s build.gradle file.
1
2
3
4
5
6
7
8
9
10
11
dependencies {
...
// Retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
// RxAndroid and RxJava
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.1.1'
}
We added an RxJava adapter to enable Retrofit to return Single objects from its Services. We included the Gson converter only because the endpoint we are going to use in this example returns responses in the JSON format.
The Retrofit Service
Let’s say we want to list the latest news from Geonet, a geological hazard monitoring system from New Zealand. The response we get from https://api.geonet.org.nz/news/geonet looks similar to this:
1
2
3
4
5
6
7
8
9
10
11
{
"feed": [
{
"title": "February 2016 landslides",
"published": "2016-03-22T02:38:15Z",
"link": "http://info.geonet.org.nz/display/slide/2016/03/22/February+2016+landslides",
"mlink": "http://info.geonet.org.nz/m/view-rendered-page.action?abstractPageId=17039668"
},
...
]
}
So let’s create a class to represent a News Story.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class NewsStory {
@SerializedName("title")
private final String title;
@SerializedName("published")
private final Date published;
@SerializedName("mlink")
private final String url;
public NewsStory(String title, Date published, String url) {
this.title = title;
this.published = published;
this.url = url;
}
public String getTitle() {
return title;
}
public Date getPublished() {
return published;
}
public String getUrl() {
return url;
}
}
And another one to represent the response from the service which contains the news feed.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NewsGeonetResponse {
@SerializedName("feed")
private final NewsStory[] newsStories;
public NewsGeonetResponse(NewsStory[] newsStories) {
this.newsStories = newsStories;
}
public NewsStory[] getNewsStories() {
return newsStories;
}
}
We are now ready to define our Service.
1
2
3
4
5
public interface GeonetService {
@GET("news/geonet")
Single<NewsGeonetResponse> getNews();
}
The Interactor
To shield our Presenter from the specifics of Retrofit we are going to create an Interactor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GetNewsInteractor {
private final GeonetService service;
private Disposable disposable;
public GetNewsInteractor(GeonetService service) {
this.service = service;
}
public void execute(
Consumer<? super NewsGeonetResponse> onSuccess,
Consumer<? super Throwable> onError
) {
disposable = service.getNews()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError);
}
public void cancel() {
if (disposable != null && !disposable.isDisposed()) {
disposable.dispose();
}
}
}
Let’s add a reference to GetNewsInteractor into our Presenter.
1
2
3
4
5
6
7
8
9
public class MainPresenter {
private final MainView view;
private final GetNewsInteractor interactor;
public MainPresenter(MainView view, GetNewsInteractor interactor) {
this.view = view;
this.interactor = interactor;
}
}
Dependency Injection
Start by letting GetNewsInteractor be provided to MainPresenter by its Module.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Module
public class MainModule {
private final MainView view;
public MainModule(MainView view) {
this.view = view;
}
@Provides
public GetNewsInteractor provideInteractor(GeonetService service){
return new GetNewsInteractor(service);
}
@Provides
public MainPresenter providePresenter(GetNewsInteractor interactor){
return new MainPresenter(view, interactor);
}
}
Then, create a module which provides GeonetService and its dependencies:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Module
public abstract class ServicesModule {
@Singleton
@Provides
public static Gson provideGson(){
return new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssz").create();
}
@Singleton
@Provides
public static Retrofit provideRetrofit(Gson gson){
return new Retrofit.Builder()
.baseUrl("http://api.geonet.org.nz/")
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
@Singleton
@Provides
public static GeonetService provideGeonetService(Retrofit retrofit){
return retrofit.create(GeonetService.class);
}
}
To be able to inject GeonetService to GetNewsInteractor we need to convert MainComponent into a Subcomponent of ApplicationComponent.
1
2
3
4
5
6
7
@Subcomponent (modules = MainModule.class)
public interface MainComponent extends AndroidInjector<MainFragment> {
@Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder<MainFragment> {
}
}
We have to provide an Injector for our Fragment. Let’s create a module which ApplicationComponent can reference:
1
2
3
4
5
6
7
8
@Module(subcomponents = MainComponent.class)
public abstract class InjectorsModule {
@Binds
@IntoMap
@FragmentKey(MainFragment.class)
abstract AndroidInjector.Factory<? extends Fragment> mainFragmentInjector(MainComponent.Builder builder);
}
Now we can expose the Injector and the Service through ApplicationComponent.
1
2
3
4
5
6
7
8
9
10
11
12
@Singleton
@Component(modules = {
AndroidInjectionModule.class,
InjectorsModule.class,
ServicesModule.class
})
public interface ApplicationComponent extends AndroidInjector<MyApplication> {
@Component.Builder
abstract class Builder extends AndroidInjector.Builder<MyApplication> {
}
}
In order to make ApplicationComponent available from our Application instance, we need to build it when our Application is created.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyApplication extends Application implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;
@Override
public void onCreate() {
super.onCreate();
DaggerApplicationComponent.builder().create(this).inject(this);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return fragmentDispatchingAndroidInjector;
}
}
Don’t forget to add MyApplication as the name attribute of the application element in AndroidManifest.xml.
Finally, we need to update MainFragment so it is injected by the appropriate injector.
1
2
3
4
5
6
7
8
9
10
11
public class MainFragment extends Fragment implements MainView {
@Inject
MainPresenter presenter;
@Override
public void onAttach(Context context) {
AndroidSupportInjection.inject(this);
super.onAttach(context);
}
}
At this stage, MainPresenter can communicate with MainView and GetNewsInteractor. In an upcoming example I will explain how I apply Test Driven Development (TDD) to define the behavior of the MainPresenter while I write a full set of Unit Tests for it.