In the past, maintainability of Android applications was quite a headache because activities, fragments and views were tightly coupled with one another. To remedy the situation, the Android development community introduced the idea of using MVP (Model-View-Presenter) pattern to lessen code coupling. In 2017, Google softly pushed for the usage of MVVM(Model-View-ViewModel) pattern with the announcement of architecture components’ ViewModel, the primary purpose of which was to help manage Android’s lifecycle better. More improvements in the Android architecture followed with the release of the Architecture Components which were aimed at aiding developers in creating scalable, maintainable and testable applications.

With this article, we would like to share our journey in implementing a modularized application to help us scale, maintain and test our app with much more ease. We will discuss our architecture, why we decided to apply modularisation, how we modularized our app, what MV* pattern we used and what we learned from all of it.

Our architecture

In 2017, we were faced with the problem that our code was no longer maintainable; the codebase featured an improperly implemented MVP pattern, was hard to scale and potential side effects of the changes applied to the codebase were not immediately apparent. We had a big serving of spaghetti and we were not enjoying it!

The following year, we were given the opportunity to rewrite our app from scratch. We brainstormed on possible architectures wherein we could have have clear separation of concern to enable us to scale efficiently and have testable code to ensure its quality. Eventually, we came up with an architecture inspired by Uncle Bob’s clean architecture.

From the figure above, it is apparent that it is a simple architecture wherein there is a clear separation for API calls and local database related logic (data layer) from UI related logic (presentation layer). With this architecture, we would only need to expose repository interfaces to the presentation layer; the presentation layer would be unaware if the data comes either from an API or a local database. Thus, it would be possible to easily switch data sources without having any impact on the presentation layer.

Additionally, it would be easier for us to add unit tests for both layers because we would only need to test the concern of each layer. For the data layer, it would be verifying if the repository is parsing the data properly into the entities or if the local database is adding/removing/updating items correctly. And for the presentation layer, we would only check if each view model emits the correct state after performing an action.

It became obvious to us that with this approach, we could separate the app into two layers or modules. But why modularize when it is also possible to separate them by package? In our case, we had two reasons:

We knew we were going to be using Kotlin to develop the new app and we wanted to take advantage of Kotlin’s language feature. With Kotlin’s internal visibility modifier, we can make a class/function/variable only available for a module which in turn helps us have additional safeguard for having separation of concern. We wanted to have the flexibility to be able to use the data layer for another app easily without having to worry about it being coupled with the UI or presentation layer.

Our modularization implementation

As explained above, we separated our app into two modules — data and app modules.

Data module — contains all API calls and follows the standard repository pattern. The repository interfaces are the only one exposed and it does not expose its implementation classes.

App module — contains all UI related code. It has all the activities, fragments and custom views needed for each screen of the application and also has the widget provider for the app widget. This module depends on the data module.

How our Android project looks like

We made the choice not to have separate modules for each feature of the app because our app consists only of small sized features, thus, it would not be very practical. Although for medium to enterprise size applications, it would be wise to separate the presentation layer into different feature modules. This is advantageous for larger teams that want to work simultaneously in different modules without impacting the other. Again, this emphasizes separation of concern.

How to provide the repositories to the app module without exposing their implementation classes?

After we decided that we were going to modularize our app, we faced a new challenge — how to provide the repositories to the app module without exposing their implementation classes? Our answer was Dagger2, a dependency injector library for Android and Java.

We created a dagger component inside the data module called DataComponent which provided all the repositories that the presentation layer might need.

@Component

interface DataComponent {

fun providesSampleRepository() : SampleRepository

...

}

For the presentation layer to be able to use this, we created a dagger component called AppComponent and added the DataComponent as its dependency. This provided the AppComponent access to the repositories from the DataComponent.

@Component(dependencies = [

DataComponent::class

])

internal interface AppComponent {...}

Another thing to note is that the repositories in the DataComponent are scoped to only have one instance. This ensures that when the repository is already declared, it would not create another instance which in turn reduces memory usage.

Our MV* pattern

In the old version of the Weeronline app, MVP (model-view-presenter) was the pattern used to extract the business logic from activities and fragments. Interfaces were created for both the view and presenter to override so they would not communicate “directly” with each other.

It was a useful approach to lessen the load for activities and fragment, however, the presenter was still aware of the view thru its interfaces meaning we still had a reference of the View inside the Presenter. As much as possible, we wanted to avoid this because it might have caused memory leaks if a process in the Presenter was not stopped when the View was destroyed.

For the new app, we still wanted the advantages that MVP pattern gave us but with two additional benefits:

something that would ensure that we do not have a reference to the view, making the business logic handler unaware of the existence of the view

something that can help us ensure we stop background tasks on process death

Thus, it was a no brainer that we chose MVVM (model-view-viewmodel) pattern.

How we implemented MVVM?

There are different ways to implement MVVM, some with data binding and some without, but one thing that remains constant in its concept is that the ViewModel is unaware of the View and the View listens for emissions from the ViewModel.

In order to implement the MVVM pattern in our app, we used architecture components’ ViewModel and LiveData and Kotlin’s sealed classes. We did not use Android’s data binding library as we found it unnecessary for our needs.

To demonstrate, I will provide an example using our search screen. It allows the user to search for a place, display a list of results when it finds a match and display a message if it did not find a result based on the provided text.

State (Model)

The State is the one we treat as the Model in the MVVM pattern. It contains valuable information for the View to render. Using Kotlin’s sealed classes, we define search screen’s possible states — InitState, SearchingState, NoResultState, and SearchResultState.

internal sealed class SearchState {

/**

* Initial state of search

*/

object InitState : SearchState()



/**

* State when search is ongoing (fetching from API)

*/

object SearchingState : SearchState()



/**

* State when there are no results found from the search string provided

*

* @param searchString - string used to search

*/

data class NoResultState(val searchString: String) : SearchState()



/**

* State with search results

*

* @param searchedPlaces - list of places returned for a given search

*/

data class SearchResultState(val searchedPlaces: List<Place>) : SearchState()

}

ViewModel

The ViewModel contains the logic to search for a place based on a query. It holds the State with LiveData to emit it to the View.

internal class SearchViewModel : ViewModel() {

private val _searchState = MutableLiveData<SearchState>()

val searchState: LiveData<SearchState> = _searchState



fun searchPlaces(searchString: String?) {

searchString?.takeIf { it.trim().length >= SEARCH_MIN_INPUT_CHARS }?.let {

Observable.timer(

SEARCH_THROTTLE_INTERVAL,

TimeUnit.MILLISECONDS

).flatMap {

placeRepository.search(searchString)

}.doOnSubscribe {

_searchState.postValue(SearchState.SearchingState)

}.subscribe({

if (it.isEmpty()) _searchState.postValue(SearchState.NoResultState(searchString))

else _searchState.postValue(SearchState.SearchResultState(it))

}, {

_searchState.postValue(SearchState.NoResultState(searchString))

})

return

} ?: _searchState.postValue(SearchState.InitState)

}

}

View

The View is dumb and listens only for the State that the ViewModel provides in order to render the user interface. From the code below, we have an instance of the ViewModel and in onCreate we listen for State emissions.

internal class SearchActivity : BaseActivity() {



@Inject

lateinit var viewModelFactory: ViewModelProvider.Factory



private val searchViewModel: SearchViewModel by lazy {

ViewModelProviders.of(this, viewModelFactory)[SearchViewModel::class.java]

}





override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)



searchViewModel.searchState.observe(this,

Observer { state ->

renderSearchState(state)

})

}



private fun renderSearchState(searchState: SearchState?) {

if (searchState == null) return

when (searchState) {

is SearchState.InitState -> renderInitState()

is SearchState.SearchingState -> renderSearchingState()

is SearchState.NoResultState -> {

renderNoResultState(searchState.searchString)

}

is SearchState.SearchResultState -> {

renderSearchResultState(searchState.searchedPlaces)

}

}

}

Using the same technique for all screens, we were able to easily pinpoint bugs as there is now a clear separation of responsibility between the business logic and rendering of the view.

Learnings

Diving into unknown territory is always an adventure and a risk. In implementing our new architecture, we have improved our codebase considerably, however it was not without its challenges. There are two major learnings we would like to share:

having the data module expose only its interfaces and necessary classes helped us when nearly at the end of development we needed to change our source of forecast data. Since the app module only knows about the interfaces and data models, we were able to switch easily into a new data source without changing the app module.

setting up the dependency graph was a bit tricky with a multi-module project. We wanted the modules to expose only what was necessary — such as the repository interfaces — and we chose to use Dagger for this. We can still improve by creating our own simple dependency injection solution so that our data module would not need to depend on Dagger. This might also give us the flexibility to change the dagger scope of each repository in the presentation layer itself.

After almost 8 months of development and around 5 months of the app being in production, we are feeling the positive effects of adopting the new architecture. Developing new features has become a breeze; we are now deploying to production faster and with fewer bugs. We have removed code coupling in our code and improved code readability.

With the revamped architecture, we, as developers, are happy with our code and to that effect, we are also making our users happy by giving them requested features faster and with quality!