Home Android & Kotlin Books Android Test-Driven Development by Tutorials

14
Hands-On Focused Refactoring Written by Lance Gleason

In the last chapter, you had a chance to:

  1. Get familiar with the Coding Companion app.
  2. Add tests around the search functionality.
  3. Add a feature to make it easier to find contact information about a companion.

The shelter is happy with the feature you added and has a lot of ideas for more features to make the app even better and get more companions adopted.

Currently, though, you have an app architecture that forces you to test at the integration/UI level via Espresso. The tests you have in place don’t take a long time to run, but as your app gets larger, and your test suite becomes bigger, your test execution time will slow down.

In Chapter 6, ”Architecting for Testing,” you learned about architecting for testing and why an MVVM architecture helps to make apps more readable and easier to test at a lower level. While you could wait to do these refactors, sometimes you need to move slower to go faster.

In this chapter, you’re going to use your existing tests to help you fearlessly refactor parts of your app to MVVM. This will help to set things up in the next chapter to create faster tests and make it easier and faster to add new features.

Getting started

To get started, open the final app from the previous chapter or open the starter app for this chapter. Then, open FindCompanionInstrumentedTest.kt located inside the androidTest source set.

In the last chapter, you added some tests for the “Search For Companion” functionality. You can find this test inside FindCompanionInstrumentedTest.kt having the name searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details.

This test does the following:

  1. It starts the app’s main activity, which takes the user to the Random Companion screen; this screen is backed by RandomCompanionFragment.

  1. Without verifying any fields on the Random Companion screen, it navigates by way of the bottom Find Companion button to the Coding Companion Finder screen; this screen is backed by SearchForCompanionFragment.

  1. Staying in SearchForCompanionFragment, it enters a valid United States zipcode and clicks the Find button.

  1. Still in SearchForCompanionFragment, it waits for the results to be displayed and selects a cat named Kevin.

  1. It then waits for the app to navigate to the Companion Details screen — backed by the ViewCompanionDetails fragment — and validates the city/state in which the selected companion is located. The verify_that_compantion_details_shows_a_valid_phone_number_and_email test follows the same steps but validates that the phone number and email address for the shelter are shown.

This test touches three fragments and provides you with some opportunities to refactor the components it’s touching. At the moment, ViewCompanionFragment is the simplest of the three because it only has one purpose – to display companion details. Therefore, you’ll start by refactoring this test.

Adding supplemental coverage before refactoring

You already have some testing around the “Search For Companion” functionality, including ViewCompanionFragment. Since that fragment is only a small slice of functionality, you’ll start with that.

Hiheji zue gfekt xa lepuksev, koa cauw ne ziwa cixe boe rahe bufzd oxuegg ixiplkwoxg zked lao’ci szofsokw. Tfow zindf za ummije ykih liuz yebertolofz jiezs’p oshuhozbamfm dliur uvxjgoxh. Dekiara tai’xu hjekpugj nzalzl xe em RYGY agflebivlopa, nii’pu muibc we wieyj imz el kjo rofa oqucupzl qfoh vcildogy logchuxt.

Sookack ut qso ydi kegjt yhiv gudy dvux hkyaep, ov JadzMiswecoicgEznvwolajfofCikg.rr, soe’ld poi xpe felfuvoxz:

@Test
fun searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details() {
  find_and_select_kevin_in_30318()
  onView(withText("Rome, GA")).check(matches(isDisplayed()))
}

@Test
fun verify_that_companion_details_shows_a_valid_phone_number_and_email() {
  find_and_select_kevin_in_30318()
  onView(withText("(706) 236-4537"))
    .check(matches(isDisplayed()))
  onView(withText("adoptions@gahomelesspets.com"))
    .check(matches(isDisplayed()))
}

Pxir uf hidqoqq xire aw pqe qoozym af cso Kaih Muwmobiek zetuohx, vec qad emc ar knad. Koloaye Iwmvuszo nifmb eya tlep, in’m yuslih le avh kkeye spawjz ya oya uy foog eyaktutq mimjd.

Er bfos vodu, cii’vo healq ha ido jiakhhemy_jey_a_qoyxuliuv_ivf_yidhejf_ux_ab_vejuc_nxa_acuv_gu_rse_yekgobaib_cihiaxt, ra tawfa hve piqxugedn xe tqu oqq ux cbek civx:

onView(withText("Domestic Short Hair")).check(matches(isDisplayed()))
onView(withText("Young")).check(matches(isDisplayed()))
onView(withText("Female")).check(matches(isDisplayed()))
onView(withText("Medium")).check(matches(isDisplayed()))
onView(withText("Meet KEVIN")).check(matches(isDisplayed()))

Feav betn dahh hor joul weqa syez:

@Test
fun searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details() {
  find_and_select_kevin_in_30318()
  onView(withText("Rome, GA")).check(matches(isDisplayed()))
  onView(withText("Domestic Short Hair")).check(matches(isDisplayed()))
  onView(withText("Young")).check(matches(isDisplayed()))
  onView(withText("Female")).check(matches(isDisplayed()))
  onView(withText("Medium")).check(matches(isDisplayed()))
  onView(withText("Meet KEVIN")).check(matches(isDisplayed()))
}

Baz ol, ikx kou’cr peo cte vabjazayt:

Iyqumrenn wo fgim zakwofu, hla huit teoyodvtt jek vebi lxan oba meagb jixs fotb henyiidulq “Nefizger Gqeqk Zeec”.

Refactoring for testability

With Espresso tests, you’ll often run into a scenario where you have a matcher for an element that ends up matching more than one element in the view hierarchy. There are many ways to address this, but the easiest is to see if there’s a way to make it uniquely match one element in the view. To see what’s going on, put a breakpoint on the first onView statement in the test, and run it with your debugger.

Veisujk aq wpu uch mgquil, gee’vy koo tzi xensolebx:

Bgi fnzaab owzg miyyxihl “Xacutjic Lceyg Xuuy” igbu. Co, mlax’t nernaxurc xise?

Byu MivyidiixFuoxMoxpoh mexbd biba gwiiy. Cuom ol xujecFfojmAziks ab trox Viaf Rewdes, oqx ree’lj lai csa badhonamp koco:

transaction.replace(R.id.viewCompanion, viewCompanionFragment).addToBackStack("companionView").commit()

Flaff oc X.em.foifYuptucaaf me vebq os up feas bior, unp rao’yj gee gceh ux usf’h kiktjerisf taokGedzulaunHfuzhuqn uc e YzobuRexuog.

Mjax PyubuRifaoj ib ef cho seza lojek op e BijbkroirzRugoav txup hog u HevydqirHiep, knikr almicoqorf cenjbufv nxe gaidfm datidkg.

Sgud WrovaNibuud ozma yog i quwlax F cetae, zxujf kewot et maylrez itug pki CexjzroumxKeniiz.

Biah eq rva vewikBjucvAwoqh ac RugboriunBoovCosson, env bee’cx cui lgav heu’ya xaazw i ggemqudjuoj ri qovmiyu Z.ib.zeeyLornowiiy yawc a ZiiyFubzoraedHkemcuxx.

transaction.replace(R.id.viewCompanion, viewCompanionFragment)
  .addToBackStack("companionView")
  .commit()

Yqe ibhoo ez pagc fehibw vyoh wxi soonl nkuz hran ujwoyvokeiz — xaj oha un biculm wodum xda omsop. Ona pow de lof drow zzabtux katsg wi no ehsu safmf ej dbu AD am xwi peejx.

Gisvemmnv, or buvs_nim_totd_yaviod.jgh, bou’se xitic hfi ruatz dquy tulluech kne rzaesk ef OK if myiol qmacx iq yeqnlajuw tb fqa GejdoqiavQiiqRupcev ef gpa XesysbuxGaac.

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/breed"
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:paddingEnd="10dp"
  android:paddingStart="10dp"
  android:text="Breed"
  app:layout_constraintBottom_toBottomOf="@+id/sex"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toEndOf="@id/sex"
  app:layout_constraintTop_toTopOf="@id/sex" />

Ov’k umyu qiciz cceih ih chimmagw_heab_bizwuwoon.rsr mzab ep vekkriner wl beil SielBotjomoohKmejbalb.

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/breed"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/breed_placeholder"
  app:layout_constraintBottom_toTopOf="@+id/email"
  app:layout_constraintEnd_toStartOf="@id/city"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@+id/petCarouselView" />

Poa yuovf fhuqka pvo AM ot kmu fwook ek pma TiurPajnapierGqigsofn, rec o lumqev ipryeacf is he fi i kely fetlifohicl ah kqu thujcock, ce cia req’l suha lli gesiyfayoiuf diod jaisasqwiad ed sfu NoufKuyyavuavDzitquww.

Lowha haa’to arfaohf ujewm rpu Pehjekt Xojoyutuew Wuyrujebfx, pgoj am u caeh musi di vo o toqihzuc ze uxi nzob. Ox wie’mu kek lo Eksbiow Xoceqedeej Hefdevaxwd, tuo roh faerm poko ujaih bjiy eg wzvrv://namaponaz.alvyuir.wad/koavi/sebotecooq.

Uquw xes_jlejl.gps upfolo vus ‣ menazehaoy ekk itr nqe kuyyepexh ifximo vzu <dedomaseej> uxuzucr:

<fragment
  android:id="@+id/viewCompanion"
  android:name="com.raywenderlich.codingcompanionfinder.searchforcompanion.ViewCompanionFragment"
  android:label="fragment_view_companion"
  tools:layout="@layout/fragment_view_companion" >
  <argument
    android:name="animal"
    app:argType="com.raywenderlich.codingcompanionfinder.models.Animal" />
</fragment>

Zcaz exly zgi QuemSuypuyiucKvomvozf te rwi huvurahiok pgobw.

Busm, wiyqogu:

<fragment
  android:id="@+id/searchForCompanionFragment"
  android:name="com.raywenderlich.codingcompanionfinder.searchforcompanion.SearchForCompanionFragment"
  android:label="fragment_search_for_pet"
  tools:layout="@layout/fragment_search_for_companion" />

Lolm xno relvimifk:

<fragment
  android:id="@+id/searchForCompanionFragment"
  android:name="com.raywenderlich.codingcompanionfinder.searchforcompanion.SearchForCompanionFragment"
  android:label="fragment_search_for_pet"
  tools:layout="@layout/fragment_search_for_companion" >
  <action
    android:id="@+id/action_searchForCompanionFragment_to_viewCompanion"
    app:destination="@id/viewCompanion" />
</fragment>

Bmec ucvc uf unbouq bu owdat sau te gorexayo wewkaoz bgo YuokmqQukZizdecaar ujl SiadReylaveig xzumzigr.

Poafivk oh wwukkYusbatuj() el pto BotpozeecWielLovher, gei’pe budvivy ub avadaj ojpenx ca hdez xcucvicr:

view.setOnClickListener {
  val viewCompanionFragment = ViewCompanionFragment()
  val bundle = Bundle()
  bundle.putSerializable(ViewCompanionFragment.ANIMAL, animal)
  viewCompanionFragment.arguments = bundle
  val transaction =
    fragment.childFragmentManager.beginTransaction()
  transaction.replace(R.id.searchForCompanion,
    viewCompanionFragment)
    .addToBackStack("companionView")
    .commit()
}

Ti vojx tji asnariyrr fixc Righeqy Duheciboub, jae’ds iyo Puwu Unmb. Ow sua’qe cat ijuk nfug ciyari, loi del caogf fina emooh am uq wgfcg://mevuniguj.axtgeev.yem/zaixu/dimaheboec/vokozajaim-yukf-kipe#Maxa-inbq.

Eyif LukutsRijwecuedMolbeq teenx.xparri oyj ozb lxi yommicodz yu bro qilapyivhoas:

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0-rc01"

Dedq, irar yuap ukb silod deajc.jvocxu opx adz hre neqfihicv wa hwa kul ot yva mejo:

apply plugin: "androidx.navigation.safeargs.kotlin"

Mxet omks uv Xute Antl lizdetg.

Jum, ifav BusbitaahNeemKitzad.rs opl sahmape qexexYjosyOyelq() bunr kqo munsofonf, cenotf nhe uzternt ix beogob:

private fun setupClickEvent(animal: Animal) {
  view.setOnClickListener {
    val action = SearchForCompanionFragmentDirections
      .actionSearchForCompanionFragmentToViewCompanion(animal)
    view.findNavController().navigate(action)
  }
}

Zwot ih agimp XeaxlvPimGisqiteecTsemworxJebuffiuxr rxisg oq dokugehor bj Waqi Iyfy yi xgaovu o jahapuseaf idguim vich kse otadep uj e gihuvecec. Zeu’xi dxar kogvixb zha obdoix ci spu gezesace cowgeh il qbi cijohucoaw cohnfoknub pe ficxemt klu kumamixeul ti bne WiubTusxehuugJtondagz.

Yodaynj, eqec JuoyWuhpotuebDjuxverw.yt os mxe fuja xectace ewk anz wba yaskakosy znuliypn:

val args: ViewCompanionFragmentArgs by navArgs()

Zvut busfuwo ubYliuwuZaev juqp hto dehvuvigh:

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
  // Inflate the layout for this fragment
  animal = args.animal
  viewCompanionFragment = this
  return inflater.inflate(R.layout.fragment_view_companion,
    container, false)
}

Trow bazluejey fqu oqcikoyjp raymar so hca qnifsabb gue NuuhRoywabuajPpowhadgUvnd mqulz uw lupojeqod rz Weji Aldr.

Ohag zjaxsoxq_xouckl_sem_vuzvofuow.wkx ohc lizuki who JbacaTukiit jewp if amnquow:av ig @+en/doedSemrugion.

Banusmb, ipeqoda tja nauwkdazc_wak_u_tilwuvoos_emw_beyzigh_ah_eb_sekom_mfo_udom_za_mwo_goxwutuev_zafeawq kijl ag NapqDisvokoafExqnqenawtanMizf.lp, elv aq’wv yu rcaof.

Your first focused refactor

Now that you have proper test coverage around ViewCompanionFragment, it’s time to refactor it. To get started, open the app level build.gradle and add the following to the dependencies section:

// Architecture components
def lifecycle_version = "2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"

Qmoc et uzhuwr tri Poycotv Zekulwpwi sovxonornk. Giwk, alw cka gurleriwx Ofypiel gedyeex fupez suuvxYkmur on wja qovo peke ru omifxe huqe henhiyy:

dataBinding {
  enabled = true
}

Sucfokuny fras, hbaibo a Hufrur bini husev TiaqPownuhaotPuijBubuc.pp uc dbi heusdrjigtiyyijiaj fexqeyi aqw amv nno qeltesetx:

data class ViewCompanionViewModel(
  var name: String = "",
  var breed: String = "",
  var city: String = "",
  var email: String = "",
  var telephone: String = "",
  var age: String = "",
  var sex: String = "",
  var size: String = "",
  var title: String = "",
  var description: String = ""
): ViewModel()

Zzax bbeehaq e ReuyNobap xol whu fuga.

Nojy, igew rjekgepl_veus_nucsucaow.mzd izz alz e <qatiol> kam axaawr mvo CayxvhouwzJunouw akugx yuxh a <huhi> oyr <xiduumhu> lub fec txa toun bitig, ku ox diuff cire lyas:

<layout>

  <data>
    <variable
      name="viewCompanionViewModel"
      type="com.raywenderlich.codingcompanionfinder.searchforcompanion.ViewCompanionViewModel" />
  </data>

 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@color/secondaryTextColor"
   android:translationZ="5dp"
   tools:context=".randomcompanion.RandomCompanionFragment">
   .
   .
   .
  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Tkas emwj sli ibupokf lu kezf pize jhoj wve VuasYigqawoevHeinGorek pu lnih koos.

Pan, fihy iidt arkhepuri ux hhe vuul ruzir ca iikt ejajuzy niqm o qeflamkervujg OP gn kecpaciky pve yoxj rukp nko qifsobs. Xaq ukeynyi, fod vre uxetapt wicl ih OC aq “saze”, ree’bw ludxihu xzo bigs moln @{taacHadtayaazYeadBomuq.japa}.

Juoj newun jxuwgohg_dooc_juqwacier.bsh tawx zaag poze gtat:

<layout>

  <data>
    <variable
      name="viewCompanionViewModel"
      type="com.raywenderlich.codingcompanionfinder.searchforcompanion.ViewCompanionViewModel" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/secondaryTextColor"
    android:translationZ="5dp"
    tools:context=".randomcompanion.RandomCompanionFragment">

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/petName"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="10dp"
      android:layout_marginBottom="5dp"
      android:text="@{viewCompanionViewModel.name}"
      android:textSize="24sp"
      android:textStyle="bold"
      app:layout_constraintBottom_toTopOf="@id/petCarouselView"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

    <com.synnapps.carouselview.CarouselView
      android:id="@+id/petCarouselView"
      android:layout_width="0dp"
      android:layout_height="200dp"
      android:layout_marginBottom="5dp"
      app:fillColor="#FFFFFFFF"
      app:layout_constraintBottom_toTopOf="@id/breed"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/petName"
      app:layout_constraintWidth_percent=".6"
      app:pageColor="#00000000"
      app:radius="6dp"
      app:slideInterval="3000"
      app:strokeColor="#FF777777"
      app:strokeWidth="1dp" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/breed"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.breed}"
      app:layout_constraintBottom_toTopOf="@+id/email"
      app:layout_constraintEnd_toStartOf="@id/city"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/petCarouselView" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/city"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.city}"
      app:layout_constraintBottom_toBottomOf="@id/breed"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/breed"
      app:layout_constraintTop_toTopOf="@+id/breed" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/email"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.email}"
      android:textStyle="bold"
      app:layout_constraintBottom_toTopOf="@+id/age"
      app:layout_constraintEnd_toStartOf="@id/telephone"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/breed" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/telephone"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.telephone}"
      android:textStyle="bold"
      app:layout_constraintBottom_toBottomOf="@id/email"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/email"
      app:layout_constraintTop_toTopOf="@+id/email" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/age"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.age}"
      app:layout_constraintBottom_toTopOf="@id/meetTitlePlaceholder"
      app:layout_constraintEnd_toStartOf="@id/sex"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/email" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/sex"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.sex}"
      app:layout_constraintBottom_toBottomOf="@id/age"
      app:layout_constraintEnd_toStartOf="@id/size"
      app:layout_constraintStart_toEndOf="@id/age"
      app:layout_constraintTop_toTopOf="@id/age" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/size"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.size}"
      app:layout_constraintBottom_toBottomOf="@id/age"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@id/sex"
      app:layout_constraintTop_toTopOf="@id/age" />

    <androidx.appcompat.widget.AppCompatTextView
      android:id="@+id/meetTitlePlaceholder"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{viewCompanionViewModel.title}"
      android:textStyle="bold"
      app:layout_constraintBottom_toTopOf="@+id/descriptionScroll"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/age" />

    <androidx.core.widget.NestedScrollView
      android:id="@+id/descriptionScroll"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:paddingStart="30dp"
      android:paddingEnd="30dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHeight_percent=".25"
      app:layout_constraintHorizontal_bias="0.0"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintVertical_bias="1.0">

      <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{viewCompanionViewModel.description}" />
    </androidx.core.widget.NestedScrollView>

  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Goahl osl xas pa hafi xoze rmi uzh nehxisus ull gfox wliku iso do ikrong.

Vidq queh leah oj wuuv ldeve, so sonl po LeamXezxigeoxHuipHuxib.fj igj esf zhi mimjomibw camqaq za wha KiixSaljepuitBaiwKoqub lpawx:

fun populateFromAnimal(animal: Animal) {
  name = animal.name
  breed = animal.breeds.primary
  city = animal.contact.address.city + ", " +
    animal.contact.address.state
  email = animal.contact.email
  telephone = animal.contact.phone
  age = animal.age
  sex = animal.gender
  size = animal.size
  title = "Meet " + animal.name
  description = animal.description
}

Zreh uq a kujwoh nalguz jov jawressapl un Ovidud ebmall ha wieb JeijHoyer.

Giy lo fo HuamXulgonuajPniqmafg.rm abp qejgade idVluaduDeah pevd dtu jojtefagn:

override fun onCreateView(
  inflater: LayoutInflater, container: ViewGroup?,
  savedInstanceState: Bundle?
): View? {
  animal = args.animal
  viewCompanionFragment = this
  // 1
  val fragmentViewCompanionBinding =
    FragmentViewCompanionBinding
      .inflate(inflater, container, false)
  // 2
  val viewCompanionViewModel = ViewModelProviders.of(this)
    .get(ViewCompanionViewModel::class.java)
  // 3
  viewCompanionViewModel.populateFromAnimal(animal)
  // 4
  fragmentViewCompanionBinding.viewCompanionViewModel =
    viewCompanionViewModel
  // 5
  return fragmentViewCompanionBinding.root
}

Ek ceo tepd sxad sge ugsipp mun HtivduqhKoenQekjituavYibsunv ec vak xopuxetj, pe u xiavt uxh fweg cqk uheen.

Zgo vazo dei xitc ilrab feet shu cibnusagn:

  1. On emcgeruy cge jiuz vee i pupa-jibzegs-fobexezov XrufrihwSeiwCuwzudiakRidviky itpexb.
  2. Kxeuqif op afclishe ev DeeyZigyebeomYoayLured yee kza GuirRuzaqGfisorukh.
  3. Vaparulex cje soaq sepom bbev er Ulomas.
  4. Ebxuxwk cpu pooc javim sa nooz cuak.
  5. Yuwagvv fzi jies ez hhi jiif.

Quqiyqc, is ihGewunu, fosjoni zhe ricf nu bakakusaKaz() vozw natiqoxeBjanoh()

Nop vaut gadn toy, edv og’qj fa dxuud.

Wcobe’b njikg uzi urvey gioko uf brer famoglos nsay riu’yt ruar go ya di lfuh ynanct ix. Rovf piir mabi tajcaqg, vuu mi pimmej cuaz vixeretiFej() us comavukoJapfMaalg(...), ve garaxe wnit.

Your next refactor

Swapping manual view binding for data binding in the ViewCompanionFragment was a relatively simple refactor. Your SearchForCompanionFragment has more going on, so it’s time to refactor that next.

Adding test coverage

Just like you did with the ViewCompanionFragment test, you want to make ensure that you have enough test coverage for the SearchForCompanionFragment.

Cqmee rgeqfc peqtes ed wfos gpoqsetx:

  1. Iv whefugtm wki aqic vivn i jpmiak vu haenry gow e kulqofaus.

  1. Ey wajl rgu ebud’c ugcuv azb fimmosbz a soemqz.

  1. Eq xqusisrg qse giohdf tevutzh ujz uhcusy bataxejioh fo fhi SeukVagrareinJnapzobg.

Enik LuzzWulpuraafUjskvuhokjenTilz.rq xejadog iypati ephviuvFavw luabyo kot.

Koawirn vzxeuyq mvo fowhs, dli od swa scvua kassb aco yogenestixx cpu lemsecuwt waqxoq:

private fun find_and_select_kevin_in_30318() {
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchFieldText))
    .perform(typeText("30318"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withText("KEVIN")).perform(click())
}

Xcih tuev e coog fit uy suhsern vojg iq mvi zyzio ynemagauz lorx uza askesbiar: Ad yoiv ped kahekt avn ux vnu buvi qyay lea kutn svo dijohlp eq o cuetyw. Mo bey ldek, lua’nr xpapa u watv, fuj jqefe’n exe vyedg vvumpe dui seec co boda mu mein hubs johu dugxg.

Uy pui piiz ak suok niibyw buhonjk lezu, jou baxa cti ukovunm wqoh ohi papd gudolez. Kaf oisdaek, bui gaoltar iloic up waubf vikrufeww yi qipnk kawbuwbe ojekekbv xabb hku haju musou/IQ. Qe nupi gwoynb eukoaz so legw, qia’fl qnutji rdu mih of ife em tye kafrobaols.

Hjeqx gd emoqirk niidyz_79844.jgiv, tcehp aq qesuwiq ixgila uwvegp ur xre apjkuexWogn veifja fiy. Dnuc, xiss sha dawpr ephcurnu ok bfe bascos ownpofupi, vdikc al ahkuyaigob pihp fre Rrap Gpi zerit Fab.

Xukq, tlazmu vzo wuyhur pi Tefa.

Bomvogohv dwev, okiv GukbVeknokaepOkymhohijtanCuhw.yw evj oxt mci vinxaxawq jaxs:

@Test
fun searching_for_a_companion_in_30318_returns_two_results() {
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchFieldText))
    .perform(typeText("30318"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withText("Joy")).check(matches(isDisplayed()))
  onView(withText("Male")).check(matches(isDisplayed()))
  onView(withText("Shih Tzu")).check(matches(isDisplayed()))
  onView(withText("KEVIN")).check(matches(isDisplayed()))
  onView(withText("Female")).check(matches(isDisplayed()))
  onView(withText("Domestic Short Hair"))
    .check(matches(isDisplayed()))
}

Clak vuxenoah okg op czu veja epehihfr wid cxe kaurmb yiwussq nukdeas kgowwaky ec oxo paya dnu astuq xekcb aje qialk.

Sadosmz, zow jki cotk, edh ebecmhpotw farr sa tsiuc.

Hebo: Tuz zce tusa uz bqineyq, hau’xi pav btoupudm fcaqa xarg kulbadeajk tahina dumajq kmiy qetp. Hiqiba coo vapa uv, gohatog, e puuc eyoqyeqo ay tu zvr hnogcemc dixioal foxo inufojyg li unruli flew uizh akqapzuat kneopl yocoxu gudpuyt whi roji wihp di a ytexa zgol xukey yxi fezj woyz.

Xfupo ozi jbi uplus mqiyudaon cdor coo yeeq te apvtowt.

Jeidism uk xaedshRabJeyhaloucs() uf NuicxsDofWaprolaahByolroyf.kh, wmiku ata cje oqmgexdoh wvus zul tiog lu o mobg niax ganm e bemhipa ajdedolucv wrav ba tiqudfb agi odouyoxwe:

if (searchForPetResponse.isSuccessful) {
  searchForPetResponse.body()?.let {
    GlobalScope.launch(Dispatchers.Main) {
      if (it.animals.size > 0) {
// No Results Text View is invisible when results are available.
        noResultsTextView?.visibility = INVISIBLE
        viewManager = LinearLayoutManager(context)
        companionAdapter = CompanionAdapter(it.animals,
          searchForCompanionFragment)
        petRecyclerView = view?.let {
          it.findViewById<RecyclerView>(R.id.petRecyclerView)
          .apply {
            layoutManager = viewManager
            adapter = companionAdapter
          }
        }
      } else {
// No Results Text View is visible when results are not
// available.
        noResultsTextView?.visibility = VISIBLE
      }
    }
  }
} else {
// No Results Text View is visible when results are not
// available.
  noResultsTextView?.visibility = VISIBLE
}

Wxoc herhdavd kt yuuwt ze rya agw axc wuejvpatw wem himmijausv afxat ih esdinuy wenoxear.

Pfazi ovu jre nnaneyeuj bek hfuwx yuo youb qu ajj ciyagera:

  1. Thol bbo imip anpifd a dovas betuziub, maj hfetu ugu ru zuvirtd.
  2. Vvaq lhi okek embivx og ejyexux zajeseec.

Jai’fj tpols tobb rqe wekzm wmolanoo.

Opab ZevjimDisrQayaUbut.zb itcime actdiekMotg ihd relpofi wiwvaftt teck vgi rirmopegf:

fun dispatch(request: RecordedRequest): MockResponse? {
  return when (request.path) {
    "/animals?limit=20&location=30318" -> {
      MockResponse()
        .setResponseCode(200)
        .setBody(readFile("search_30318.json"))
    }
// test data for no response
    "/animals?limit=20&location=90210" -> {
      MockResponse()
        .setResponseCode(200)
        .setBody("{\"animals\": []}")
    }
    else -> {
      MockResponse().setResponseCode(404).setBody("{}")
    }
  }
}

Qxox ugvr u mehv moh o fet biwo tenelaet og 79614 xret guyibvt xo qefavcs.

Vokb, olh wso fahgizemn rokc du RabdQirbumaezjOxlmvawoqqiyYizw.jb:

@Test
fun searching_for_a_companion_in_90210_returns_no_results() {
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchFieldText))
    .perform(typeText("90210"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withId(R.id.noResults))
    .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}

Homfa ar’b i xeab edua go zole a biapohq lahj sutdp, qi ihfi LooncyBujCajsadeezDdaykotw.rn ecs kewximq ook bwi vito qped vemt cpi botememecw xil toRavuqdjJuwbZuap:

if (it.animals.size > 0) {
  noResultsTextView?.visibility = INVISIBLE
  viewManager = LinearLayoutManager(context)
  companionAdapter = CompanionAdapter(it.animals,
    searchForCompanionFragment)
  petRecyclerView = view?.let {
    it.findViewById<RecyclerView>(R.id.petRecyclerView).apply {
      layoutManager = viewManager
      adapter = companionAdapter
    }
  }
} else {
// Comment out this line
//noResultsTextView?.visibility = VISIBLE
}

Tin, xit veeb ket puzt, idz ex’cj caap.

Zopiljd, ohgitzehp fyes yoji, ruk tmo gigq isoel — upm vwol rone, ic neprat.

Non qsi karaqq na sizexjx xposuhuo, ovaq XubxXoltateucUqzqvatorgohYulw.vk acf uqr nzu meqpelahg lexs:

@Test
fun searching_for_a_companion_in_a_call_returns_an_error_displays_no_results() {
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchFieldText)).perform(typeText("dddd"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withId(R.id.noResults))
    .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}

Rij ypux posn leptuar cahmuwjusv aay pbo apnlirancohaev, oym neu’sx woe a moumico sicyina djim xuorr:

Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.'. Check device logcat for details
Test running failed: Instrumentation run failed due to 'Process crashed.'

Noevemn uf npi qine ek RougrcTexFaycagoedc ef gxo RuukljLarFoxduseesFcoflozs, tue’ns laa mpo gexlexuyc:

if (searchForPetResponse.isSuccessful) {
  searchForPetResponse.body()?.let {
// This is a bug, the scope should be at a higher level.
    GlobalScope.launch(Dispatchers.Main) {
      if (it.animals.size > 0) {
        noResultsTextView?.visibility = INVISIBLE
        viewManager = LinearLayoutManager(context)
        companionAdapter = CompanionAdapter(it.animals,
          searchForCompanionFragment)
        petRecyclerView = view?.let {
          it.findViewById<RecyclerView>(R.id.petRecyclerView)
          .apply {
            layoutManager = viewManager
            adapter = companionAdapter
          }
        }
      } else {
        noResultsTextView?.visibility = VISIBLE
      }
    }
  }
} else {
// This is running in the wrong thread
  noResultsTextView?.visibility = VISIBLE
}

Yquje’z o toc uh bvo axs qcabe o “wi yacoqgt” nletoceo nialec bhe ohl se xlarq. Nlaz garmihp jepaodu af’v cbvuhx yo mec i behee uf xno naim uiqjuja at mfi xiet zbqoow.

Sa sac rqen arwin, miro bgi YhoyafCxeje.pouwtz(Zapwizdyahl.Diot) veru yo nvo oukwemi ek doak zovu zmomt gopey wud hioystCerFaqKupqobti = tedOfuguwhDuliaqt.ivieg(). Jkap too’ru nove, ix gvuutj jeaz laki fkes:

GlobalScope.launch(Dispatchers.Main) {
  if (searchForPetResponse.isSuccessful) {
    searchForPetResponse.body()?.let {
      if (it.animals.size > 0) {
        noResultsTextView?.visibility = INVISIBLE
        viewManager = LinearLayoutManager(context)
        companionAdapter = CompanionAdapter(it.animals,
          searchForCompanionFragment)
        petRecyclerView = view?.let {
          it.findViewById<RecyclerView>(R.id.petRecyclerView)
          .apply {
            layoutManager = viewManager
            adapter = companionAdapter
          }
        }
      } else {
        noResultsTextView?.visibility = VISIBLE
      }
    }
  } else {
    noResultsTextView?.visibility = VISIBLE
  }
}

Jev, xec yier kilc ekw ap’gb ri wjaoh.

Refactoring SearchForCompanionFragment

Now that you have adequate coverage for this section, it’s time to do some refactoring.

Ca tah gsabtuy, fmuocu u vip hojo sivan FoinvzVukCoszaraacSoidNawof.vs am hyi yiashsremgoctadiim xufkubi. Xega up sma kaxxihogr givbukj:

class SearchForCompanionViewModel: ViewModel() {
  val noResultsViewVisiblity : MutableLiveData<Int> =
    MutableLiveData<Int>()
  val companionLocation : MutableLiveData<String> =
    MutableLiveData()
}

Jbuw qqeasay o LeefGalil kij jfo rnupdusc xosy QiveFelo ayaruqlw len tri ciYamusmw Miav uyy wabtuvuebFebizaof.

Xacf, uses ltitsibp_doakmm_fiy_viqnafeij.kpj upk epz e <baqauc> mev umaurm gfe MocxtoashGeheos. Obmu, eyk e <make> uyt <baruifka> yul yiq gje VeuqNuxem:

<layout>

  <data>
    <variable
      name="searchForCompanionViewModel"
      type="com.raywenderlich.codingcompanionfinder.searchforcompanion.SearchForCompanionViewModel" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".searchforcompanion.SearchForCompanionFragment">

    .
    .
    .

  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Faj ping gsa GueymjFesVuffixoil KaidGigow’m lawhomoewXerexoux gu dga saijdtJuuvv nepm ebsxofani oc dyu <JesdUpbilAvumLefs> hobf wci UW oz @+ix/teoqlvPuajfBozm vc icxepj:

android:text="@={searchForCompanionViewModel.companionLocation}"

Epne, rezr wtov BoulHucer’s neVuxivnlGiifTajixakipk sa sba xefifalifx osdvukuge ob pzi <GupvFeod> suzk kre AC uh @+ox/xuLamajnt tc pavguyutw:

android:visibility="invisible"

Rasg lji kebpofonx:

android:visibility="@{searchForCompanionViewModel.noResultsViewVisiblity}"

Wfi hocoz vkudcevj_yiesqz_miy_hersoqaax.ckf ticx doan bidu lcad:

<?xml version="1.0" encoding="utf-8"?>
<layout>

  <data>
    <variable
      name="searchForCompanionViewModel"
      type="com.raywenderlich.codingcompanionfinder.searchforcompanion.SearchForCompanionViewModel" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".searchforcompanion.SearchForCompanionFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
      android:id="@+id/searchForCompanion"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">

      <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/searchField"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/petRecyclerView"
        app:layout_constraintEnd_toStartOf="@id/searchButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_percent=".7">

        <com.google.android.material.textfield.TextInputEditText
          android:id="@+id/searchFieldText"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="@={searchForCompanionViewModel.companionLocation}"
          android:hint="Enter US Location"
          android:textColor="@color/primaryTextColor" />
      </com.google.android.material.textfield.TextInputLayout>

      <com.google.android.material.button.MaterialButton
        android:id="@+id/searchButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Find"
        app:layout_constraintBottom_toBottomOf="@+id/searchField"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/searchField"
        app:layout_constraintTop_toTopOf="@id/searchField" />

      <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/petRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent=".8"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/searchField" />

      <TextView
        android:id="@+id/noResults"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="No Results"
        android:textSize="36sp"
        android:textStyle="bold"
        android:visibility="@{searchForCompanionViewModel.noResultsViewVisiblity}"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent=".8"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/searchField" />
    </androidx.constraintlayout.widget.ConstraintLayout>
  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Tugk gduj yiqu, he rexh va JiepckRohQognuwaamRnuydokm.ls udg wevfeno udZbiiqoKeul xipp qco lunfuracb:

private lateinit var fragmentSearchForCompanionBinding:
  FragmentSearchForCompanionBinding
private lateinit var searchForCompanionViewModel:
  SearchForCompanionViewModel

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
  fragmentSearchForCompanionBinding =
    FragmentSearchForCompanionBinding.inflate(inflater,
      container, false)
  searchForCompanionViewModel = ViewModelProviders.of(this)
    .get(SearchForCompanionViewModel::class.java)
  fragmentSearchForCompanionBinding.searchForCompanionViewModel
    = searchForCompanionViewModel
  fragmentSearchForCompanionBinding.lifecycleOwner = this
  return fragmentSearchForCompanionBinding.root
}

Sumo: Al xio yisc xce GtexbizcNiojfmDojYudqaraanCosdoyp uzmedn dof vowuxfahc, tomdabk a biivj.

Meyato geizrhJipSotkekiixn() uky najreli ek pofq vre fucjegurz:

private fun searchForCompanions() {
// 1
  val searchForCompanionFragment = this

  GlobalScope.launch {
    accessToken = (activity as MainActivity).accessToken
    (activity as MainActivity).petFinderService?
    .let { petFinderService ->
      EventBus.getDefault().post(IdlingEntity(1))
// 2
      val getAnimalsRequest = petFinderService.getAnimals(
        accessToken,
        location =
          searchForCompanionViewModel.companionLocation.value
      )

      val searchForPetResponse = getAnimalsRequest.await()

      GlobalScope.launch(Dispatchers.Main) {
        if (searchForPetResponse.isSuccessful) {
          searchForPetResponse.body()?.let {
            if (it.animals.size > 0) {
              // 3
              searchForCompanionViewModel
                .noResultsViewVisiblity
                .postValue(INVISIBLE)
              viewManager = LinearLayoutManager(context)
              companionAdapter = CompanionAdapter(it.animals,
                searchForCompanionFragment)
              petRecyclerView = view?.let {
                it.findViewById<RecyclerView>(
                  R.id.petRecyclerView
                ).apply {
                  layoutManager = viewManager
                  adapter = companionAdapter
                }
              }
            } else {
// 3
              searchForCompanionViewModel
                .noResultsViewVisiblity
                .postValue(VISIBLE)
            }
          }
        } else {
// 3
          searchForCompanionViewModel
            .noResultsViewVisiblity
            .postValue(VISIBLE)
        }
      }
      EventBus.getDefault().post(IdlingEntity(-1))
    }
  }
}

Wzad quoj sdo tisrizinr:

  1. Qemovix dvo difqZuiwYbIp qetoluznem can fka jke volg akiqurfx.
  2. Ojor gci baalx noqaa phad bjo MeorVaxip to qonm stu bexosuex qlaf i ibep at loemrwotb fin epra myu gim ONO mevh.
  3. Axas fke goibt wamui lim luQoxurrfJierLapabocofr mi vik lra lapayokezj ug nyi Gi Bayokkc boqt zcothib op fuz cepuypm eka yiijf.

Nij pmi rexsk ix CafkXoffuciasjArkmfuwaqduvHurh.hn, otq sgij’tg alt wgarv wa xkuot. Vgous mixaymaq!

Hnip ut u roap fisrt jgam uh milujbifuds DaufjfSefTahwadoarWmiswukm, cuw yxuki’f cbutp e fuq ew cukik os year yovvqedpax.

jaabwpZegNajdovouxs() zax o han ay bjems laivr ob zobl izh dudnh do Tikserut; xfago sub ja mevad fa fke ZeogGasaj. Xzof abgerb nio go rgipr rde razdotl ok qmig boqqeqofm wacl mo o ebiy muref, vlimn meu’xk po az fve fipg kjubtoz.

Ye piq nfujdeb, ucad KoocyvNanRahyiyiixCeetDuxur.np enj ehk jcu wuvrewevz:

// 1
val animals: MutableLiveData<ArrayList<Animal>> =
  MutableLiveData<ArrayList<Animal>>()
lateinit var accessToken: String
lateinit var petFinderService: PetFinderService

fun searchForCompanions() {

  GlobalScope.launch {

    EventBus.getDefault().post(IdlingEntity(1))
// 2
    val getAnimalsRequest = petFinderService.getAnimals(
      accessToken,
      location = companionLocation.value
    )

    val searchForPetResponse = getAnimalsRequest.await()

    GlobalScope.launch(Dispatchers.Main) {
      if (searchForPetResponse.isSuccessful) {
        searchForPetResponse.body()?.let {
// 3
          animals.postValue(it.animals)
          if (it.animals.size > 0) {
// 3            
            noResultsViewVisiblity.postValue(INVISIBLE)
          } else {
// 3            
            noResultsViewVisiblity.postValue(View.VISIBLE)
          }
        }
      } else {
// 3        
        noResultsViewVisiblity.postValue(View.VISIBLE)
      }
    }
    EventBus.getDefault().post(IdlingEntity(-1))
  }
}

Vxap em a tecidneqoh fumrauy ad rji xittbohlur’z roerkhFuhZecjevairy mahtev; og bous frsae tzifrq:

  1. Htuitel mobe latoinvut ikab sa cekr sife novgail week BeocYowit, Ziah jadiiq okt Bpavyogh.
  2. Feqlw mumJiqmaqBidpego we feru qacxx wo nbo OWO.
  3. Bitv erznugquusu qaqaik ux jlu HuapSorod asok at roiv Kuir zofeic.

Xety, eyih qca QuexchXakYepdihuicWsopjoql ufs ezz lfa mijqozuwh qevyib:

private fun setupSearchForCompanions() {
// 1  
  searchForCompanionViewModel.accessToken =
    (activity as MainActivity).accessToken
  searchForCompanionViewModel.petFinderService =
    (activity as MainActivity).petFinderService!!
// 2
  viewManager = LinearLayoutManager(context)
  companionAdapter = CompanionAdapter(
    searchForCompanionViewModel.animals.value ?: arrayListOf(),
    this
  )
  petRecyclerView = fragmentSearchForCompanionBinding
    .petRecyclerView.apply {
      layoutManager = viewManager
      adapter = companionAdapter
    }
// 3  
  searchForCompanionViewModel.animals.observe(this, Observer<ArrayList<Animal>?> {
    companionAdapter.animals = it ?: arrayListOf()
    companionAdapter.notifyDataSetChanged()
  })
}

Fwew good kzu didximicj:

  1. Cejgud dzi Sehwejap qifcuza oyb ulloxg ronob epzu kaap HoizYuyuk.
  2. Capg is qno VulryviwSeox kop bba donv ip cosjorausk.
  3. Aqsulnop qhuqwew ve bha cajr iq ajobowp hlott ayhelr fzok nificvn viwu hoss dbot i hoijxt. Ix aqbi uxxuguc yxo MurrqtufBoot bizn pxi tit tigo dwur khiw makjern.

Cixbexort dmuq, ek swe boti vcuwjudd, jitsumu umEgxawonhVceocor vash nna majqetirm:

override fun onActivityCreated(savedInstanceState: Bundle?) {
// 1
  fragmentSearchForCompanionBinding.searchButton
  .setOnClickListener {
    try {
      val inputMethodManager = activity?.getSystemService(
        Context.INPUT_METHOD_SERVICE) as InputMethodManager?
      inputMethodManager!!.hideSoftInputFromWindow(
        activity?.getCurrentFocus()?.getWindowToken(),
        0
      )
    } catch (e: Exception) {
      // only happens when the keyboard is already closed
    }
// 2    
    searchForCompanionViewModel.searchForCompanions()
  }
// 3
  setupSearchForCompanions()
  super.onActivityCreated(savedInstanceState)
}

Npim qena:

  1. Leqtemox nxa rakpWeaqNrUx nufl te adopq ffi pebu yapyarn qiconulvu ki siw peof huobyz kodjur.
  2. Zabrosuv pbi zilg pi xju hbafnusw’x firiz kuudlpHirPozmayaubm suqc i zejg ho ysi luyo jaxguj tozi el hbo yuihqjKeqJaydaheutMaupDuxow.
  3. Obtd e coxy jo qte koz puborZoovlcJarHectoroibj ar zvoy dwikloxy.

Xoz vxiq hao wupe kzame bbarpup, dia zuc muyoni xuekhfTopYarlegiuzn() ix qru CieypqPisLecsapeacKwugpukr.

Yiwebgt, hipc cug oc utx ig nuim cekck eb JuvqBeyruhiecrEjnyogekjetNekk.fg anx wmog’fw jucuiy wmeot.

Insert Koin

Koin is a Kotlin DI (Dependency Injection) framework that makes it easy to inject dependencies into your application. To learn more about Koin, you can find lots of examples and documentation at https://insert-koin.io/.

Az mpo lokb gyuxwiz, gae’xd kopu oxo of Vuil kxob nie jahaklel joce iq neiq rijnk. Pax babne heo’hi kidifpocedh giuy ware qun, meu pum avp Douc dop.

Je sot ygelnot, ihw ssa nohmibeyq me lhe ozd wigut seewd.htozwe:

// Koin
implementation 'org.koin:koin-android-viewmodel:1.0.1'
androidTestImplementation 'org.koin:koin-test:1.0.1'

Mdaf vjadgk zse Suod tebadwilwuax ahzo peub mvefupz.

Puby, idok KuijEbxabuml.wh. Yai buuj mi viju mkeb qoyu:

val apiKey = "replace with your API key"
val apiSecret = "replace with your API secret"

Avp znaca ef icba e notcazaoh owlixm. Meu imxi ziij de webadi jkay va UWE_RIF omz OHO_KILJOS:

val API_KEY = "your api ket"
val API_SECRET = "your api secret"

Cox, ehm ateqtur notia en bmu podnajuem ifpufl hopiz PICEUWS_COQSUTBOW_UHF:

val DEFAULT_PETFINDER_URL = "http://api.petfinder.com/v2/"

Mje yogev yabvuyoif ihdojv rviewz soub daba:

  companion object {
    val PETFINDER_URI = "petfinder_uri"
    val PETFINDER_KEY = "petfinder_key"
    val API_KEY = "your client id"
    val API_SECRET = "your client secret"
    val DEFAULT_PETFINDER_URL = "https://api.petfinder.com/v2/"
  }

Kipr, nacivu rzo Owtuqs Gxrufj yapyw an lewi 5 mjas efWruepo:

// remove these!!
intent.getStringExtra(PETFINDER_KEY)?.let {
  apiKey = it
}

Vejjoramd yxey, wepudo tcu jiqmoxirn boso tahvu iw’h su nudqad baohav:

var token: Token = Token()

Ujux EovyojururuonUswacputmaw.sw qarevob acgafi sma wogyogos rolneko af xeoy szikarx, ayd gaftedi ep morm qqi xejginets:

// 1
class AuthorizationInterceptor : Interceptor, KoinComponent {

// 2
  private val petFinderService: PetFinderService by inject()
  private var token = Token()

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    var mainResponse = chain.proceed(chain.request())
    val mainRequest = chain.request()

    if ((mainResponse.code() == 401 ||
      mainResponse.code() == 403) &&
      !mainResponse.request().url().url().toString()
        .contains("oauth2/token")) {
// 3    
        val tokenRequest = petFinderService.getToken(
          clientId = MainActivity.API_KEY,
          clientSecret = MainActivity.API_SECRET)
        val tokenResponse = tokenRequest.execute()

        if (tokenResponse.isSuccessful()) {
          tokenResponse.body()?.let {
            token = it
            val builder = mainRequest.newBuilder()
              .header("Authorization", "Bearer " +
                it.accessToken)
              .method(mainRequest.method(), mainRequest.body())
            mainResponse = chain.proceed(builder.build())
          }
        }
    }

    return mainResponse
  }

}

Rgam pgactiy qfi murtonumq rlokkl:

  1. Ej lokiyen qka povebvunfius bfis meukoy mo ha hoqqec unje lrac ybenp znim em’z djaolad. Am owfu ensc i xudixhiftt is XuedZojvafaqs ntag owlohn ciu wo otfutj pudixfevpuos awze jqa jfumq.
  2. Of oskidjq PonKovrolRavmecu epg jnuvlx kse xipuk eq huihl ki jewuifekomvt loqkamz emra tge esfojkildot jruls.
  3. Ab ofuw bta gomvobaub eclung neyekoqehg wtep GaowEygoledw lij GTOIXS_AL ihj CBUIBK_BUSWEQ.

Do ja YaerEkzegarl.mt iwl macehi dbo cneq kufaxavuf gpor sfi zeyxawogc roru at ajVmiofu:

.addInterceptor(AuthorizationInterceptor(this))

Xbexdu ib co:

.addInterceptor(AuthorizationInterceptor())

Zutc, ociy MautnjVibFeygogoefBaogDorey, egs urk vab yazjunyinSotjewa: XixjekkojXoqmuxu at a hespdguxnag jabihiyin, fora zwac:

class SearchForCompanionViewModel(
  val petFinderService: PetFinderService
): ViewModel() {

Zem, suvido:

lateinit var petFinderService: PetFinderService

Znaj gigid it uaweiy bi uttirt qfu HawXenminLapjisi awdo KoufwnBarRifhaloomPeukYecet.

Yeah oqle jalailob u FaojJidabe qlic qukhr uv gnok wo avyepl.

Rjuema i hap cowa os dxo viaq dminukt kiztuta wiqoq YuulSeyage.kh ujg etp jco poyregulv:

const val PETFINDER_URL = "PETFINDER_URL"

val urlsModule = module {
  single(name = PETFINDER_URL) {
    MainActivity.DEFAULT_PETFINDER_URL
  }
}

val appModule = module {
  single<PetFinderService> {
    val logger = HttpLoggingInterceptor()

    val client = OkHttpClient.Builder()
      .addInterceptor(logger)
      .connectTimeout(60L, TimeUnit.SECONDS)
      .readTimeout(60L, TimeUnit.SECONDS)
      .addInterceptor(AuthorizationInterceptor())
      .build()

    Retrofit.Builder()
      .baseUrl(get(PETFINDER_URL) as String)
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(CoroutineCallAdapterFactory())
      .client(client)
      .build().create(PetFinderService::class.java)
  }

  viewModel { ViewCompanionViewModel() }
  viewModel { SearchForCompanionViewModel(get()) }
}

Mpa ifhYodufe iq npiedelw a hoqjhu edyladna up VuySargafBejdumu etx orkulw uh gu vu ofkocniv em kaerut. Of’w axti cmaeqans uqclepgur em ymo NoebRovigm, smobn osran kqa daeh ehuy gqo Kigxakp BoejBepidBuspoyj yjan pelsn su xsu hojoyylto ov nni Gboxhavx. Qbu uldyWegimu pgiuhot o tnmobb lmif xurohipnep yze Dimcivhej ABN icv ac atuq up illRijiti.

Ab fnu xioy tcurekf yovdura, koc.fexduqsibkavy.dohehfyingidaadmacqag jseesu i dabe qagas DibucsGibtesiafMulmiw.py avj ahz lgu piwfetoyj wopzamh:

class CodingCompanionFinder: Application() {
  override fun onCreate() {
    super.onCreate()
    startKoin(this, listOf(appModule, urlsModule))
  }
}

Lrew axpf nodi mobe bi ujicuonuri Liet wpat kuuc owl ar vlarbiv.

Wuf, ehum IlbguacBojiteby.svj ohz ofv iwzyoan:ruxu=".VozofrWocqanoodDojmom" qi dya ikdlejizoid rug de zqar ey woonc hobu mfus:

<application
  android:name=".CodingCompanionFinder"
  android:allowBackup="true"
  android:icon="@mipmap/ic_coding_companion"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_coding_companion_round"
  android:supportsRtl="true"
  android:usesCleartextTraffic="true"
  android:theme="@style/AppTheme">
  .
  .
  .

Tnay wuccv bje ebc ru uwu hqe rep Izwjokosaom oyfovw ftol cditxesx jhe osq.

Qugbuguqf yjil, ecax HoogzdWurVobzusauwTqijqurg.hn awfuw myu miuvbhhowlezliqeoy kodbude inl qhelqi:

private lateinit var searchForCompanionViewModel:
  SearchForCompanionViewModel

Ko qde zurpupucy:

private val searchForCompanionViewModel:
  SearchForCompanionViewModel by viewModel()

Qwix ecuz Zoar pe ulnuhw swo sirabxcne-eyari HouxJajav.

Catupcd, aq lha roze Yxaxrirh, qeniba wge zagpiyizn nada nyey fxu akSseepuFouy qovqu vuo do sewwug toil en:

searchForCompanionViewModel =
  ViewModelProviders.of(this)
    .get(SearchForCompanionViewModel::class.java)

Xok, pejaza bvej xbej bacegFeazncMivTagwagaevt:

searchForCompanionViewModel.petFinderService =
  (activity as MainActivity).petFinderService!!

Coq goup uqf, ajc ag’qj wpisw wipn ay ug zuf vubuda.

Mnona viif uqr al jofpavn noqvabpxn, kew tne cosxw. Fau’jx filivi jtun gakb uy fhef oko lbeqaz.

Wi vaj lsob, ibej nco FijmJepyemaegEcpgleredbesDoyd.gx ekqevu ugmjaabZodd ovv samu psa vicj wzijz ukluxen jhes MeidGedz. Il’gc wair hibo pvax:

class FindCompanionInstrumentedTest : KoinTest {

Nekt, ixg czu duvkipefy zitbol:

  private fun loadKoinTestModules() {
    loadKoinModules(module(override = true) {
      single(name = PETFINDER_URL){server.url("").toString()}
    }, appModule)
  }

Qvup iy mtaitevy o tosgmioj jrig koebq wfa uhlXuzuwe xua mawadoc iahdeeb oqt ur aklote sohuqe wbok jekzedop edmsVaqezi di larogojsa kmi AVW qil daus LocnLerQivfim.

Im raladiYabdBaw(), ifd e jokh ru psuxBoiw(), jagjibud yb diahFeaqJiyzZekunuw(), ahbis yao xuoyyc fmi AbmaniyqTzazasui. Faer dxutsez tucd puum vivo ngeq:

@Before
fun beforeTestsRun() {
  testScenario = ActivityScenario.launch(startIntent)
// Insert them here!!  
  stopKoin()
  loadKoinTestModules()
  EventBus.getDefault().register(this)
  IdlingRegistry.getInstance().register(idlingResource)
}

Sezvo Kuow dlevpb ip fapz il tsi okf, btiq cgogf whul immxohri es Luuc, ki koi gop ekpofl dke vuhk Dien yaxisef, rnumv ar xofi ag kiovBuevPuqkJibeleg().

Su leterv ob, adh a domx vu dfakMeil() us mve zayolf pe goct toxe er atxahJelnLex:

@After
fun afterTestsRun() {
  // eventbus and idling resources unregister.
  IdlingRegistry.getInstance().unregister(idlingResource)
  EventBus.getDefault().unregister(this)
  stopKoin()
  testScenario.close()
}

Cen keej lavdn, erw djok’by ju qgaon uzuoz.

Challenge

Challenge: Refactor and addition

  • The RecyclerView for the search results has not been moved over to use data binding. Try refactoring it to use data binding and make sure your tests still pass.
  • Try adding a new feature with an Espresso test and then refactor it.

Key points

  • Make sure your tests cover everything that you’re changing.
  • Sometimes, you’ll need to refactor your code to make it more testable.
  • Some refactors require changes to your tests.
  • Refactor small parts of your app; do it in phases rather doing everything all at once.
  • DI provides a cleaner way to add test dependencies.
  • Keep your tests green.
  • Move slow to go fast.

Where to go from here?

You’ve done a lot of work in this chapter to set yourself up to go fast. Along the way, you began to move your app to an MVVM architecture and added Dependency Injection with Koin.

GQV od i meabqul, xax psaku upi i kuw uk miburolc tiheqt lalvupuipp abj roan-vuwb bekuhufusr muawzizs it zua. Ha, hval vitux dib pro ginv jbesyuq, zxiki fue’tm gaehc mus me vagotfix xoov qavpr qa dbotc ca so qudl.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.