Using SearchView

Now that FlickrFetchr supports searching, it is time to add a way for the user to enter a query and initiate a search. Do this by adding a SearchView.

As we said at the beginning of the chapter, SearchView is an action view, meaning your entire search interface can live in your application’s app bar.

Create a new menu XML file for PhotoGalleryFragment called res/menu/fragment_photo_gallery.xml. This file will specify the items that should appear in the toolbar. (See Chapter 14 for detailed steps on adding the menu XML file, if you need a reminder.)

Listing 26.6  Adding a menu XML file (res/menu/fragment_photo_gallery.xml)

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item android:id="@+id/menu_item_search"
          android:title="@string/search"
          app:actionViewClass="androidx.appcompat.widget.SearchView"
          app:showAsAction="ifRoom" />

    <item android:id="@+id/menu_item_clear"
          android:title="@string/clear_search"
          app:showAsAction="never" />
</menu>

You will see a couple of errors in the new XML complaining that you have not yet defined the strings you are referencing for the android:title attributes. Ignore those for now. You will fix them in a bit.

The first item entry in Listing 26.6 tells the toolbar to display a SearchView by specifying the value androidx.appcompat.widget.SearchView for the app:actionViewClass attribute. (Notice the usage of the app namespace for the showAsAction and actionViewClass attributes. Refer back to Chapter 14 if you are unsure of why this is used.)

The second item in Listing 26.6 will add a Clear Search option. This option will always display in the overflow menu because you set app:showAsAction to never. Later, you will configure this item so that, when pressed, the user’s stored query will be erased from the disk. For now, you can ignore this item.

Now it is time to address the errors in your menu XML. Open res/values/strings.xml and add the missing strings.

Listing 26.7  Adding search strings (res/values/strings.xml)

<resources>
    ...
    <string name="search">Search</string>
    <string name="clear_search">Clear Search</string>

</resources>

Finally, open PhotoGalleryFragment.kt. Add a call to setHasOptionsMenu(true) in onCreate(…) to register the fragment to receive menu callbacks. Override onCreateOptionsMenu(…) and inflate the menu XML file you created. This will add the items listed in your menu XML to the toolbar.

Listing 26.8  Overriding onCreateOptionsMenu(…) (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        retainInstance = true
        setHasOptionsMenu(true)
        ...
    }
    ...
    override fun onDestroy() {
        ...
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.fragment_photo_gallery, menu)
    }
    ...
}

Fire up PhotoGallery and see what the SearchView looks like. Pressing the search icon expands the view to display a text box where you can enter a query (Figure 26.2).

Figure 26.2  SearchView collapsed and expanded

SearchView collapsed and expanded

When the SearchView is expanded, an x icon appears on the right. Pressing the x one time clears out what you typed. Pressing the x again collapses the SearchView back to a single search icon.

If you try submitting a query, it will not do anything yet. Not to worry. You will make your SearchView more useful in just a moment.

Responding to SearchView user interactions

When the user submits a query, your app should execute a search against the Flickr web service and refresh the images the user sees with the search results. First, update PhotoGalleryViewModel to keep track of the user’s latest search term and update the search results when the query changes.

Listing 26.9  Storing the most recent query in PhotoGalleryViewModel (PhotoGalleryViewModel.kt)

class PhotoGalleryViewModel : ViewModel() {

    val galleryItemLiveData: LiveData<List<GalleryItem>>

    private val flickrFetchr = FlickrFetchr()
    private val mutableSearchTerm = MutableLiveData<String>()

    init {
        mutableSearchTerm.value = "planets"

        galleryItemLiveData = FlickrFetchr().searchPhotos("planets")
                Transformations.switchMap(mutableSearchTerm) { searchTerm ->
                    flickrFetchr.searchPhotos(searchTerm)
                }
    }

    fun fetchPhotos(query: String = "") {
        mutableSearchTerm.value = query
    }
}

Every time the search value changes, the gallery item list should change to reflect the new results. Since both the search term and gallery item lists are wrapped in LiveData, you use Transformations.switchMap(trigger: LiveData<X>, transformFunction: Function<X, LiveData<Y>>) to implement this relationship. (If you need to brush up on LiveData transformations, see Chapter 12.)

You also stash an instance of FlickrFetchr in a property. This way, you only create an instance of FlickrFetchr once in the lifetime of the ViewModel instance. Reusing the same FlickrFetchr instance avoids the unnecessary overhead of re-creating a Retrofit and FlickrApi instance every time the app performs a search. This means a speedier app for your user.

Next, update PhotoGalleryFragment to change PhotoGalleryViewModel’s search term value whenever the user submits a new query through the SearchView. Fortunately, the SearchView.OnQueryTextListener interface provides a way to receive a callback when a query is submitted.

Update onCreateOptionsMenu(…) to add a SearchView.OnQueryTextListener to your SearchView.

Listing 26.10  Logging SearchView.OnQueryTextListener events (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
         super.onCreateOptionsMenu(menu, inflater)
         inflater.inflate(R.menu.fragment_photo_gallery, menu)

         val searchItem: MenuItem = menu.findItem(R.id.menu_item_search)
         val searchView = searchItem.actionView as SearchView

         searchView.apply {

             setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                 override fun onQueryTextSubmit(queryText: String): Boolean {
                     Log.d(TAG, "QueryTextSubmit: $queryText")
                     photoGalleryViewModel.fetchPhotos(queryText)
                     return true
                 }

                 override fun onQueryTextChange(queryText: String): Boolean {
                     Log.d(TAG, "QueryTextChange: $queryText")
                     return false
                 }
             })
         }
     }
     ...
}

When importing SearchView, select the androidx.appcompat.widget.SearchView option from the choices presented.

In onCreateOptionsMenu(…), you pull the MenuItem representing the search box from the menu and store it in searchItem. Then you pull the SearchView object from searchItem using getActionView().

Once you have a reference to the SearchView, you are able to set a SearchView.OnQueryTextListener using setOnQueryTextListener(…). You must override two functions in the SearchView.OnQueryTextListener implementation: onQueryTextSubmit(String) and onQueryTextChange(String).

The onQueryTextChange(String) callback is executed any time text in the SearchView text box changes. This means that it is called every time a single character changes. You will not do anything inside this callback for this app except log the input string and return false. Returning false indicates to the system that your callback override did not handle the text change. This cues the system to perform SearchView’s default action (which is to show relevant suggestions, if available).

The onQueryTextSubmit(String) callback is executed when the user submits a query. The query the user submitted is passed as input. Returning true signifies to the system that the search request has been handled. This callback is where you call into PhotoGalleryViewModel to trigger the photo download for your search query.

Run your app and submit a query. You should see log statements reflecting the execution of your SearchView.OnQueryTextListener callback functions. You should also see the images displayed change based on the search term you enter (Figure 26.3).

Figure 26.3  Working SearchView

Working SearchView

Note that if you use the hardware keyboard to submit your search query on an emulator (versus the emulator’s onscreen keyboard), you may see the search executed two times, one after the other. This is because there is a small bug in SearchView. You can ignore this behavior because it is a side effect of using the emulator and will not affect your app when it runs on a real Android device.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.88.249