MVP In Practice – Swapping the Network Layer

One of the many benefits of using a clean architecture in Android is that it allows us to replace individual components of our app without having to touch the rest of the code (and everything breaking).

In this post I want to show how easy it is to change one networking library for another (Volley <-> Retrofit) when the networking logic is entirely contained within the network layer!

Something that I see too often for my liking are apps where the network logic sits alongside everything else in the Activity/Fragment.  Take for instance this example method from the BadTimesActivity class:

private void getCatsData() {

    Retrofit retrofit = App.getInstance().getRetrofit();
    RetrofitCatsHttpInterface http = retrofit.create(RetrofitCatsHttpInterface.class);

    recycler.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    loadingView.setVisibility(View.VISIBLE);

    Subscription s = http
            .getCats()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                    result -> {
                        if (result.isSuccessful()) {

                            recycler.setVisibility(View.VISIBLE);
                            errorView.setVisibility(View.GONE);
                            loadingView.setVisibility(View.GONE);

                            adapter.setCats(result.body());
                        }
                        else {
                            
                            recycler.setVisibility(View.GONE);
                            errorView.setVisibility(View.VISIBLE);
                            loadingView.setVisibility(View.GONE);
                        }
                    },
                    throwable -> {
                        throwable.printStackTrace();
                        
                        recycler.setVisibility(View.GONE);
                        errorView.setVisibility(View.VISIBLE);
                        loadingView.setVisibility(View.GONE);
                    }
      subscriptions.add(s);
}

There are many problems with this approach but they all generally mean that your code will become more and more difficult to maintain over time as any changes needed to one part of the code will have a ripple of side effects.

  • There is repetition of code
  • The one class is responsible for both Network & View
  • It would be very hard to test as we can’t separate out the parts to provide mocks

Separating Into Model View Presenter (MVP)

Ok lets start by taking our BadTimesActivity above and making a first attempt at splitting the separate responsibilities out into their own layers.  We can then make further improvements afterwards.  For this example I will split it into the following layers:

View – This will be an interface which the Activity implements.  It is very dumb and has only two responsibilities.  Firstly displaying what the Presenter tells it and secondly passing any user input (clicks) to the Presenter.

This interface just has three methods to represent the Loading, Content & Error states (but there is no reason it can’t have more in a more complex app)

interface MainView {

    void setContentView(List<Cat> cats);
    void setLoadingView();
    void setErrorView();
}

Our Activity now implements MainView and the three methods.  It no longer has any idea about where the data is coming from so we are free to change that in the future without changing this class at all.

@Override
public void setContentView(List<Cat> cats) {

    recycler.setVisibility(View.VISIBLE);
    errorView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);

    adapter.setCats(cats);
}

@Override
public void setLoadingView() {

    recycler.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    loadingView.setVisibility(View.VISIBLE);
}

@Override
public void setErrorView() {

    recycler.setVisibility(View.GONE);
    errorView.setVisibility(View.VISIBLE);
    loadingView.setVisibility(View.GONE);
}

Presenter – This will be a pure java class which implements the ‘business logic’ or our application.  It fetches the data from the model and formats it in a way that can be passed to the view for display.

class MainPresenter extends Presenter<MainView> {

    private CatsRepository catsRepository;

    MainPresenter(CatsRepository catsRepository) {
        this.catsRepository = catsRepository;
    }

    void init() {
        subscribeToCats();
    }

    private void subscribeToCats() {

        view().setLoadingView();
        
        Subscription s = catsRepository
                .getCats()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        response -> {
                            if (response.isSuccessful()) {
                                view().setContentView(response.body());
                            }
                            else {
                                view().setErrorView();
                            }
                        },
                        throwable -> {
                            throwable.printStackTrace();
                            view().setErrorView();
                        }
                );
        unsubscribeOnUnbindView(s);
    }

    void tryAgain() {
        
        clearSubscriptions();
        subscribeToCats();
    }
}

Repository – This is an abstraction layer which sits between the Presenter and the model classes.  It allows us to hide the source of the data from the Presenter (which should neither know nor care).  In many cases it will simply pass the request onto the appropriate model class but it allows the flexibility to change the implementation at a later date without changing anything else

public class CatsRepository {

    private CatsHttp http;

    public CatsRepository(CatsHttp http) {
        this.http = http;
    }

    public Observable<Response<List<Cat>>> getCats() {
        return http.getCats();
    }
}

Model – In this simple example app there will be only one model type and that is the Http classes.  In more complex applications you would likely have models for database, shared prefs, cache, etc.

public class CatsHttp {

    private CatsRestApi http;

    public CatsHttp(Retrofit retrofit) {
        http = retrofit.create(CatsRestApi.class);
    }

    public Observable<Response<List<Cat>>> getCats() {
        return http.getCats();
    }
}

This is now much better however there are still some things I think we need to improve.  As you can see above I am passing the Response result of the Retrofit request back to my Presenter.  This means that we are leaking implementation details of the network layer through our Repository and into the presentation layer.  On top of that we are leaking an external class from our network library causing more of our code than necessary to have a dependency on it.

Creating Our Own Data Class

To solve this problem we can create our own data transport class which we can use to pass the required data around our application.  Let’s define it as follows:

public class Data<T> {

    public static final int LOADING = 0;
    public static final int SUCCESS = 1;
    public static final int ERROR = 2;

    public final int status;
    public final T result;
    public final DataError error;

    public Data(int status, T result, DataError error) {
        this.status = status;
        this.result = result;
        this.error = error;
    }
}

Now instead of our CatsHttp class returning Retrofit’s Response we can update it to return our own Data:

@Override
public Observable<Data<List<Cat>>> getCats() {

    return http
            .getCats()
            .map(response -> response.isSuccessful()
                    ? new Data<>(Data.SUCCESS, response.body(), null)
                    : new Data<List<Cat>>(Data.ERROR, null, new DataError(1, "Request failed"))
            );
}

And update our Repository so that it no longer gives any indication of where the data is coming from:

public Observable<Data<List<Cat>>> getCats() {

    return http
        .getCats()
        .startWith(new Data<>(Data.LOADING, null, null));
}

With these changes complete the only class that now knows about the implementation of how we fetch the data from the network is our CatsHttp model class.  This means that if at some point in the future we want to change which networking library we are using we only have to change the one class.  All the other layers down from Repository -> Presenter -> View will not know that anything has changed.

As an example we can change CatsHttp to use Volley as follows:

public Observable<Data<List<Cat>>> getCats() {

    PublishSubject<Data<List<Cat>>> subject = PublishSubject.create();

    String url = "http://www.mocky.io/v2/58fcfef20f00004c21513695";

    StringRequest request = new StringRequest(
            Request.Method.GET,
            url,
            response -> {

                Data<List<Cat>> result;
                try {
                    Cat[] catsArray = gson.fromJson(response, Cat[].class);
                    List<Cat> cats = Arrays.asList(catsArray);

                    result = new Data<>(Data.SUCCESS, cats, null);
                }
                catch (Exception e) {
                    result = new Data<>(Data.ERROR, null, new DataError(e));
                }
                subject.onNext(result);
            },
            error -> {

                Data<List<Cat>> result = new Data<>(Data.ERROR, null, new DataError(error));
                subject.onNext(result);
            }
    );

    requestQueue.add(request);

    return subject.asObservable();
}

Although, I think it’s much more likely that you’d want to migrate the other way!

Conclusions

By separating the code from being all together in BadTimesActivity into its own distinct classes with defined responsibilities we have gained several advantages

  • We can make changes to the implementation of any of our layers without having to make changes to the other layers.
  • We have made the code much more testable.  Each of the layers can be tested by providing mocks of the other layers.
  • Removing the dependency of most of our code on a specific network library

The whole source for this project can be found at https://github.com/Jahnold/mvp-switch-network.  There are different branches for the different stages.