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:
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.
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:
-
BussoEndpointimplementation, which accesses the network. -
Observable<LocationEvent>, anObservablethat 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:
- Have the constants for the name you’ll use to look up the
BussoEndpointand theObservable<LocationEvent>. - If you have
LOCATION_OBSERVABLE, you returnObservable<LocationEvent>. - In the
BUSSO_ENDPOINTcase, you returnBussoEndpoint.
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
BussoEndpointis 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.
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:
- Decouples the Presenter from the actual View implementation, which is usually an Activity or a Fragment.
- Avoids using the name View, which is also the name of one of the most important classes in Android. The
Viewclass 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:
The code is very simple:
// 1
interface ViewBinder<V> {
// 2
fun init(rootView: V)
}
ViewBinder is an interface with two important things to note:
- 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 theViewBinderimplementation access all the UI components. Using generics allows you to avoid any dependencies with the Android framework. - It defines
init(), accepting a parameter of typeV. That’s because most of theViewBinderimplementations 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:
- Create the UI components or get references to the existing ones.
- Use the UI components to display some information.
- 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 forFragmentand prepare the UI to display the BusStop information. - In
useLocation(), you display theBusStopdata by invokingsubmitList()onAdapter. - You have some operations to manage errors, like
handleBusStopError()anddisplayLocationNotAvailable(). - For user events, you need to manage the selection of a
BusStopin 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:
-
BusStopListViewBinderis an interface that extends theViewBinderusingView(the Android one) as the actual type for the generic type variableV. Here, you inherit the definition forinitwith aViewas the parameter. - You define the
displayBusStopList()operation you’ll invoke to display theBusStopdata onscreen. - You invoke
displayErrorMessage()to display alerts in case of errors or warnings. - To monitor the events the user can generate from the UI, you define the
BusStopItemSelectedListener. You implement this interface to observe theBusStopa user selects. - You use
onBusStopSelected()to receive the information about the selectedBusStop. - You also provide
retry(), which lets the user attempt to fetch theBusStopinformation 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:
- Define the private properties for the
RecyclerViewand theBusStopListAdapter. You’ll initialize these in theinit()function implementation. - Implement
init(), saving the reference to theRecyclerViewin the privatebusStopRecyclerViewproperty and creating theBusStopListAdapter. Here, you also initialize theRecyclerView, moving the code for the privateinitRecyclerView()function from theBusStopFragment.
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:
- Invokes the
submitList()on theBusStopAdapter, passing the parameter you received indisplayBusStopList(). - 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:
- The user selects a
BusStopfrom the list. - 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:
- Define an optional primary constructor parameter of type
BusStopListViewBinder.BusStopItemSelectedListener. - Pass an implementation of the
OnItemSelectedListenerinterface as a parameter to the existingBusStopListAdapter. Here, you just invoke the callback on theBusStopListViewBinder.BusStopItemSelectedListener. - Add an action to the
Snackbarthat displays the error message. When the user selects the action, you invoke theretry()callback operation on theBusStopListViewBinder.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:
- You invoke the
displayBusStopList(), the app displays the data you pass as a parameter in aRecyclerView. - The user selects an item, it calls the callback function
onBusStopSelected()with the selectedBusStop.
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.
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:
-
It’s a generic interface in the generic type variables
VandVB.Vis related to the View andVBto the ViewBinder, which has to be related to the same typeV. -
A
Presenteris usually bound to the lifecycle of an Android standard component related to the View it manages.bind()is what binds theViewBinderimplementation. If you’re familiar with RxJava, this is where you’d usually subscribe to anObservableto receive the updates. -
unbind()is the symmetric function you invoke to unbind theViewBinderfrom 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.
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:
- Define the
BasePresenterabstract class, which implements thePresenter interfaceusing the same constraints for the generic-type parameters. - Create the private property,
viewBinder, which references theViewBinderimplementation. - Implement the
bind()operation, saving the reference to theViewBinder, which you receive as a parameter, to the privateviewBinderproperty.@CallSuperforces the realizations of theBasePresenterto call the same operation onsuperwhen overriding thebind()operation. This makes the initialization of theviewBinderproperty safe. - Implement
unbind(), resettingviewBindertonull.unbind()also uses the@CallSuperannotation. - Create
useViewBinder(), which is a Kotlin way of accessing theViewBinderproperty 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:
-
BusStopListPresenteris an interface extending thePresenterabstraction using the AndroidViewandBusStopListViewBinderas actual values for the generic type variablesVandVB. - You define two functions,
start()andstop(), which allow you to bind theBusStopListPresenterto the lifecycle of an Android component. In this case, you bind it to theBusStopFragment.
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:
- Observe
Observable<LocationEvent>and use theLocationinformation to fetch the data from the server that usesBussoEndpoint. - Display the
BusStoplist on the screen using theBusStopListViewBinder. - React to the selection of a
BusStopon the list and use theNavigatorto get to the next screen with the list of arrival times. - In case of error, use the
BusStopListViewBinderto notify the user and manage theretry()option. - Release all the resources when
Fragmentis 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:
- The dependencies for
BusStopListPresenterImplare not all parameters of its primary constructor. You’re using constructor injection to note that you’ll pass theBusStopListViewBinderlater, using thebind()operation you inherit fromBasePresenter. - When the app invokes
start(), you start observing theObservable <LocationEvent>, just as theBusStopFragmentdid in the starter project. -
stop()releases the resources by invokingclear()on theCompositeDisposable, which is now a property ofBusStopListPresenterImpl.
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
BusStopListPresenterImplrequires 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:
- Create and manage the instances of
BusStopListPresenterandBusStopListViewBinderimplementations into theServiceLocatorfor the proper scope. - Use
BusStopListPresenterandBusStopListViewBinderin theBusStopFragment. - 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:
- You create instances for the
BusStopListPresenterandBusStopListViewBinderimplementations in a lazy way and retain them with a scope bound to theFragmentlifecycle. - You use the
ServiceLocatorto 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:
- Define just the property for
BusStopListViewBinderandBusStopListPresenter. - Assign a value to the previous properties using
BusStopFragmentInjectorinonAttach(). - Invoke
init()on theBusStopListViewBinderimplementation inonCreateView(). - Bind
BusStopListViewBinderto theBusStopListPresenterinonStart(). In the same method, you also invokestart()on the Presenter. - Invoke
stop()and thenunbind()ononStop().
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:
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.