Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

5. Dependency Injection & Testability
Written by Massimo Carli

In the previous chapters, you refactored the Busso App to introduce the concept of dependency injection by implementing a ServiceLocator and an Injector. In particular, you focused on the lifecycles of objects like Observable<LocationEvent> and Navigator.

This has simplified the code a bit, but there’s still a lot of work to do. Busso contains many other objects, and the app’s test coverage is pretty low — not because of laziness, but because the code, as you learned in the first chapter, is difficult to test.

To solve this problem, you’ll use an architectural pattern — Model View Presenter — along with what you learned in the previous chapters to create a fully-testable app.

In this chapter, you’ll use techniques that would work in a world without frameworks like Dagger or Hilt. Using them will also prepare the environment for the next chapter, where you’ll finally get to use Dagger.

Note: In this chapter, you’ll prepare Busso for Dagger and, later, Hilt. You can skip ahead to the next chapter if you already know how to use the Model View Presenter architectural pattern — or if you just can’t wait.

Model View Presenter

Maintainability, testability and making changes easy to apply are some of the main reasons to use an architectural pattern. Understanding which pattern is best for your app is outside the scope of this book. For Busso, you’ll use Model View Presenter (MVP).

Note: To learn all about architectural patterns in Android, read our book, Advanced Android App Architecture.

As the name implies, MVP is a pattern that defines the following main components:

  • Model
  • View
  • Presenter

A pattern gives you some idea about the solution to a specific problem. Different projects implement patterns in different ways. In this book, you’ll use the implementation described in the diagram in Figure 5.1:

Figure 5.1 — The Model View Presenter Architectural Pattern
Figure 5.1 — The Model View Presenter Architectural Pattern

Before you move on, take a quick look at the responsibilities of each component and how you use them to define the main abstractions in code.

Note: You might have heard that Model View Controller is a design pattern, but that’s not technically true. Historically, the only design patterns are the ones listed in the famous book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, also known as “The Gang Of Four”.

Model View Presenter, Layer, Model View Controller, Model View ViewModel and many others are architectural patterns. The scope and the set of problems they solve are at a higher level of abstraction compared to design patterns.

Next, you’ll take a closer look at each of the components that compose MVP.

Model

The Model is the data layer — the module responsible for handling the business logic and communication with the network or database layers. In Figure 5.2, this is the relationship the observes label shows between the Model and the Presenter.

Figure 5.2 — The Model interactions
Figure 5.2 — The Model interactions

It might be a little confusing to see that the arrow points from the Presenter to the Model. That’s because if A observes B it means that the data goes from B to A. Consider that in real life, if a person is listening to the radio, it means that the sound is going from the radio to the person.

The Model state changes in response to external events or events from the user. The updates label shows that relationship.

But what is the Model in Busso?

Busso App’s Model

In Busso, the Model contains:

  1. BussoEndpoint implementation, which accesses the network.
  2. Observable<LocationEvent>, an Observable that passes updates about the current location.

Busso’s starter project contains an implementation to manage the Model. Open ServiceLocatorImpl.kt in the di.locators package of the app module and you’ll see the following code:

// 1
const val BUSSO_ENDPOINT = "BussoEndpoint"
const val LOCATION_OBSERVABLE = "LocationObservable"
const val ACTIVITY_LOCATOR_FACTORY = "ActivityLocatorFactory"

class ServiceLocatorImpl(
  val context: Context
) : ServiceLocator {

  private val locationManager =
    context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  private val geoLocationPermissionChecker = GeoLocationPermissionCheckerImpl(context)
  private val locationObservable =
    provideRxLocationObservable(locationManager, geoLocationPermissionChecker)
  private val bussoEndpoint = provideBussoEndPoint(context)

  @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
  override fun <A : Any> lookUp(name: String): A = when (name) {
    // 2
    LOCATION_OBSERVABLE -> locationObservable
    // 3
    BUSSO_ENDPOINT -> bussoEndpoint
    ACTIVITY_LOCATOR_FACTORY -> activityServiceLocatorFactory(this)
    else -> throw IllegalArgumentException("No component lookup for the key: $name")
  } as A
}

Here, you can see that you:

  1. Have the constants for the name you’ll use to look up the BussoEndpoint and the Observable<LocationEvent>.
  2. If you have LOCATION_OBSERVABLE, you return Observable<LocationEvent>.
  3. In the BUSSO_ENDPOINT case, you return BussoEndpoint.

It’s important to remember that these objects have the same lifecycle as the app.

Testing Busso’s Model

Thinking about the Model components this way makes the test implementation easier. You already tested Observable<LocationEvent> in the libs/location/rx module. But what about the test for BussoEndpoint?

As you learned in Chapter 2, “Meet the Busso App”, the BussoEndpoint implementation uses Retrofit, which is a fully-tested framework by Square. Because of that, you don’t need to do anything now.

Note: The interesting part about the test for BussoEndpoint is the implementation of a mock for it, as you’ll see later in this chapter.

At this point, you should have a better understanding of the Model. Next, you’ll take a deeper look at the View component.

View & ViewBinder

The View component is the UI Layer. It has a bidirectional interaction with the Presenter. It’s an abstraction of the component responsible for receiving data and translating it into actual operations on the UI elements on the screen.

Figure 5.3 shows the relationship the View has with the Presenter, under the updates label.

Figure 5.3 — View interaction
Figure 5.3 — View interaction

The View has the fundamental responsibility of handling how the user events affect the UI elements, translating them to actions in the Presenter. In Figure 5.3, this is the relationship with the label, observes.

In some Model View Presenter implementations, the Presenter interacts with the View through the ViewBinder abstraction. Using a ViewBinder has two main advantages. It:

  1. Decouples the Presenter from the actual View implementation, which is usually an Activity or a Fragment.
  2. Avoids using the name View, which is also the name of one of the most important classes in Android. The View class is the base class for all the UI Android widgets.

For type safety, it’s useful to define an abstraction for the ViewBinder. In Figure 5.4, you can see the ViewBinder interface from libs/mvp:

Figure 5.4 — The ViewBinder abstraction in the Busso Project
Figure 5.4 — The ViewBinder abstraction in the Busso Project

The code is very simple:

// 1
interface ViewBinder<V> {
  // 2
  fun init(rootView: V)
}

ViewBinder is an interface with two important things to note:

  1. It’s a generic interface in the generic type variable V. This represents the type for the actual View or for another object that lets the ViewBinder implementation access all the UI components. Using generics allows you to avoid any dependencies with the Android framework.
  2. It defines init(), accepting a parameter of type V. That’s because most of the ViewBinder implementations need an entry point where they get the reference to the actual UI components of the screen they represent.

How do you implement the ViewBinder interface? Busso gives you a very good opportunity to find out.

Using ViewBinder for the BusStopFragment

Open BusStopFragment.kt and look at the code. Keeping the View responsibility in Figure 5.3 in mind, find the place in the code where you:

  1. Create the UI components or get references to the existing ones.
  2. Use the UI components to display some information.
  3. Observe user events.

Note: For this chapter, you’ll need to read the code of the existing Busso project directly in Android Studio. Copying all the code into the chapter would take too much space.

Looking at the structure of BusStopFragment, note that:

  • onCreateView() is where you inflate the layout for Fragment and prepare the UI to display the BusStop information.
  • In useLocation(), you display the BusStop data by invoking submitList() on Adapter.
  • You have some operations to manage errors, like handleBusStopError() and displayLocationNotAvailable().
  • For user events, you need to manage the selection of a BusStop in the list to navigate to the arrivals information.

Now, you have all the information you need to define the specific ViewBinder interface for the BusStopFragment.

Create a new file named BusStopListViewBinder.kt in the ui.view.busstop package and give it the following code:

// 1
interface BusStopListViewBinder : ViewBinder<View> {
  // 2
  fun displayBusStopList(busStopList: List<BusStopViewModel>)
  // 3
  fun displayErrorMessage(msg: String)
  // 4
  interface BusStopItemSelectedListener {
    // 5
    fun onBusStopSelected(busStopViewModel: BusStopViewModel)
    // 6
    fun retry()    
  }
}

This is the translation, in code, of the previous bullet points. In particular:

  1. BusStopListViewBinder is an interface that extends the ViewBinder using View (the Android one) as the actual type for the generic type variable V. Here, you inherit the definition for init with a View as the parameter.
  2. You define the displayBusStopList() operation you’ll invoke to display the BusStop data onscreen.
  3. You invoke displayErrorMessage() to display alerts in case of errors or warnings.
  4. To monitor the events the user can generate from the UI, you define the BusStopItemSelectedListener. You implement this interface to observe the BusStop a user selects.
  5. You use onBusStopSelected() to receive the information about the selected BusStop.
  6. You also provide retry(), which lets the user attempt to fetch the BusStop information again if an error occurs.

For your next step, you’ll create the BusStopListViewBinder implementation — which will make everything clearer.

Implementing BusStopListViewBinder

Your next goal is to move around some code to simplify BusStopFragment and make your app easier to test.

Create a new file named BusStopListViewBinderImpl.kt in the ui.view.busstop package and enter the following code:

class BusStopListViewBinderImpl : BusStopListViewBinder {
  override fun init(rootView: View) {
    TODO("Not yet implemented")
  }

  override fun displayBusStopList(busStopList: List<BusStopViewModel>) {
    TODO("Not yet implemented")
  }

  override fun displayErrorMessage(msg: String) {
    TODO("Not yet implemented")
  }
}

This is a skeleton for a class that implements the BusStopListViewBinder interface, allowing you to start addressing the responsibilities listed above.

Creating the UI components

init()’s implementation is very simple. All it needs to do is to create the UI for the list of BusStops. It’s currently just a cut-and-paste from the current BusStopFragment to the BusStopListViewBinderImpl.

Replace the existing init() implementation with the following code:

class BusStopListViewBinderImpl : BusStopListViewBinder {
  // 1
  private lateinit var busStopRecyclerView: RecyclerView
  private lateinit var busStopAdapter: BusStopListAdapter
  // 2
  override fun init(rootView: View) {
    busStopRecyclerView = rootView.findViewById(R.id.busstop_recyclerview)
    busStopAdapter = BusStopListAdapter()
    initRecyclerView(busStopRecyclerView)
  }
  // 3
  private fun initRecyclerView(busStopRecyclerView: RecyclerView) {
    busStopRecyclerView.apply {
      val viewManager = LinearLayoutManager(busStopRecyclerView.context)
      layoutManager = viewManager
      adapter = busStopAdapter
    }
  }
  // ...
}

In this code, you simply:

  1. Define the private properties for the RecyclerView and the BusStopListAdapter. You’ll initialize these in the init() function implementation.
  2. Implement init(), saving the reference to the RecyclerView in the private busStopRecyclerView property and creating the BusStopListAdapter. Here, you also initialize the RecyclerView, moving the code for the private initRecyclerView() function from the BusStopFragment.

Note: Throughout the process of switching to the Model View Presenter architectural pattern, you’ll break Busso repeatedly. Don’t worry, everything will be fine in the end.

The next step is to implement the function that updates the UI components.

Displaying information in the UI components

In the BusStopListViewBinder interface, you now need to do two things: Implement the operation that displays the list of BusStops onscreen and show an error message.

Start by replacing the implementation of the displayBusStopList() and displayErrorMessage() operations with the following code:

class BusStopListViewBinderImpl : BusStopListViewBinder {
  // ...
  override fun displayBusStopList(busStopList: List<BusStopViewModel>) {
    // 1
    busStopAdapter.submitList(busStopList)
  }

  override fun displayErrorMessage(msg: String) {
    // 2
    Snackbar.make(
      busStopRecyclerView,
      msg,
      Snackbar.LENGTH_LONG
    ).show()
  }
  // ...
}

The code above is very simple. It:

  1. Invokes the submitList() on the BusStopAdapter, passing the parameter you received in displayBusStopList().
  2. Displays a snackbar with the error message you received as its parameter.

This is quite straightforward. But you still need to handle the events.

Observing user events

Your BusStopListViewBinder implementation needs to manage two events:

  1. The user selects a BusStop from the list.
  2. A retry() option, if something goes wrong.

To do this, you need to apply the following changes to the existing code for BusStopListViewBinderImpl, leaving the rest as it is:

class BusStopListViewBinderImpl(
  // 1
  private val busStopItemSelectedListener: BusStopListViewBinder.BusStopItemSelectedListener? = null
) : BusStopListViewBinder {

  private lateinit var busStopRecyclerView: RecyclerView
  private lateinit var busStopAdapter: BusStopListAdapter

  override fun init(rootView: View) {
    busStopRecyclerView = rootView.findViewById(R.id.busstop_recyclerview)
    // 2
    busStopAdapter = BusStopListAdapter(object : OnItemSelectedListener<BusStopViewModel> {
      override fun invoke(position: Int, selectedItem: BusStopViewModel) {
        busStopItemSelectedListener?.onBusStopSelected(selectedItem)
      }
    })
    initRecyclerView(busStopRecyclerView)
  }

  // ...

  override fun displayErrorMessage(msg: String) {
    Snackbar.make(
      busStopRecyclerView,
      msg,
      Snackbar.LENGTH_LONG
      // 3
    ).setAction(R.string.message_retry) {
      busStopItemSelectedListener?.retry()
    }.show()
  }
}

As you can see, you now:

  1. Define an optional primary constructor parameter of type BusStopListViewBinder.BusStopItemSelectedListener.
  2. Pass an implementation of the OnItemSelectedListener interface as a parameter to the existing BusStopListAdapter. Here, you just invoke the callback on the BusStopListViewBinder.BusStopItemSelectedListener.
  3. Add an action to the Snackbar that displays the error message. When the user selects the action, you invoke the retry() callback operation on the BusStopListViewBinder.BusStopItemSelectedListener.

Great job! You’ve just made a big improvement by implementing a ViewBinder for the BusStopFragment. You did this for a reason: testing! Next, you’ll put that test into place.

Testing BusStopListViewBinderImpl

The BusStopListViewBinderImpl you just implemented isn’t difficult to test. Create the test class with Android Studio, just as you learned in Chapter 2, “Meet the Busso App”, and add the following code:

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class BusStopListViewBinderImplTest {

  private lateinit var busStopListViewBinder: BusStopListViewBinder
  private lateinit var fakeBusStopItemSelectedListener: FakeBusStopItemSelectedListener
  private lateinit var activityController: ActivityController<Activity>
  private lateinit var testData: List<BusStopViewModel>

  @Before
  fun setUp() {
    activityController = Robolectric.buildActivity(
      Activity::class.java
    )
    testData = createTestData()
    fakeBusStopItemSelectedListener = FakeBusStopItemSelectedListener()
    busStopListViewBinder = BusStopListViewBinderImpl(fakeBusStopItemSelectedListener)
  }

  // 1
  @Test
  fun displayBusStopList_whenInvoked_adapterContainsData() {
    val rootView = createLayoutForTest(activityController.get())
    with(busStopListViewBinder) {
      init(rootView)
      displayBusStopList(testData)
    }
    val adapter = rootView.findViewById<RecyclerView>(R.id.busstop_recyclerview).adapter!!
    assertEquals(3, adapter.itemCount)
  }

  // 2
  @Test
  fun busStopItemSelectedListener_whenBusStopSelected_onBusStopSelectedIsInvoked() {
    val testData = createTestData()
    val activity = activityController.get()
    val rootView = createLayoutForTest(activity)
    activity.setContentView(rootView)
    activityController.create().start().visible();
    with(busStopListViewBinder) {
      init(rootView)
      displayBusStopList(testData)
    }
    rootView.findViewById<RecyclerView>(R.id.busstop_recyclerview).getChildAt(2).performClick()
    assertEquals(testData[2], fakeBusStopItemSelectedListener.onBusStopSelectedInvokedWith)
  }

  private class FakeBusStopItemSelectedListener :
    BusStopListViewBinder.BusStopItemSelectedListener {

    var onBusStopSelectedInvokedWith: BusStopViewModel? = null
    var retryInvoked = false

    override fun onBusStopSelected(busStopViewModel: BusStopViewModel) {
      onBusStopSelectedInvokedWith = busStopViewModel
    }

    override fun retry() {
      retryInvoked = true
    }
  }

  private fun createTestData() = listOf(
    createBusStopViewModelForTest("1"),
    createBusStopViewModelForTest("2"),
    createBusStopViewModelForTest("3"),
  )

  private fun createBusStopViewModelForTest(id: String) = BusStopViewModel(
    "stopId $id",
    "stopName $id",
    "stopDirection $id",
    "stopIndicator $id",
    "stopDistance $id"
  )

  private fun createLayoutForTest(context: Context) = LinearLayout(context)
    .apply {
      addView(RecyclerView(context).apply {
        id = R.id.busstop_recyclerview
      })
    }
}

Aside from a lot of scaffolding, this class allows you to test that when:

  1. You invoke the displayBusStopList(), the app displays the data you pass as a parameter in a RecyclerView.
  2. The user selects an item, it calls the callback function onBusStopSelected() with the selected BusStop.

These tests use Roboletric, which is outside the scope of this book, but it’s good to prove that BusStopListViewBinderImpl contains code you can simply test in isolation.

Note: Robolectric (http://robolectric.org/) is a testing framework that allows you to test Android classes without the actual Android environment. This allows you to run tests more quickly, saving a lot of time.

At this point, Busso has a Model and a ViewBinder but you still need to connect all the dots. To do this you need a Presenter — a kind of mediator between the Model and the View. You’ll learn about Presenters next.

Presenter

As a mediator, the Presenter has two jobs. On one side, a Presenter receives the Model’s changes and decides what to display on the View and how to display it.

On the other side, the Presenter receives user events from the View and decides how to change the Model accordingly.

You can abstract the Presenter in different ways. Open Presenter.kt into the libs/mvp module, as shown in Figure 5.5.

Figure 5.5 — The Presenter abstraction in the Busso Project
Figure 5.5 — The Presenter abstraction in the Busso Project

Now, look at the following code:

// 1
interface Presenter<V, VB : ViewBinder<V>> {
  // 2
  fun bind(viewBinder: VB)
  // 3
  fun unbind()
}

The interface is simple, but it has some important things to note:

  1. It’s a generic interface in the generic type variables V and VB. V is related to the View and VB to the ViewBinder, which has to be related to the same type V.

  2. A Presenter is usually bound to the lifecycle of an Android standard component related to the View it manages. bind() is what binds the ViewBinder implementation. If you’re familiar with RxJava, this is where you’d usually subscribe to an Observable to receive the updates.

  3. unbind() is the symmetric function you invoke to unbind the ViewBinder from the Presenter. You also have the opportunity to release some resources here. In RxJava, this would be where you’d dispose of the subscriptions to some observables.

All the Presenter implementations have something in common so it’s handy to have a simple base implementation. You’ll cover that next.

Using a base Presenter implementation

Binding and unbinding the ViewBinder from the Presenter is very common. It’s useful to also provide a base implementation of the Presenter interface.

Figure 5.6 — The Presenter base implementation in the Busso Project
Figure 5.6 — The Presenter base implementation in the Busso Project

As shown in Figure 5.6, find BasePresenter.kt in the libs/mvp module. It’s in a impl subpackage with the following code:

// 1
abstract class BasePresenter<V, VB : ViewBinder<V>> : Presenter<V, VB> {
  // 2
  private var viewBinder: VB? = null
  // 3
  @CallSuper
  override fun bind(viewBinder: VB) {
    this.viewBinder = viewBinder
  }
  // 4
  @CallSuper
  override fun unbind() {
    viewBinder = null
  }
  // 5
  protected fun useViewBinder(consumer: VB.() -> Unit) {
    viewBinder?.run {
      consumer.invoke(this)
    }
  }
}

In this code, you:

  1. Define the BasePresenter abstract class, which implements the Presenter interface using the same constraints for the generic-type parameters.
  2. Create the private property, viewBinder, which references the ViewBinder implementation.
  3. Implement the bind() operation, saving the reference to the ViewBinder, which you receive as a parameter, to the private viewBinder property. @CallSuper forces the realizations of the BasePresenter to call the same operation on super when overriding the bind() operation. This makes the initialization of the viewBinder property safe.
  4. Implement unbind(), resetting viewBinder to null. unbind() also uses the @CallSuper annotation.
  5. Create useViewBinder(), which is a Kotlin way of accessing the ViewBinder property through a function, as you’ll see in the next paragraph.

Now, you have everything you need to implement the Presenter for the BusStopFragment class.

The BusStopListPresenter interface

Setting up the Presenter for BusStopFragment is simple. Create a new file named BusStopListPresenter.kt in ui.view.bustop and enter the following code:

// 1
interface BusStopListPresenter : Presenter<View, BusStopListViewBinder>, BusStopListViewBinder.BusStopItemSelectedListener  {
  // 2
  fun start()
  fun stop()
}

In these few lines of code, note that:

  1. BusStopListPresenter is an interface extending the Presenter abstraction using the Android View and BusStopListViewBinder as actual values for the generic type variables V and VB.
  2. You define two functions, start() and stop(), which allow you to bind the BusStopListPresenter to the lifecycle of an Android component. In this case, you bind it to the BusStopFragment.

Implementing BusStopListPresenter is also very simple.

The BusStopListPresenter implementation

Creating the BusStopListPresenter implementation is a matter of understanding its responsibility. Looking at the existing code in BusStopFragment, this class needs to:

  1. Observe Observable<LocationEvent> and use the Location information to fetch the data from the server that uses BussoEndpoint.
  2. Display the BusStop list on the screen using the BusStopListViewBinder.
  3. React to the selection of a BusStop on the list and use the Navigator to get to the next screen with the list of arrival times.
  4. In case of error, use the BusStopListViewBinder to notify the user and manage the retry() option.
  5. Release all the resources when Fragment is no longer displayed.

From the previous list, you understand how BusStopListPresenter depends on the following components:

  • Navigator
  • Observable
  • BussoEndpoint
  • BusStopListViewBinder

This makes the code quite simple to understand.

Create a new file named BusStopListPresenterImpl.kt in the ui.view.bustop of BusStopListPresenter and enter the following code:

class BusStopListPresenterImpl(
  // 1
  private val navigator: Navigator,
  private val locationObservable: Observable<LocationEvent>,
  private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusStopListViewBinder>(), BusStopListPresenter {

  private val disposables = CompositeDisposable()

  // 2
  override fun start() {
    disposables.add(
      locationObservable
        .filter(::isLocationEvent)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(::handleLocationEvent, ::handleError)
    )
  }

  private fun handleLocationEvent(locationEvent: LocationEvent) {
    when (locationEvent) {
      is LocationNotAvailable -> useViewBinder {
        displayErrorMessage("Location Not Available")
      }
      is LocationData -> useLocation(locationEvent.location)
    }
  }

  private fun useLocation(location: GeoLocation) {
    disposables.add(
      bussoEndpoint
        .findBusStopByLocation(location.latitude, location.longitude, 500)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .map(::mapBusStop)
        .subscribe(::displayBusStopList, ::handleError)
    )
  }

  private fun displayBusStopList(busStopList: List<BusStopViewModel>) {
    useViewBinder {
      displayBusStopList(busStopList)
    }
  }

  private fun handleError(throwable: Throwable) {
    useViewBinder {
      displayErrorMessage("Error: ${throwable.localizedMessage}")
    }
  }

  // 3
  override fun stop() {
    disposables.clear()
  }

  private fun isLocationEvent(locationEvent: LocationEvent) =
    locationEvent !is LocationPermissionRequest && locationEvent !is LocationPermissionGranted

  override fun onBusStopSelected(busStopViewModel: BusStopViewModel) {
    navigator.navigateTo(
      FragmentFactoryDestination(
        fragmentFactory = { bundle ->
          BusArrivalFragment().apply {
            arguments = bundle
          }
        },
        anchorId = R.id.anchor_point,
        withBackStack = "BusArrival",
        bundle = bundleOf(
          BUS_STOP_ID to busStopViewModel.stopId
        )
      )
    )
  }

  override fun retry() {
    start()
  }
}

The main things to note here are:

  1. The dependencies for BusStopListPresenterImpl are not all parameters of its primary constructor. You’re using constructor injection to note that you’ll pass the BusStopListViewBinder later, using the bind() operation you inherit from BasePresenter.
  2. When the app invokes start(), you start observing the Observable <LocationEvent>, just as the BusStopFragment did in the starter project.
  3. stop() releases the resources by invoking clear() on the CompositeDisposable, which is now a property of BusStopListPresenterImpl.

The remaining code is mostly the same as what you previously had in BusStopFragment, except for the access to the BusStopListViewBinder, which now uses useViewBinder(). The big difference is that now, the code is much simpler to test. You’ll see that for yourself in the next step.

Testing BusStopPresenterImpl

Testing BusStopPresenterImpl is now much simpler. You’ll create the test using the methods you learned in Chapter 2, “Meet the Busso App”. To start, enter the following code:

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class BusStopListPresenterImplTest {

  lateinit var presenter: BusStopListPresenter
  lateinit var navigator: Navigator
  lateinit var locationObservable: PublishSubject<LocationEvent>
  lateinit var bussoEndpoint: BussoEndpoint
  lateinit var busStopListViewBinder: BusStopListViewBinder

  @Before
  fun setUp() {
    navigator = mock(Navigator::class.java)
    locationObservable = PublishSubject.create();
    bussoEndpoint = mock(BussoEndpoint::class.java)
    busStopListViewBinder = mock(BusStopListViewBinder::class.java)
    presenter = BusStopListPresenterImpl(
      navigator,
      locationObservable,
      bussoEndpoint,
    )
    presenter.bind(busStopListViewBinder)
  }

  @Test
  fun start_whenLocationNotAvailable_displayErrorMessageInvoked() {
    presenter.start()
    locationObservable.onNext(LocationNotAvailable("Provider"))
    verify(busStopListViewBinder).displayErrorMessage("Location Not Available")
  }
}

The test in this code allow you to verify that, when Observable<LocationEvent> emits a LocationNotAvailable event, BusStopListPresenterImpl sends a Location Not Available error message to the BusStopListViewBinder.

Note: Complete testing coverage for BusStopListPresenterImpl requires knowledge of RxJava and RxKotlin. You can learn more about them by reading the Reactive Programming with Kotlin book.

Note: Tests in this chapter use the Mockito Library, which is outside the scope of this book.

Congratulations! You’ve implemented a Model, a ViewBinder and a Presenter for BusStopFragment. You’re getting close to the end now.

Putting it all together

Now that you’ve implemented the Model, ViewBinder and Presenter for the BusStopFragment, you need to connect all the dots. Following what you’ve done in the previous chapters, you need to:

  1. Create and manage the instances of BusStopListPresenter and BusStopListViewBinder implementations into the ServiceLocator for the proper scope.
  2. Use BusStopListPresenter and BusStopListViewBinder in the BusStopFragment.
  3. Implement the Injector for BusStopFragment.

Extending the FragmentServiceLocator

You now have two more objects to manage. Open FragmentServiceLocator.kt from the di.locators package for the app module, then add the following code without changing the existing fragmentServiceLocatorFactory definition:

const val BUSSTOP_LIST_PRESENTER = "BusStopListPresenter"
const val BUSSTOP_LIST_VIEWBINDER = "BusStopListViewBinder"

// ...

class FragmentServiceLocator(
  val fragment: Fragment
) : ServiceLocator {

  var activityServiceLocator: ServiceLocator? = null
  var busStopListPresenter: BusStopListPresenter? = null
  var busStopListViewBinder: BusStopListViewBinder? = null

  @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
  override fun <A : Any> lookUp(name: String): A = when (name) {
    BUSSTOP_LIST_PRESENTER -> {
      // 1
      if (busStopListPresenter == null) {
        // 2
        val navigator: Navigator = activityServiceLocator!!.lookUp(NAVIGATOR)
        // 2
        val locationObservable: Observable<LocationEvent> = activityServiceLocator!!.lookUp(
          LOCATION_OBSERVABLE
        )
        // 2
        val bussoEndpoint: BussoEndpoint = activityServiceLocator!!.lookUp(BUSSO_ENDPOINT)
        busStopListPresenter = BusStopListPresenterImpl(
          navigator,
          locationObservable,
          bussoEndpoint
        )
      }
      busStopListPresenter
    }
    BUSSTOP_LIST_VIEWBINDER -> {
      // 1
      if (busStopListViewBinder == null) {
        // 2
        val busStopListPresenter: BusStopListPresenter = lookUp(BUSSTOP_LIST_PRESENTER)
        busStopListViewBinder = BusStopListViewBinderImpl(busStopListPresenter)
      }
      busStopListViewBinder
    }
    else -> activityServiceLocator?.lookUp<A>(name)
      ?: throw IllegalArgumentException("No component lookup for the key: $name")
  } as A
}

Important to note is:

  1. You create instances for the BusStopListPresenter and BusStopListViewBinder implementations in a lazy way and retain them with a scope bound to the Fragment lifecycle.
  2. You use the ServiceLocator to look up the dependencies for the objects you’re providing.

Now, it’s time to use the BusStopListPresenter and BusStopListViewBinder in the BusStopFragment

Injecting BusStopListPresenter and BusStopListViewBinder into the BusStopFragment

Open BusStopFragment.kt and replace the existing code with the following:

class BusStopFragment : Fragment() {
  // 1
  lateinit var busStopListViewBinder: BusStopListViewBinder
  lateinit var busStopListPresenter: BusStopListPresenter

  override fun onAttach(context: Context) {
    // 2
    BusStopFragmentInjector.inject(this)
    super.onAttach(context)
  }

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? = inflater.inflate(R.layout.fragment_busstop_layout, container, false).apply {
    // 3
    busStopListViewBinder.init(this)
  }


  override fun onStart() {
    super.onStart()
    // 4
    with(busStopListPresenter) {
      bind(busStopListViewBinder)
      start()
    }
  }

  override fun onStop() {
    // 5
    with(busStopListPresenter) {
      stop()
      unbind()
    }
    super.onStop()
  }
}

If you compare this to BusStopFragment’s previous code, there’s a great improvement. Now you:

  1. Define just the property for BusStopListViewBinder and BusStopListPresenter.
  2. Assign a value to the previous properties using BusStopFragmentInjector in onAttach().
  3. Invoke init() on the BusStopListViewBinder implementation in onCreateView().
  4. Bind BusStopListViewBinder to the BusStopListPresenter in onStart(). In the same method, you also invoke start() on the Presenter.
  5. Invoke stop() and then unbind() on onStop().

Great! Now there’s just one more step to take.

Extending BusStopFragmentInjector

The very last step is to implement BusStopFragmentInjector. Open BusStopFragmentInjector.kt and replace the existing code with the following:

object BusStopFragmentInjector : Injector<BusStopFragment> {
  override fun inject(target: BusStopFragment) {
    val parentActivity = target.context as AppCompatActivity
    val activityServiceLocator =
      parentActivity.lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
        .invoke(parentActivity)
    val fragmentServiceLocator =
      activityServiceLocator.lookUp<ServiceLocatorFactory<Fragment>>(FRAGMENT_LOCATOR_FACTORY)
        .invoke(target)
    with(target) {
      // HERE
      busStopListPresenter = fragmentServiceLocator.lookUp(BUSSTOP_LIST_PRESENTER)
      busStopListViewBinder = fragmentServiceLocator.lookUp(BUSSTOP_LIST_VIEWBINDER)
    }
  }
}

Here, you just use fragmentServiceLocator to look up the references to the BusStopListViewBinder and BusStopListPresenter implementations, assigning them to the corresponding BusStopFragment properties.

And that’s it! Build and run the Busso App. Everything should work, and you’ll see what’s shown in Figure 5.7:

Figure 5.7 — The Presenter base implementation in the Busso Project
Figure 5.7 — The Presenter base implementation in the Busso Project

Great job!

Key points

  • Using an architectural pattern like Model View Presenter is a fundamental step toward the creation of a professional app.
  • Design Patterns and Architectural Patterns address different problems in different contexts.
  • Model, View and Presenter allow the creation of classes that are easier to test.
  • The Model is the data layer.
  • The View is the UI Layer.
  • Using a ViewBinder allows you to decouple the presentation logic from the specific Android component.
  • The Presenter mediates between View and Model. It’s often bound to the lifecycle of an Android standard component.

Congratulations! In this chapter, you’ve achieved a lot by applying an architectural pattern, Model View Controller, to the Busso App. The code for BusStopFragment is much cleaner now and you have good testing coverage.

You’ve now written a lot of code using only the information about the dependencies between the different components of the Busso App. But… do you really need to write all this code? Since you only needed the information about dependencies, would it be possible to somehow provide the same information and generate all the code you need?

Welcome on board, you’re now ready to begin your journey to Dagger and Hilt!

Where to go from here?

If you want to learn more about Mockito, Roboelectric and testing in Android, read the Android Test-Driven Development by Tutorials book.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.