Creating Clean Architecture Multi-Project App

One of the best ways to experiment with frameworks or libraries in Android is to create a ToDo-style app. This is exactly what I did recently in order to explore what Conductor library has to offer. One thing led to another and I ended up with a practical example of a Clean Architecture App that I thought I’d share here in hope that someone will learn from it. Feel free to jump to the source code directly at https://github.com/jshvarts/ConductorMVP
The app can be summarized using these points:
- MVP design pattern
- Clean Architecture
- Multi-Project (multi-module) Gradle setup with each layer in its own module
- Single Activity app using Conductor library
- 100% Kotlin
- Dagger 2 with custom scopes
- Data layer implementation with Room
- Data access using RxJava2
- Near 100% unit test coverage
- PMD, FindBugs, Checkstyle, Lint with custom rules integrated
- Travis CI for continuous integration
Let’s go into details about each point above.
MVP design pattern
This design pattern has been around for some time now so there is plenty of info about it out there — I won’t rehash it here. Suffice it to say that the pattern encourages component separation and making your Views “dumb” (i.e. passive). The Views delegate all user interaction events to its Presenters. Each Presenter works with the Data layer via the Domain layer to process a particular request. Once done, the Presenter tells the View what to do next (display data, etc.). Views do not hold any business logic hence they are described as passive.
Presenters should be framework-independent so they could be tested using fast JUnit/Mockito rather than resorting to slow Robolectric tests.
Check out how the Presentation layer is implemented here. Packages are set up by feature (screen):

Clean Architecture
A lot has been written about Clean Architecture already on Medium alone. If you work on a new app, there is no good excuse not to structure your code with Clean Architecture in mind. While the actual implementation will vary widely, the important things to remember are:
- Views should not contain any business logic and should only interact with Presenters (or ViewModels if you use MVVM).
- Presenters should not contain any business logic and should talk to Domain layer via Interactors/Use Cases.
- Domain layer contains business logic and Data layer abstractions. It serves as a middle-man between Presentation and Data layers yet it has no knowledge about either of them by virtue of Inversion of Control. Business logic (the most important aspect of your app) is centralized for a good reason: for ease of maintenance, it should not be spread across many layers or, worse, duplicated in multiple places. When it comes to updating your business rules, you should not be forced to debug the whole codebase to determine how it works — the Domain layer is the place to look.
- Data layer is simply an implementation of Domain layer abstractions. The goal is to isolate the Data layer from the Presentation layer to the point that we can easily swap it should the need arise. For instance, this sample project uses Room to implement its Data layer but we can easily swap it for another database implementation. All Data layer does is implement the contract (set of interfaces) defined by the Domain layer. Then new implementation can be “plugged-in” into the rest of the application simply via updating a single Gradle dependency, as you will see below.
The center of your application is not the database. Nor is it one or more of the frameworks you may be using. The center of your application is the use cases of your application. -Uncle Bob
Designing for separate layers forces us to think of dependencies in isolation. It makes code easier to reason about, maintain, extend and test. Making each layer a separate Gradle module is a good way to do it.
Multi-Project Gradle setup
This app contains 3 Gradle modules:
- presentation — Android application module
- domain — Java library module
- data— Android library module
The presentation module depends on domain and data modules via Gradle dependencies config:
dependencies {
implementation project(":domain")
implementation project(":data")
...
}
The data module depends on domain module (it implements repository interface defined in domain module):
dependencies {
implementation project(":domain")
...
}
The domain module does not depend on any modules. Being the layer that contains your Business Rules/Use Cases/Repository abstractions, the domain layer should be shielded from changes as much as possible.
We should not need to modify the Domain layer as a result of changes to Data or Presentation layers.
Having separate modules also gives us build speed improvements since the modules can now be built in parallel. Simply enable parallel processing in Gradle in project’s gradle.properties:
# When configured, Gradle will run in incubating parallel mode.
org.gradle.parallel=true
Unifying Gradle dependencies
RxJava dependency is used in every layer of this app and it would not make sense to specify and maintain its version in every layer separately (it would introduce maintenance headache in the long run). Therefore, we define it at the project level and then add as a dependency to each layer:
Top-level build.gradle:
buildscript {
ext {
rxJavaVersion = "2.1.6"
libs = [
rxJava: ('io.reactivex.rxjava2:rxjava:' + rxJavaVersion),
]
}
Each module that requires RxJava now simply uses this dependency by adding the following in its own build.gradle:
dependencies {
implementation libs.rxJava
...
}
Similar approach can be taken regarding the SDK config.
Top-level build.gradle:
allprojects {
ext {
androidCompileSdkVersion = 26
androidMinSdkVersion = 19
androidTargetSdkVersion = 26
}
Android modules (presentation and data) have the following setup in their build.gradle:
android {
def globalConfiguration = rootProject.extensions.getByName("ext")
compileSdkVersion globalConfiguration["androidCompileSdkVersion"]
defaultConfig {
minSdkVersion globalConfiguration["androidMinSdkVersion"]
targetSdkVersion globalConfiguration["androidTargetSdkVersion"]
...
}
Single Activity app using Conductor library
If you’ve dealt with Fragments in Android, you probably walked away seeking a better alternative to build Android apps. Fragment lifecycle adds a lot of complexity (and potentially bugs). A lot has been written about it and several libraries were created to address the issue.
One of the best alternatives to using Fragments is open-source Conductor library from BlueLine Labs.
Apps built with Conductor have the following in common:
- A single Activity to bootstrap your root View and enable navigation via ActionBar/Toolbar
- Each View is implemented as a
Controller
. It is described as a lighter-weight and more predictable Fragment alternative with an easier to manage lifecycle. Router
is used for navigation and backstack management to transition between Views (Controllers
).ControllerChangeHandlers
can be used for animating enter and exit transition animations between Controllers. Conductor comes with several out-of-the-box or you can roll your own.
For instance, in this app the single Activity displays the main screen, a List of Notes, via router.setRoot()
router = Conductor.attachRouter(this, container, savedInstanceState)
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(NotesView()))
}
Clicking on an item in the RecyclerView of the Notes View screen, opens a Note Detail screen via router.pushController()
val noteDetailView = NoteDetailView().apply {
args.putLong(NoteDetailView.EXTRA_NOTE_ID, note.id)
}
router.pushController(RouterTransaction.with(noteDetailView)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
Note that the enter and exit transition animations (set with pushChangeHandler() and popChangeHandler() respectively) are completely optional but make user experience much more polished.
To navigate to the Edit Note screen from the Note Detail screen, the code is simply:
val editNoteView = EditNoteView().apply {
args.putLong(NOTE_ID, this@NoteDetailView.args.getLong(NOTE_ID))
}
router.pushController(RouterTransaction.with(editNoteView)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
Note that the Edit Note screen is pushed onto the backstack on top of the Note Detail screen. If we need to implement navigation back up the backstack (to Note Detail screen) without ActionBar, the code is simply:
router.popCurrentController()
Working with Controllers
Controllers are a lot simpler to use with than Fragments. There are fewer lifecycle callbacks to deal with and, unlike Fragments, you don’t need to rely on Android framework to create them. You instantiate them yourself via constructor with or without a Bundle
.
I find these Controller lifecycle callbacks to be the most useful:
onContextAvailable(context: Context)
—signifies that Controller is attached to an Activity (even if it’s not visible). Inject your Dagger dependencies here.onCreateView(inflater: LayoutInflater, container: ViewGroup): View)
— similar to Fragments. This is where you inflate your View and bind your UI widgets.onAttach(view: View)
— Controller is attached to the Window and Activity is visible. This is a good place to bind your Presenter.onDetach(view: View)
— Controller is detached from the Window of an Activity that became invisible. This is a good place to unbind your Presenter.onDestroyView(view: View)
— use it you if you need to perform an action during device configuration change.onDestroy()
— invoked when your Controller is getting destroyed. This is where I clear RxJava2 disposables.
Note: onDestroy()
does not get called on device configuration changes since Controllers survive configuration changes similar to Fragments with Fragment#setRetainedInstance(true)
or ViewModels from Architecture Components.
Injecting dependencies in Controller
The MVP design in this case is not ideal for Dagger constructor injection (preferred injection type) so the Controller (View) uses field injection to inject its Presenter.
As you saw above, Controllers survive configuration changes, such as device rotation. There is no onCreate()
or other lifecycle callbacks that happen only once. To avoid re-injecting Dagger dependencies every time, the following was added to BaseView
:
abstract class BaseView : Controller() {
// Inject dependencies once per life of Controller
val inject by lazy { injectDependencies() }
override fun onContextAvailable(ctx: Context) {
super.onContextAvailable(ctx)
inject
}
This way despite fun onContextAvailable(context: Context)
being invoked on configuration changes, Lazy
delegate guarantees that the injection happens only once and the value is re-used during Controller lifetime.
Dagger 2 with custom scopes
The example uses Dagger 2 to inject Application-wide (Singleton) dependencies as well as screen-specific dependencies. For instance, the Repository is Singleton Application-wide dependency:
@Singleton
@Provides
fun provideNoteRepository(noteDao: NoteDaoImpl, mapper: NoteModelMapperImpl): NoteRepository = NoteRepositoryImpl(noteDao, mapper)
Note that Dagger ties domain abstractions (such as NoteRepository
interface) and data implementations of those abstractions (such as NoteRepositoryImpl
). Take a look at the di package and screen-specific Modules in the example app — it should be self-explanatory.
On the other hand, Note Detail screen defines its own dependencies that live only during the lifetime of that particular screen. These include NoteDetailUseCase
(aka Interactor) and NoteDetailPresenter
:
@PerScreen
@Provides
fun provideNoteDetailUseCase(noteRepository: NoteRepository) = NoteDetailUseCase(noteRepository)@PerScreen
@Provides
fun providePresenter(noteDetailUseCase: NoteDetailUseCase, deleteNoteUseCase: DeleteNoteUseCase) = NoteDetailPresenter(noteDetailUseCase, deleteNoteUseCase)
Room for Data layer
If you’ve followed my previous articles, you’ve probably noticed that I am a big fan of Room persistence library announced at Google I/O 2017. You can do a lot with very little code simply by defining interfaces with annotations. And a cherry on top is compile-time checking! I just wish Room also supported Completable
RxJava2 return type out-of-the-box. Most likely support for this is coming soon.
Data access with RxJava2
The power of RxJava can be seen all layers of the app. In addition to the ease of making data access asynchronous, RxJava with its powerful operators helps make the code concise as well. For instance, the entire NoteDetailPresenter
is basically this function:
override fun loadNote(id: Long) {
disposables.add(noteDetailUseCase.findNoteById(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { view?.showLoading() }
.doFinally { view?.hideLoading() }
.subscribe({ view?.onLoadNoteSuccess(it) }, { view?.onLoadNoteError(it) }))
}
How many lines of code would it take to accomplish the same without RxJava and Kotlin?
Another instance where RxJava shines is in the Domain layer: AddNoteUseCase
validates Note
and sends it to the data layer only if the Note
is valid. This is done using 1 line of code simply by chaining two Completable
streams with help of the andThen()
operator:
fun add(note: Note): Completable = validate(note).andThen(repository.insertOrUpdate(note))
Near 100% unit test coverage
Separating Data layer and injecting Repository into our Use Cases makes it easy to test layers in isolation as well as how they interact with each other. Repository is mocked while Use Cases are not. This can help detect integration issues between Presenters, Use Cases and Repositories sooner rather later which will limit the overhead of maintaining tests when you business logic changes. For instance:
private val repository: NoteRepository = mock()
private val addNoteUseCase: AddNoteUseCase = AddNoteUseCase(repository)@Before
fun setUp() {
testSubject = AddNotePresenter(addNoteUseCase)
}
The Repository in the Data layer is fully tested separately. See the code here.
Using Jacoco plug-in to measure code coverage is a great way to see tangible results of your efforts.
PMD, FindBugs, Checkstyle, Lint integration
It’s important to have these checks integrated at the start of a project. Just as it is important to have some kind of continuous integration in place as early as possible (this project uses Travis CI). This effort will not be wasted and will result in quicker iterations and source code that’s significantly less buggy. This will make your users and product owners happy and you can sleep better too.
Recommended Reading
- Source code for this article: https://github.com/jshvarts/ConductorMVP
- Conductor implementation examples from the creators of the library
- Context Podcast Ep 13: Conductor with Eric Kuck
- Clean Architecture sample implementation by Fernando Cejas
- Coordinators: solving a problem you didn’t even know you had by Gabor Varadi
Thanks to Alex Hart.
Visit my Android blog to read about Jetpack Compose and other Android topics