Open menu with table of contents Android Assignment 03
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Android Development

Android Assignment 03

Introduction to Fragments

Stuttgart Media University

Please conduct the following tasks alone. For implementation details you can refer to the lecture slides or the Android developer website. Please do not hesitate to ask me or the tutor if you have any questions.

1 Tasks

  • Reuse your assignment2 code and copy it to assignment3 folder.
  • Goal is to use Fragment or Fragments as your view holder for your Activities. You can move your Home and Search Views into one or two Fragment(s). Hint: It´s easier to have seperate Fragments with own Views for Home and Search.
  • MainActivity RecyclerView should be empty on app start.
  • Add Button to MainActivity above RecyclerView with id "searchBtn"
  • When the button is pressed, open new Activity "SearchActivity"
  • Add the following elements to SearchActivity
    • EditText with id "searchMovieTitleEditText"
    • Button with id "searchMovieTitleButton"
    • RecyclerView with id "searchRecyclerView"
  • Reuse the model from Assignment2
    • When movie title is entered in searchMovieTitleEditText and buttons searchMovieTitleButton is clicked, show search results in searchRecyclerView. Use "contains" to look for movie title. Example: Search string is "uss", show only "From Russia with Love"
  • When list row in search is clicked, save this movie, close SearchActivity (use finish() call) and show all saved movies in MainActivity homeRecyclerView. Order MainActivity search results by time added. First clicked results are on top of list.
  • Keep homeRecyclerView row click from assignment2. When row is clicked, remove movie from search result

2 Hint

To pass data between activities, use a Singleton pattern. Example implementation of MovieDb class to access search result list

package de.hdmstuttgart.movietracker

class MovieDb private constructor() {
    var savedMovies: List<MovieViewModel> = ArrayList()

    companion object {

        @Volatile
        private var instance: MovieDb? = null

        fun getInstance() =
            instance ?: synchronized(this) {
                instance ?: MovieDb().also { instance = it }
            }
    }
}

Call MovieDb.getInstance().savedMovies.xxx to access instance of saved movies

3 Views

3.1 MainActivity

On app start

With single saved movie

With multipe saved moviews

3.2 SearchActivity

On activity start

With search result

4 Movie Data

Use this data to fill out your rows:

title year actor
Dr. No 1962 Sean Connery
From Russia with Love 1963 Sean Connery
Goldfinger 1964 Sean Connery
Thunderball 1965 Sean Connery
You Only Live Twice 1967 Sean Connery

5 Espresso Test

package de.hdmstuttgart.movietracker

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.core.internal.deps.guava.base.Preconditions
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.LargeTest
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@LargeTest
@RunWith(androidx.test.ext.junit.runners.AndroidJUnit4::class)
class Assignment3Test {

  @get:Rule
  var activityScenarioRule = ActivityScenarioRule(
    MainActivity::class.java
  )

  @Test
  fun assignment3Test() {
    onView(withId(R.id.searchBtn))
      .perform(click())
    onView(withId(R.id.searchMovieTitleEditText))
      .perform(replaceText("Gold"), closeSoftKeyboard())
    onView(withId(R.id.searchMovieTitleButton))
      .perform(click())
    onView(withId(R.id.searchRecyclerView))
      .check(matches(atPosition(0, hasDescendant(withText("Goldfinger")))))
    onView(withId(R.id.searchRecyclerView))
      .perform(
        RecyclerViewActions.actionOnItemAtPosition<MovieAdapter.ViewHolder>(
          0,
          click()
        )
      )
    onView(withId(R.id.homeRecyclerView))
      .check(matches(atPosition(0, hasDescendant(withText("Goldfinger")))))
    onView(withId(R.id.searchBtn))
      .perform(click())
    onView(withId(R.id.searchMovieTitleEditText))
      .perform(replaceText("uss"), closeSoftKeyboard())
    onView(withId(R.id.searchMovieTitleButton))
      .perform(click())
    onView(withId(R.id.searchRecyclerView))
      .check(matches(atPosition(0, hasDescendant(withText("From Russia with Love")))))
    onView(withId(R.id.searchRecyclerView))
      .perform(
        RecyclerViewActions.actionOnItemAtPosition<MovieAdapter.ViewHolder>(
          0,
          click()
        )
      )
    onView(withId(R.id.homeRecyclerView))
      .check(matches(atPosition(1, hasDescendant(withText("From Russia with Love")))))
    onView(withId(R.id.homeRecyclerView))
      .perform(
        RecyclerViewActions.actionOnItemAtPosition<MovieAdapter.ViewHolder>(
          0,
          click()
        )
      )
    onView(withId(R.id.homeRecyclerView))
      .check(matches(atPosition(0, hasDescendant(withText("From Russia with Love")))))
  }

  companion object {
    fun atPosition(
      position: Int,
      itemMatcher: Matcher<View?>
    ): BoundedMatcher<View?, RecyclerView> {
      Preconditions.checkNotNull(itemMatcher)
      return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
        override fun describeTo(description: Description) {
          description.appendText("has item at position $position: ")
          itemMatcher.describeTo(description)
        }

        override fun matchesSafely(view: RecyclerView): Boolean {
          val viewHolder = view.findViewHolderForAdapterPosition(position)
            ?: // has no item on such position
            return false
          return itemMatcher.matches(viewHolder.itemView)
        }
      }
    }
  }
}

6 Gradle Dependencies

dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.9.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    implementation("androidx.recyclerview:recyclerview:1.3.1")
    implementation("androidx.test.espresso:espresso-contrib:3.5.1")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}