Clean Android Code articles series:

We already have one example of UI in the project: MainActivity that has a NavigationView on it, let’s add some more.

Main part of the display will be made by a Fragment, that will have MapView on it. That fragment will be displayed by MainActivity — it’s presenter will receive a showInitialScreen event from NavigationDrawerViewExtension.

Navigation

It’s a good idea to keep all navigation-related code in the same place — it’s easier to maintain that way and keeps responsibilities separated.

I usually call my class that is responsible for navigation a UINavigator:

@ActivityScope

class UINavigator @Inject constructor(

private val fragmentManager: FragmentManager,

private val fragmentBackstackChangeListener: FragmentBackStackChangeListener

) {



companion object {

const val CURRENT_FRAGMENT_TAG = "CURRENT_FRAGMENT_TAG"

}



init {

fragmentBackstackChangeListener.onFragmentPopped = { parentFragment ->

parentFragment.onResume()

}

fragmentBackstackChangeListener.onFragmentPushed = { parentFragment ->

parentFragment.onPause()

}

fragmentManager.addOnBackStackChangedListener(fragmentBackstackChangeListener)

}



fun showGoogleMapsScreen() {

showFragment(GoogleMapsFragment.create())

}





private fun showFragment(fragment: Fragment, addToBackStack: Boolean = true) {

clearBackStack()

val transaction = fragmentManager

.beginTransaction()

.replace(R.id.fl_content, fragment, UINavigator.Companion.CURRENT_FRAGMENT_TAG)



if (addToBackStack)

transaction.addToBackStack(UINavigator.Companion.CURRENT_FRAGMENT_TAG)



transaction.commit()

}



private fun clearBackStack() {

if (fragmentManager.backStackEntryCount > 0) {

(1..fragmentManager.backStackEntryCount).forEach {

fragmentManager.popBackStack()

}

}

}

}

As you can see, UINavigator’s job is to know how to show and hide fragments and how to manage back stack. When you would add more activities to the project, UINavigator will also take the job of starting activities by using Intents, and a job of creating intents will be, again, delegated to another class — IntentProvider. That way navigation can be tested.

Another nice addition I like to add to my UINavigator is a custom FragmentBackStackChangeListener, that would notify us when stack is changing, so we could react. For example, when I have one fragment on top of another fragment, I prefer to call onPause on the fragment beneath, and onResume when top fragment is hidden.

Showing the screen

Now, we call our UINavigator in MainPresenter

@Inject lateinit var uiNavigator: UINavigator //... override fun showInitialScreen() {

uiNavigator.showGoogleMapsScreen()

}

This would display our fragment with a map. There’s only one problem — map is not showing up at the moment. That is because we need to pass some lifecycle event to the map object.

If you are not following code on Github and just build similar project yourself — don’t forget to add Google Play Services to the project, have them on testing device, add Google API key from Google Api Console and add necessary permissions in AndroidManifest

Let’s make a GoogleMapViewExtension, that will be responsible for the map UI and lifecycle

interface GoogleMapViewExtensionDelegate : EventsDelegate {

fun onMapReady()

}



@ActivityScope

class GoogleMapViewExtension @Inject constructor() : EventsDelegatingViewExtension<GoogleMapViewExtensionDelegate>, GoogleMapViewExtensionContract, OnMapReadyCallback {

override var eventsDelegate: GoogleMapViewExtensionDelegate? = null



private lateinit var map: MapView

private var googleMap: GoogleMap? = null



fun setViews(map: MapView) {

this.map = map

}



override fun onCreate(savedInstanceState: Bundle?) {

try {

map.onCreate(savedInstanceState)

} catch (t: Throwable) {

t.printStackTrace()

}

map.getMapAsync(this)

}





override fun onMapReady(googleMap: GoogleMap?) {

this.googleMap = googleMap



googleMap?.let { map ->

eventsDelegate?.onMapReady()

}

}



override fun onPause() {

map.onPause()

}



override fun onResume() {

map.onResume()

}



override fun onDestroy() {

map.onDestroy()

}



}

All it is doing at the moment — passing lifecycle events to the MapView, and notifies interested parties when map is ready.

Hint for Andoroid Studio: to go between files faster use a Cmd+Shift+O shortcut and type class name

Let’s add some logic to the screen by adding a GoogleMapPresenter, that would ask FlickrAPI for Photos and show them on the map. To simplify things a little, let’s say that user would long-click on a map to pick a location. When that happens, we would search for photos, add some markers on the map and navigate user to clicked position.

@ActivityScope

class GoogleMapsPresenter @Inject constructor(

private val flickrRepo: FlickrRepo,

private val schedulers: Schedulers

) : BasePresenter<GoogleMapsScreenContract>(), GoogleMapViewExtensionDelegate {



lateinit var googleMapViewExtension: GoogleMapViewExtensionContract



override fun refreshWithCoordinates(latitude: Double, longitude: Double) {

requestPhotos(latitude, longitude)

}



private fun requestPhotos(latitude: Double, longitude: Double) {

val testRadius = 10

flickrRepo.searchPhotos(latitude, longitude, testRadius)

.subscribeOn(schedulers.io)

.observeOn(schedulers.mainThread)

.subscribe({

googleMapViewExtension.addPhotoMarkers(it)

}, {

it.printStackTrace()

}, {

googleMapViewExtension.navigateTo(latitude, longitude)

})

}

}

A few important things to note:

we wrap RxSchedulers and inject them as a dependency — this will let us provide a MockRxSchedulers dependency in tests and ensure immediate Rx code execution

We provide 3 callbacks to our subscription: onNext, onError and onCompleted. It is very important not to forget about onError, because if you do you would get very little indication about something wrong happening during a network call. I will give example of better error handling in further parts of this article series.

First, let’s make sure that existing tests pass, then the let’s do a clean build and check what is going on with our code coverage.

./gradlew clean testStageDebugUnitTestCoverageDiff (note Diff in the end)

As I have mentioned before, this task would show you code coverage on classes that have been changed comparing to develop branch. The output report can be found in <project_root>/app/build/reports/jacoco

Jacoco Difference Coverage Report

Not so nice. Only 25% of classes that were added or modified are covered with tests.

Now, unfortunately, we can’t test everything, and some classes does not make sense to test. Let’s remove untestable classes from coverage: RxSchedulers, WebServicesConfig, MapUtils.

Hint: To create a new test class open your existing class, select class name and press Cmd+Shift+T, then select Create new test… It will generate a test name and ask where you want to put it — select ‘test’ flavor, not ‘androidTest’

Then we add tests for the rest: UINavigatorTest, MainPresenterTest, GoogleMapsPresenterTest, GoogleMapViewExtensionTest, FragmentBackStackChangeListenerTest

And make another check on coverage:

Fix Jacoco Coverage

Now, this looks much better. We are not 100% covered mostly because of property getters and setters. Hopefully, that would resolve some time soon with improved Jacoco filtering.

Now we have some simple network requests, entities we can use to pass data to UI layer, a simple UI that displays a map and puts markers on it. And good thing — almost everything is covered by unit-tests, so it is safe to refactor this code.

Hope this a bit more complex example shows how this architectural approach helps writing clean and understandable code.

All relevant changes can be found in this feature branch

Clean Android Code articles series: