← All work
Fueled · iFIT
iFIT Android Tablet App
Built dashboard sections and a reusable test library for a connected-fitness app.
- Role
- Senior Android Developer
- When
- 2021 – 2022
- Platforms
- Android, Tablet
- Industry
- Fitness
- Release
- Released on Google Play.
About the app
iFIT is a connected-fitness platform with trainer-led workouts. I worked on the dashboard of its Android tablet app, where members find featured workouts, browse categories, join challenges and resume a paused workout.
What I built
- Dashboard sections for featured workouts, workout categories, challenges, and resuming a
paused workout — built with Airbnb’s Epoxy on top of nested RecyclerViews, so each section is a
model and adding or reordering one is a small change in
buildModels(). - A reusable Espresso library for testing RecyclerView items, at iFIT’s request, so other teams could test their list screens the same way. Espresso’s built-in actions can act on a whole item but not easily on one view inside it, or on a list inside a list. I wrote custom Matchers and ViewActions that find a view inside the item at a given position, reach into a nested inner list, and act on a single child view.
How a matcher reaches into a list inside a list
I no longer have the iFIT code, so this is the approach rewritten as a sketch. A custom Matcher
finds one view inside an inner list’s item, inside an outer list’s item:
fun nestedItemView(
@IdRes outerListId: Int,
outerPosition: Int,
@IdRes innerListId: Int,
innerPosition: Int,
@IdRes targetViewId: Int,
): Matcher<View> = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText(
"view $targetViewId in inner item $innerPosition of outer item $outerPosition",
)
}
override fun matchesSafely(view: View): Boolean {
val outerList = view.rootView.findViewById<RecyclerView>(outerListId) ?: return false
val outerItem = outerList.findViewHolderForAdapterPosition(outerPosition)?.itemView
?: return false
val innerList = outerItem.findViewById<RecyclerView>(innerListId) ?: return false
val innerItem = innerList.findViewHolderForAdapterPosition(innerPosition)?.itemView
?: return false
return view === innerItem.findViewById<View>(targetViewId)
}
}
A view holder exists only while its item is laid out, so the test scrolls the outer list first:
onView(withId(R.id.dashboard))
.perform(scrollToPosition<RecyclerView.ViewHolder>(2))
onView(nestedItemView(R.id.dashboard, 2, R.id.workouts, 0, R.id.title))
.check(matches(withText("Morning Run")))
.perform(click())