I thought it was time to write a series of articles about how to design a proper, scalable architecture that you may apply to any medium-large projects. This series of articles is the sum of all the knowledge that I’ve learned from working on different projects for different companies. Working on all these different projects has taught me the does and donts that you need to keep in mind when you are working on a large project. There are things related to the technology itself and there are things related to how the architecture that you choose to apply is going to impact your engineering team.
This article assumes that you are at a semi-senior or senior level. That means that you feel comfortable working with the most well-known technologies related to Android development, such as Dagger, Kotlin, RxJava, Google-related libraries (FCM, Remote Config, Crashlytics) and obviously, the Android SDK.
I’m going to start talking about the tech-stack setup, then show the example app that I have created for this article, and then in the next article I'll go on through each layer of the architecture.
Since some time ago I have permanently moved from MVP to MVVM because MVVM has become the first officially supported Android architecture, through Android’s LiveData and ViewModels. Also, since this article aims towards a large-scale architecture this is going to be a multi-module architecture, with feature modules, a core module, and a base module.
The technologies that I’m going to use here are; Dagger for dependency injection, OkHttp/Retrofit as my networking layer, RxJava to wrap the network layer into repositories.
I have created an example app as an example - no pun intended - of how this architecture works. This example app shows different real world scenarios, scenarios where we have to use cache, perform search, mix different endpoints, and so on.
Each feature module shows different use cases and different scenarios. I often see that when you look for examples online you only see the most straightforward, simple and unrealistic scenarios¹. For example, just fetching something from the backend and display it on the UI. In my personal experience that’s not how real-world development works like. In the real world, you’re going to have to go back and forth with different resources within your company, and due to different constraints you may have to settle with API endpoints that are not totally mobile-friedly², or you may have to hit different endpoints to display the whole information to the user.
The "RxJava Example #1" shows how we can conditionally zip two API calls together.
In this example, we use two different endpoints to fetch the "recently viewed news" and the regular paginated news. If we do a Pull-to-Refresh or open the Activity from scratch, we are going to fetch both, the recently viewed news and the first page of the paginated news. If we continue to scroll down until we reach the next page, we are only going to call the API to fetch the second page.
This example also handles error situations like doing a Pull-to-Refresh without internet connection.
The "RxJava Example #2" shows how we can zip responses from different endpoints through RxJava.
In this example, we zip the responses from two different endpoints, one that provides the user's "business skills information" and another that provides the user's "personal information". We zip these requests together through RxJava and use wrapper objects to carry the responses around. Also, we handle different error situations for each endpoint and update the UI accordingly.
This example supports Pull-to-Refresh behavior and handling no internet connection states.
The "Cache Example" shows how we can use a cache repository and a network repository to display data to the user, update data dynamically, and without using any sort of EventBus technology, get these updates into the UI by relying on Room's Flowables.
In this example, we fetch a list of recipes from a mock endpoint host on ApiAry, store those recipes on cache, and allow the user to bookmark the recipes. When we bookmark a recipe we can see the change reflected on the recipe list as soon as we do it, all this thanks to Room's Flowables.
This example also supports Pull-to-Refresh behavior and handles different error states, such as trying to fetch the list of recipes without internet connectivity.
The "LiveData Example" shows how we can achieve a complex search by relying on LiveData.
In this example, we can search image posts using the Imgur API, through this API we are able to search using a query String as the search term, and we have a few filtering options. We can select if we want to get the latest posts (Top) or if we want to get the most trendy posts (Viral). If we choose to filter by top posts, we can also specify a date windows, getting the top posts from the day, week, or month.
This example supports handling the Pull-To-Refresh gesture and pagination. Also, we handle different error states such as trying to search for something without internet connection (it shows a Snackbar if we are seeing content or a full error state if we open the Activity from scratch).
All this is done through LiveData.
In the next article, I will go in detail through each layer of the architecture.
 By unrealistic scenarios I mean when you go through an article and they show you how to fetch data, save it on cache, and display it on a RecyclerView. That’s all well and good; But what happens if you fetch a 500 (Internal error)? What happens if you fetch a 304 (Not Modified)? Do you waste time and store a duplicate response on the cache? What happens if there’s no internet connection? All these edge-case scenarios, that are actually very likely to happen in the real world, are totally omitted by most articles.
 Some may argue that if this was to happen, you should push forward and settle for nothing else other than what you want. But that’s not how a company works. A company needs to deliver a product. Backend and frontend developers, they have deadlines, and trying to fix an endpoint so it is more mobile-friendly may take time, time that the backend resources don’t have.