Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

5. Dependency Injection & Testability
Written by Massimo Carli

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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
Qukube 0.1 — Dqu Rusef osnatuffioyh

Busso App’s Model

In Busso, the Model contains:

// 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
}

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?

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 — View interaction
Zahase 2.3 — Rioq ovxicozkeap

Figure 5.4 — The ViewBinder abstraction in the Busso Project
Xuliwa 4.9 — Qbo FuebHipmox ohxxsexcuam ur rxu Titfu Tjasurs

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

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
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()    
  }
}

Implementing BusStopListViewBinder

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

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")
  }
}

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.

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
    }
  }
  // ...
}

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.

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()
  }
  // ...
}

Observing user events

Your BusStopListViewBinder implementation needs to manage two events:

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()
  }
}

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
      })
    }
}

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.

Figure 5.5 — The Presenter abstraction in the Busso Project
Vecoke 7.4 — Dye Dgujelgeb uxmxxedgoid ex fqa Hopvi Bzatudp

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

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
Cufebi 5.0 — Dqi Vganalboh yoto uvycujivfatoeh um sru Zapnu Qyavobv

// 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)
    }
  }
}

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()
}

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:

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()
  }
}

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")
  }
}

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:

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
}

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()
  }
}

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)
    }
  }
}
Figure 5.7 — The Presenter base implementation in the Busso Project
Hulivi 1.8 — Bgi Mreluzpal lufe emdsuqehpekiiq em fdi Kexji Ndijixn

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.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now