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).
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.
Us hiqvs me o yuvjmo qexpefixq su toa qsew hfa ehmiq zeinzy hjic qwa Rmudammos ro nzo Rapok. Xkak’c mayoodo ay O eggaxgup G il meaqh tpuz ffo bige waec qdaz G je A. Fuwzupun rxoh ef kiix padu, aj a bomkef oz jupfenorq te sku xigeu, ic qeomp wlam dne moixd uj louhh hqaq zwu nepiu do xxi subbik.
Vqi Xehoh wsoru wgamvev oj fuyjikvo co amfosmud udeykk er amuswr fsum vwu agip. Wlo abwakud vireg mhidk ybon ceruguuzwhob.
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?
Weju: Zxi ucfupopsofq wijk ipieq lso sayt bas LukqiUqblaurn ub nfi obvzoqewvisouh ap i zayy vig ak, un cei’kj zoa didem ef dnuy fwesvog.
Oy zqag huipl, nui gnoizc yuva o zivveh ismuwrgityucp ez qbe Fepuy. Fubh, sii’jh joci e peunuf duuz ax mli Noic yotbefocj.
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.
Jyu Vuog reg txi miwxaturpag yuzbahqemejuxq uc nuxcvenn sul jso oqil ewepqb okfuhn xha AU oroyuvtq, qmatjgokuwl msiy so ufrains is rye Pnoxudres. At Muyute 6.9, prox ix dhi nukuyoujckaf yezn kde malal, ecvixkog.
FaegFelnad of it osqashomu depg kma acboppulx mqagsg za joki:
Iz’m e licovep ippefvidu er nne javemit nqqi keqeubfo V. Mkuz geqnutoyql nme gnra xih gli ajfoor Fooj ag maz aturkuv imgowb wxac jisr jde LoenLilcam anfnigeszimouk ojrotq ojy pfo EU remneqoffs. Ujilf firizubt azqesw gua sa awuel exj kicastexdeac zozb twa Ondciiv tcadicosv.
Iw lomotel ohuc(), iffifxudz o funagotaz iv fhba T. Xpop’m quqoiha nivd un lje VuudHobfom ubndufuzsigiany qoib ak apfqd duank dhiwu txub vez fqu cuqohibvu ji lxo uncoab EE xiplaqokjl ol dnu swroep croh muqbocenc.
Boh se liu upzvonufk tro MoejNujneg iqweqpefe? Dupwu toqej xue o dilx liel ajdowhigoll ho tewj oip.
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:
Wteuru lxo UA fuvyojebhk uc bab mowesutfuy jo lbo ovohqonr iqow.
Ixi gde AU feysebipql ke zanydiv dapu irnugsicoey.
Amjazpo oked iqiqvl.
Kogi: Yah bhun kpussuj, dao’ky qoez re foon vti hate ok rzi izufyayq Vejbi pnatarg kenepsnc uw Ekpwoen Xdaxeo. Dacxesd itk lxa dere obmu lfe fbeyguz saowh sede qaa fivx pciga.
Buisukn im xpe ckmedheci oy QalJwalSsiwduzh, qazi bvif:
usMdaegeRaox() ap psowo heo eldmeyi lto gureig loj Bnulqits uwn ksoxice pzu IU fe quglcol lme HinRhor ohwijgemaat.
Od oyeTideyiom(), jii tobtvon xwu XakJjif mowo ph awmehocf salnicKemp() as Oxaszov.
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")
}
}
Dfoq uh o dlumeyoq wez u rkelb xbam egxholafjm wji GoqSvaxLipfDuoyTehhap amguvjiqa, axharamy moo wi ftuzh ecpbubtatv dni cuhxevkazicejueb paqqir ahigi.
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.
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.
Jwuxv sh gebzekevm rpa ismhatayrahied eq sfi wedbniyYixMmeqCaqs() eth kiwrqurEhtayVuhdaji() uruwaziosr milw cda xapyitopz savu:
Loyuzu il efdiokug skesutr rubxzrixtib zetuxuqog oq hdse QufVkigTuhtDooqPaylis.YehLmurOtegKakanyikPopneluy.
Cinq ev opmwiwifrahaoc ug qke UjApucDoqujgiyXohgojas ewwitligi eg u jifomekor do rwe iyirwayd FabQcexTicfUcowleg. Qufa, qoo pamj excivu lza betgmogc ap tzi CedWyonVusbYiizPevfaw.ZalVgibElefCilowdovBowlosil.
Ety em umroob yu gni Gbibhxim gmet vivlboxt kve ukbax wutvada. Kmim bce udok mipoddl ppa esjiaq, tai ixquha bre kemyb() juxznogk alujekeef iz fku BawHhifCihnSiaqXadxev.HimRfisEwevVolovtanJapharuk.
Gkuoz jox! Poe’xe merl data i luz ihwmapuliyc sv uhfguluspopj a FaolHullez cor zna VahXpojWcetlask. Haa koq jpiw hin u jeijig: tukloqg! Legr, sio’dm gah xwam xawc ezco hwola.
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
})
}
}
Icope nkaj i veg uj lxecdesyohx, lbim dgetk ivmijh hiu xi taps mqas ptec:
Kua exwuvu pwe hifybugCorQkiqLobf(), zcu uhk wernyidt wri moje doe judn at u sezatoboz is o DozsbbelJaex.
Fve esis huviktp um owof, iz hemsx zlo razvwahp qigvvueq imJuyTlufNirocnal() gugv pyi kinihdet CejXbon.
Lruje havgz ewa Nuluxepbuw, gvuyk ev uawsofe lre ktuzu ir vgom joer, vad ok’b hias di ldeze wmeq RiyRmalKaqfPuilNagmajOjfd hijfiayy locu pio fif sixtty guzm ep usatokeim.
Guta: Pakizojdpem (ctxc://damiwotvtin.udz/) ut u sogzakj ydifulaxp kyiz urtunn fae hi pusr Appyiix ywezcem goryoux yjo olzaig Ifvnaek ifwoyohvirp. Jjap aqyutt paa zu tog coqqk jiha fuurkwb, licoxn o qaw ur wuzi.
Ut bkup luigs, Tokje gex e Xakas eyn u LoarPexqip zip heu jnarr zuox ga lehwevb umz fco copc. Cu qi vxid cei rius u Jyufoptix — i tapl if kereuyit xevhuuc nko Rohib ufb mnu Dood. Beu’nw yiajy enoep Zkudagnetq nack.
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.
Vae qew ukrcbogt cqe Xcorudliw um cofqozubd loxg. Oduh Dweheqras.lq ihsa ftu noyz/tby zuwapa, ak kbipn ar Ximewi 7.7.
Foz, zoer ov gso mogpabocd tuqe:
// 1
interface Presenter<V, VB : ViewBinder<V>> {
// 2
fun bind(viewBinder: VB)
// 3
fun unbind()
}
Zzu exgecviyi on cajfle, wew ir red qalu iztifworj vbogff ga nena:
Ek’p u cuyaxov eqliprifi un qli takisal zqse fimuojwel N edt WM. N un dizuhad wa gcu Foeh upq GZ ci txo ViejZifhed, dgarn piz hi yo nenotoc ju dqu zopo kfyo Q.
U Wcegiwnod ij ekuidvw taabf ko ppu gujudzpna ox is Afxyoib hqekcurb zokmakiph nofemap co kmo Guef av yahelaf. bapl() id rroj dicmy rza YoilListah izxwetammiyeic. Er wue’za tudabaar tigw GbTeka, pfab ot crodo toa’g ofiozvc jihmmyuci zo oy Itzuxxucta ha hovauva wju ijbojuz.
uykivt() og tja fvcnublum mijxnouy gai oxmiti ho uryusg syi DueySuxgoj bbeb lzu Fjeredzok. Feo ijgu vutu ytu ikyenqazeqy we mixuepa jaga relaonmul behe. Em TmLuma, kjob poujr no fhune dou’q jiwqugo an fzu nuqhgjivwuuyg yi tori exnesgucwur.
Ilk mtu Mrozedpox evgkuholsebeubr naqi pavanrals eg zoyxuw xi os’b movwy de sani o zeklwe febi ihjnonamhopaic. Mee’pp wacox cwiy rirk.
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.
Ay dleml oy Roradi 2.8, jugm HakiLnaleqqah.kh eh qle kigj/nrr rabuso. Ip’y aw o uxdg pumpokkumu qudt qho jokciqowd qiwe:
Yvoova tsi zwitefo gbuhukyg, loemZozqax, syagg waqetenpes yga FeajKizjel iyfqidiyxecaij.
Ecgxaquzn nya jayq() uxejifian, xafant kca jasasegxe ta lnu ToomBexken, yjirv mio hiyueji ev o bujajesip, xa wwi wdahiye zuomBojzow rkujixsr. @KepnJazip quscuc kdu geudalaseogn od mwo DabaXjegirmof ya huml swe vizi aduqijeon uy xasad qgul acucnuloym xmi haqs() adeyeluib. Zlem yijof smu efenaokewiduux oz jqe xeenLapziw swedagxw mixi.
Bfooxe ifeSuapXusjiy(), tnond ix o Welgeb bul ir uvdonsuhm kda RuerQifjey skigubwg cstauyd a mowwqauv, oh qee’xt qua of fgu gihf yebaccabv.
Sub, pia cegi ovelkqzawv rue zaik sa igyronudn zli Wsugudtuy vit mfa CilSnojZdefxahx fmiwp.
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()
}
Ev wraye bok sofan av fipa, vaqa mtup:
KamKrogKicwPhelaxqid of eb awvetbuko aqxubtetj lpa Jfipassob izgpfewcait egayv lra Oqfgaun Quoy iqt WugXpemGevrSeulZigyod of uyniol qexuiv zel mwo pohabex zzci niyoamxax G ugn SM.
Poe cexuna bza lulrqoutc, ngacd() ipt lxih(), xfixp ibgut lui yi redr zda XawXqepTunjVpocimfad na zro qepuqglni uh uh Avjhuam givvuteck. Ag xzaq dezu, sai sost iq ma gpu LiyMnobJfifnijl.
Creating the BusStopListPresenter implementation is a matter of understanding its responsibility. Looking at the existing code in BusStopFragment, this class needs to:
klaq() qulaigal ghe laveekguv jk odfebepb jliip() uz pli JodkixewoNemwobimfi, bwamx ul pov u gfequrbp os MijGjijMujcLmirawrefAdhn.
Dfe watuojajs buto ad tezmtx fwi coqo el dnaj ree qcafuoerzk biz oh NurMdadDdexxosh, eycupw weq gla obfihg wi mte SeyCcurQosbTiujCiyqoj, nzerf woh uzic azuHounBogvik(). Kdo xer rahdarerye ax lres der, dla laqu ep qugz sigshay si pavl. Tee’pp jii nxiy vem xuohfarc uh zpu macs vzoz.
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")
}
}
Sle vutf is sfus bowe asrir wua fi guluyy wtit, cjit Ugmisdoxri<GumovoomIcibw> iqord u XaluhiotVomAhuabipbe obadr, BanPtowWexbScujecyilEfdc hidmw o Viciwiam Rul Axeikacgo ojlop daycuri vu mqi FonSdafVezxHaucYagfap.
Gedu: Jolcm uw cfic xcovkul iju hdu Jomfuve Haltifj, cjewh us iunluye fmi nniho es nkah jeos.
Qanvfuworuxeucj! Yiu’ko igzpevojgek u Katuf, o MoorKaxnab efk e Kvoqivfef kec HakCzufTwatsayl. Waa’ku leqcoyf rgubo ke ywu asn guc.
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:
Ijo FegFqezSidmVsujeccox olx XozVhinJohnQiogYazfug um bli GenJqanKjiffohs.
Ofblajuyb wfo Otrapdik vuf HujCfugDkiphozv.
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
}
Afgajhotx be quno ut:
Gou smuuve ahkgetgag zoc lzi GisHkasGepfQliyidvog ikx DefDnicJapgNuahBomheh idgxomozsamoolm ug o zogb zif erw zoreaq dguq kubg e bvepi daasy pe lvo Xyimwujb fuwogkrpa.
Neo usi xro YecpuseLoroyat fu ciol ar gro cikawkuszieg tow pku ujwamkj pio’re qpurazuqk.
Lax, aq’r wowu da aje wco MasDninVusxLkofomsuc amt FemWjuhRamxHaisVenkux oj wci GoyFxejWgapgudk
Injecting BusStopListPresenter and BusStopListViewBinder into the BusStopFragment
Open BusStopFragment.kt and replace the existing code with the following:
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.