Android Architecture Part 4: Applying Clean Architecture on Android, Hands on (source code included)
Originally published at five.agency on June 29, 2017. This is a republished version to celebrate our listing in the Google Developers Agency Program. We also published an E-book, covering all five parts, that you can download here.
In the last part of our Android Architecture series, we adjusted Clean Architecture a bit to the Android platform. We separated Android and the real world from our business logic, satisfied stakeholders and made everything easily testable.
The theory is great, but where do we start when we create a new Android project? Let’s get our hands dirty with clean code and turn that blank canvas into an architecture.
Foundations
We will lay down the foundations first — create modules and establish dependencies among them to be on par with the dependency rule.
Those will be our modules, in order from the most abstract one to the concrete implementations:
1. domain
Entities, use cases, repositories interfaces, and device interfaces go into the domain module.
Ideally, entities and business logic should be platform agnostic. To feel safe,
and to prevent us from placing some android stuff in here, we will make it a pure java module
2. data
The data module should hold everything related to data persistence and manipulation. Here we will find DAOs, ORMs, SharedPreferences, network related stuff like Retrofit services and similar.
3. device
The device module should have everything related to Android that’s not data persistence and UI. In example, wrapper classes for ConnectivityManager, NotificationManager and misc sensors.
We will make both Data and Device modules android modules, as they must know about Android and cannot be pure java.
4. The easiest part, app module (UI module)
This module is already created for you by the Android studio when you create a project.
Here you can place all the classes related to the Android UI such as presenters, controllers, view models, adapters and views.
Dependencies
Dependency rule defines that concrete modules depend on the more abstract ones.
You might remember from the third part of this series that UI (app), DB — API (data) and Device (device) stuff is together in the outer ring. Meaning that they are on the same abstraction level. How do we connect them together then?
Ideally, these modules would depend only on the domain module. In that case, dependencies would look somewhat like a star:
But, we are dealing with Android here and things just cannot be perfect. Because we need to create our object graph and initialize things, modules sometimes depend on an another module other than the domain.
For example, we are creating object graph for dependency injection in the app module. That forces app module to know about all of the other modules.
Our adjusted dependencies graph:
Bricks, a lot of bricks
Finally, it’s time to write some code. To make things easier, we will take an RSS Reader app as an example. Our users should be able to manage their RSS feed subscriptions, fetch articles from a feed and read them.
Domain
let’s start with the domain layer and create our core business models and logic.
Our business models are pretty straightforward:
– Feed — holds RSS feed related data like the url, thumbnail URL, title and description
– Article — holds article related data like the article title, url and publication date
And for our logic, we will use UseCases, aka. Interactors. They encapsulate small parts of business logic in concise classes. All of them will implement general UseCase contract:
public interface UseCase<P> { interface Callback { void onSuccess();
void onError(Throwable throwable);
} void execute(P parameter, Callback callback);
}
The very first thing our users will do when they open our app is add a new RSS feed subscription. So to start with our interactors, we will create AddNewFeedUseCase and its helpers to handle feed addition and validation logic.
AddNewFeedUseCase will use FeedValidator to check feed URL validity, and we will also create the FeedRepository contract which will provide our business logic some basic CRUD capabilities to manage feed data:
public interface FeedRepository { int createNewFeed(String feedUrl); List<Feed> getUserFeeds(); List<Article> getFeedArticles(int feedId); boolean deleteFeed(int feedId);}
Note how our naming in the domain layer clearly propagates the idea of what our app is doing.
Put everything together, our AddNewFeedUseCase looks like this:
public final class AddNewFeedUseCase implements UseCase<String> { private final FeedValidator feedValidator;
private final FeedRepository feedRepository; @Override
public void execute(final String feedUrl, final Callback callback) {
if (feedValidator.isValid(feedUrl)) {
onValidFeedUrl(feedUrl, callback);
} else {
callback.onError(new InvalidFeedUrlException());
}
} private void onValidFeedUrl(final String feedUrl, final Callback callback) {
try {
feedRepository.createNewFeed(feedUrl);
callback.onSuccess();
} catch (final Throwable throwable) {
callback.onError(throwable);
}
} }
*Constructors are omitted for the sake of brevity.
Now, you might be wondering, why is our use case, as well as our callback, an interface?
To demonstrate our next problem better, let’s investigate GetFeedArticlesUseCase.
It takes a feedId -> fetches feed articles via FeedRespository -> returns feed articles
Here comes the data flow problem, the use case is in between presentation and data layer. How do we establish communication between layers? Remember those input and output ports?
Our use case must implement the input port (interface). Presenter calls the method on the use case, and the data flows to the use case (feedId). Use case maps feedId to feed articles and wants to send them back to the presentation layer. It has a reference to the output port (Callback), as the output port is defined in the same layer, so it calls a method on it. Hence, data goes to the output port — presenter.
We will tweak our UseCase contracts a bit:
public interface UseCase<P, R> { interface Callback<R> {
void onSuccess(R return);
void onError(Throwable throwable);
}
void execute(P parameter, Callback<R> callback);
}- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -public interface CompletableUseCase<P> {
interface Callback {
void onSuccess();
void onError(Throwable throwable);
} void execute(P parameter, Callback callback);}
UseCase interfaces are Input ports, and Callback interfaces are output ports.
GetFeedArticlesUseCase implementation is as follows:
class GetFeedArticlesUseCase implements UseCase<Integer, List<Article>> { private final FeedRepository feedRepository; @Override
public void execute(final Integer feedId, final Callback<List<Article>> callback) {
try {
callback.onSuccess(feedRepository.getFeedArticles(feedId));
} catch (final Throwable throwable) {
callback.onError(throwable);
}
}
}
One last thing to note in the domain layer is that Interactors should only contain business logic. And in doing so, they can use repositories, combine other interactors and use some utility objects like FeedValidator in our example.
UI
Awesome, we can fetch articles, let’s show them to the user now.
Our view has a simple contract:
interface View { void showArticles(List<ArticleViewModel> feedArticles);
void showErrorMessage();
void showLoadingIndicator();
}
Presenter for that view has a very simple presentation logic. It fetches articles, maps them to the view models and passes on to the view, simple, right?
Simple presenters are yet another feat of the clean architecture and presentation-business logic separation.
Here is our FeedArticlesPresenter :
class FeedArticlesPresenter implements UseCase.Callback<List<Article>> { private final GetFeedArticlesUseCase getFeedArticlesUseCase;
private final ViewModeMapper viewModelMapper;
public void fetchFeedItems(final int feedId) {
getFeedArticlesUseCase.execute(feedId, this);
} @Override
public void onSuccess(final List<Article> articles) {
getView().showArticles(viewModelMapper.mapArticlesToViewModels(articles));
} @Override
public void onError(final Throwable throwable) {
getView().showErrorMessage();
}
}
Note that FeedArticlesPresenter implements Callback interface and it passes itself to the use case, it is actually an output port for the use case and in that way it closes the data flow. This is the concrete example of the data flow that we mentioned earlier, we can tweak labels on the flow diagram to match this example:
Where our parameter P is integer feedId, and return type R is a list of the Articles.
You do not have to use presenters to handle presentation logic, we could say that Clean architecture is “frontend” agnostic — meaning you can use MVP, MVC, MVVM or anything else on top of it.
Let’s throw some rx in the mix
Now, if you were wondering why there is such hype about RxJava, we will take a look at the reactive implementation of our use cases:
public interface UseCase<P, R> {
Single<R> execute(P parameter);
}- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - public interface CompletableUseCase<P> {
Completable execute(P parameter);
}
Callback interfaces are now gone and we use rxJava Single/Completable interface as our output port.
Reactive implementation of the GetFeedArticlesUseCase:
class GetFeedArticlesUseCase implements UseCase<Integer, List<Article>> {
private final FeedRepository feedRepository; @Override
public Single<List<Article>> execute(final Integer feedId) {
return feedRepository.getFeedArticles(feedId);
}
}
And reactive FeedArticlePresenter is as follows:
class FeedArticlesPresenter {
private final GetFeedArticlesUseCase getFeedArticlesUseCase;
private final ViewModeMapper viewModelMapper; public void fetchFeedItems(final int feedId) {
getFeedItemsUseCase.execute(feedId)
.map(feedViewModeMapper::mapFeedItemsToViewModels)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onSuccess, this::onError);
}
private void onSuccess(final List articleViewModels) {
getView().showArticles(articleViewModels);
}
private void onError(final Throwable throwable) {
getView().showErrorMessage();
}
}
Although it’s a bit hidden, the same data flow inversion principle still holds, because without RxJava presenters were implementing the callback, and with RxJava subscribers are also contained in the outer layer — somewhere in the presenter.
Data and Device
Data and Device modules contain all of the implementation details that business logic does not care about. It only cares about the contracts, allowing you to easily test it and swap out implementations without touching the business logic.
Here you can use your favorite ORMs or DAOs to store data locally and network services to fetch data from the network. We will implement FeedService to fetch articles, and use FeedDao to store articles data on the device.
Each data source, both network, and local storage, will have its own models to work with.
In our example, they are ApiFeed — ApiArticle and DbFeed — DbArticle.
Concrete implementation of the FeedRepository is found in the Data module as well.
Device module will hold implementation of the Notifications contract that is a wrapper around the NotificationManager class. We could perhaps use Notifications from our business logic to show the user a notification when there are new articles published in which the user might be interested in and drive engagement.
Models, models everywhere.
You might have noticed that we mentioned more models than just entities or business models.
In reality, we also have db models, API models, view models and of course, business models.
It is a good practice for every layer to have its own model that it works with, so your concrete details, such as views, do not depend on the specific details of your lower layer implementations. This way, you won’t have to break unrelated code if you, for example, decide to change from one ORM to another.
To make that possible, it is necessary to use object mappers in each layer. In the example, we used ViewModelMapper to map domain Article model to the ArticleViewModel.
Conclusion
Following these guidelines, we created a robust and versatile architecture. At first, it may seem like a lot of code, and it kind of is, but remember that we are building our architecture for the future changes and features. And if you do it correctly, future you will be thankful.
In the next part, we will cover maybe the most important part of this architecture, its testability and how to test it out.
So, in the meantime, what part of the architecture implementation did you find interesting the most?
This is a part of Android Architecture series. Check our other parts:
P.S. You can download the source here.
This is the 4th part of Android Architecture series. Continue to Part 5:
Check out previous parts:
Words: Mihael Franceković / Illustrations: Alen Stojanac