Android VIPER Tutorial
In this tutorial, you’ll become familiar with the various layers of the VIPER architecture pattern and see how to keep your app modules clean and independent. By Pablo L. Sordo Martinez.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android VIPER Tutorial
30 mins
- Why are you (probably) reading this?
- When Android I met Architecture Patterns…
- VIPER at a glance
- Getting started
- App definition and description
- App modules and entities
- Main Module
- View
- Presenter
- Interactor
- Detail Module
- View
- Presenter
- Shaping the router
- Guided by a Cicerone
- App performance analysis
- Testing the snake
- Where To Go From Here?
Detail Module
The detail module implementation is similar to the previous one: you only need each class to extend from the proper parent and implement the right interfaces according to the module contract.
View
Update the DetailActivity
declaration to be:
class DetailActivity : BaseActivity(), DetailContract.View {
// ...
Add the following properties and companion object:
companion object {
val TAG = "DetailActivity"
}
private var presenter: DetailContract.Presenter? = null
private val toolbar: Toolbar by lazy { toolbar_toolbar_view }
private val tvId: TextView? by lazy { tv_joke_id_activity_detail }
private val tvJoke: TextView? by lazy { tv_joke_activity_detail }
Instantiate the presenter in onCreate()
:
presenter = DetailPresenter(this)
Override the view interface methods as follows:
override fun getToolbarInstance(): android.support.v7.widget.Toolbar? = toolbar
override fun showJokeData(id: String, joke: String) {
tvId?.text = id
tvJoke?.text = joke
}
override fun showInfoMessage(msg: String) {
toast(msg)
}
Configure the toolbar back button by overriding onOptionsItemSelected()
:
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return when (item?.itemId) {
android.R.id.home -> {
presenter?.backButtonClicked()
true
}
else -> false
}
}
The Router will bring arguments to this Activity from the previous module (remember listItemClicked(joke: Joke?)
in the Main Module presenter), and they are retrieved and passed to the presenter once the view is ready. Add the following lifecycle overrides:
override fun onResume() {
super.onResume()
// add back arrow to toolbar
supportActionBar?.let {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
// load invoking arguments
val argument = intent.getParcelableExtra<Joke>("data")
argument?.let { presenter?.onViewCreated(it) }
}
override fun onPause() {
super.onPause()
}
We’ll finish these overrides in the next main section.
Presenter
To complete the classes in this module, add the following class to the presenter package:
class DetailPresenter(private var view: DetailContract.View?) : DetailContract.Presenter {
override fun backButtonClicked() {
}
override fun onViewCreated(joke: Joke) {
view?.showJokeData(joke.id.toString(), joke.text)
}
override fun onDestroy() {
view = null
}
}
You can see that the backButtonClicked function is not defined yet, since it needs the module Router, whose implementation is pending.
Shaping the router
In some parts of this tutorial the term Router has appeared. However, you have never addressed its implementation, but why? The reason relates to how Android is designed. As mentioned before, the Router is the VIPER layer in charge of the navigation across the app views. In other words, the router is aware of every view existing in the app, and possesses the tools and resources to navigate from one view to another and vice-versa.
Keeping this in mind for each module, it would make sense to create the Presenter (the brain of the module), and then the rest of entities (View, Interactor, and Entity). Finally, the Router should wrap the Views up and somehow let the Presenter command the navigation.
This sounds perfectly reasonable, but things are a bit more complicated on Android. Think of what is the entry point of any Android application (corresponding to the the “main” method in other applications).
Yes, you are right, the entry point is that Activity which is marked with the appropriate intent-filter
in the Android Manifest. In fact, every module is always accessed through an Activity, i.e. a View. Once created, you can instantiate any class or entity from it.
Therefore, the way Android has been designed makes it much more difficult to implement VIPER. At the end of the day, you will need a startActivity() function call to move to a new screen. The question that follows would be: can this be done in effortless way for the developer?
It’s possible that the new Jetpack Navigation Controller will help in this regard. But Jetpack is still in alpha, so, in the meantime, we’ll turn to a different tool.
Guided by a Cicerone
Having reached this point, it is important that you get to know Cicerone. I came across this excellent library not long ago, when trying to improve a VIPER implementation on Android. Although it is not flawless, I believe it comprises a few tools and resources which help to keep things neat and clear. It helps you make VIPER layers as decoupled as possible.
In order to use this library, there are a few steps to accomplish:
- Add the library dependency to the application build.gradle file.
dependencies { // ... implementation 'ru.terrakok.cicerone:cicerone:2.1.0' // ... }
- Create your own Application instance in the app root package and enable Cicerone:
class BaseApplication : Application() { companion object { lateinit var INSTANCE: BaseApplication } init { INSTANCE = this } // Routing layer (VIPER) lateinit var cicerone: Cicerone<Router> override fun onCreate() { super.onCreate() INSTANCE = this this.initCicerone() } private fun BaseApplication.initCicerone() { this.cicerone = Cicerone.create() } }
-
Then in your Android Manifest, set the application name to your new class:
... <application android:name=".BaseApplication" ...
dependencies {
// ...
implementation 'ru.terrakok.cicerone:cicerone:2.1.0'
// ...
}
class BaseApplication : Application() {
companion object {
lateinit var INSTANCE: BaseApplication
}
init {
INSTANCE = this
}
// Routing layer (VIPER)
lateinit var cicerone: Cicerone<Router>
override fun onCreate() {
super.onCreate()
INSTANCE = this
this.initCicerone()
}
private fun BaseApplication.initCicerone() {
this.cicerone = Cicerone.create()
}
}
...
<application
android:name=".BaseApplication"
...
Now, we are ready to use Cicerone. You only need to make any View aware of this new partner.
The best option is to add this line to onResume():
BaseApplication.INSTANCE.cicerone.navigatorHolder.setNavigator(navigator)
and this other one to onPause()
BaseApplication.INSTANCE.cicerone.navigatorHolder.removeNavigator()
Go ahead and do so in MainActivity
and DetailActivity
now.
But, what is navigator
? Well, according to the documentation, it is a class property that defines exactly what happens when the Router is invoked.
Let's see an example for MainActivity. Add both a companion object and the navigator
property:
companion object {
val TAG: String = "MainActivity" // 1
}
private val navigator: Navigator? by lazy {
object : Navigator {
override fun applyCommand(command: Command) { // 2
if (command is Forward) {
forward(command)
}
}
private fun forward(command: Forward) { // 3
val data = (command.transitionData as Joke)
when (command.screenKey) {
DetailActivity.TAG -> startActivity(Intent(this@MainActivity, DetailActivity::class.java)
.putExtra("data", data as Parcelable)) // 4
else -> Log.e("Cicerone", "Unknown screen: " + command.screenKey)
}
}
}
}
Here's what's going on:
- The View needs a
TAG
as identifier. - By default, applyCommand() handles the navigation logic. In this case, only
Forward
commands are processed. -
forward is a custom function which undertakes navigation when
command.screenKey
matches. - At the end of the day, and due to Android design, you are going to need a startActivity somewhere, so that the navigation actually takes place.
So, you may be thinking this is just a wrapper that adds boilerplate to your code to do exactly what you used to do before. No doubt on the boilerplate, but now in any presenter, you can have a class member like
private val router: Router? by lazy { BaseApplication.INSTANCE.cicerone.router }
and use it easily. Go ahead and add it to both MainPresenter
and DetailPresenter
.
Do you remember listItemClicked(joke: Joke?)
in MainPresenter? Now you are in position to add this beautiful line:
router?.navigateTo(DetailActivity.TAG, joke)
It is time to implement the Router layer for every module, even for the Splash Module.
Try to do it yourself as an exercise, and check the final project if needed. As a hint, the navigator
in SplashActivity looks a lot like the one in MainActivity. And the navigator
in DetailActivity looks like:
private val navigator: Navigator? by lazy {
object : Navigator {
override fun applyCommand(command: Command) {
if (command is Back) {
back()
}
}
private fun back() {
finish()
}
}
}