Simple Persistence with SharedPreferences

In your app, there will only be one active query at a time. PhotoGalleryViewModel persists the query for the user’s perceived life of the fragment. But the query should also be persisted between restarts of the app (even if the user turns off the device).

You will achieve this using shared preferences. Any time the user submits a query, you will write the search term to shared preferences, overwriting whatever query was there before. When the application first launches, you will pull the stored query from shared preferences and use it to execute a Flickr search.

Shared preferences are files on the filesystem that you read and edit using the SharedPreferences class. An instance of SharedPreferences acts like a key-value store, much like Bundle, except that it is backed by persistent storage. The keys are strings, and the values are atomic data types. If you look at them, you will see that the files are simple XML, but SharedPreferences makes it easy to ignore that implementation detail.

By the way, shared preferences files are stored in your application’s sandbox, so you should not store sensitive information (like passwords) there.

Add a new file named QueryPreferences.kt to serve as a convenient interface for reading and writing the query in the shared preferences.

Listing 26.11  Adding an object to manage a stored query (QueryPreferences.kt)

private const val PREF_SEARCH_QUERY = "searchQuery"

object QueryPreferences {

    fun getStoredQuery(context: Context): String {
        val prefs = PreferenceManager.getDefaultSharedPreferences(context)
        return prefs.getString(PREF_SEARCH_QUERY, "")!!
    }

    fun setStoredQuery(context: Context, query: String) {
        PreferenceManager.getDefaultSharedPreferences(context)
                .edit()
                .putString(PREF_SEARCH_QUERY, query)
                .apply()
    }
}

Your app only ever needs one instance of QueryPreferences, which can be shared across all other components. Because of this, you use the object keyword (instead of class) to specify that QueryPreferences is a singleton. This enforces your intention that only one instance will be created in your app. It also allows you to access the functions in the object using ClassName.functionName(…) syntax (as you will see shortly).

PREF_SEARCH_QUERY is used as the key for the query preference. You will use this key any time you read or write the query value.

PreferenceManager.getDefaultSharedPreferences(Context) returns an instance with a default name and private permissions (so that the preferences are only available from within your application). To get a specific instance of SharedPreferences, you can use the Context.getSharedPreferences(String, Int) function. However, in practice, you will often not care too much about the specific instance, just that it is shared across the entire app.

The getStoredQuery(Context) function returns the query value stored in shared preferences. It does so by first acquiring the default SharedPreferences for the given context. (Because QueryPreferences does not have a Context of its own, the calling component will have to pass its context as input.)

Getting a value you previously stored is as simple as calling SharedPreferences.getString(…), SharedPreferences.getInt(…), or whichever function is appropriate for your data type. The second input to SharedPreferences.getString(String, String) specifies the default return value that should be used if there is no entry for the PREF_SEARCH_QUERY key.

The return type for SharedPreferences.getString(…) is defined as a nullable String because the compiler cannot guarantee that the value associated with PREF_SEARCH_QUERY exists and that it is not null. But you know that you never store a null value for PREF_SEARCH_QUERY – and you supply an empty String as the default value in cases where setStoredQuery(context: Context, query: String) has not yet been called. So it is safe to use the non-null assertion operator (!!) here without a try/catch block around it.

The setStoredQuery(Context) function writes the input query to the default shared preferences for the given context. In QueryPreferences, you call SharedPreferences.edit() to get an instance of SharedPreferences.Editor. This is the class you use to stash values in your SharedPreferences. It allows you to group sets of changes together in transactions, much like you do with FragmentTransaction. If you have a lot of changes, this will allow you to group them together into a single storage write operation.

Once you are done making all of your changes, you call apply() on your editor to make them visible to other users of the SharedPreferences file. The apply() function makes the change in memory immediately and then does the actual file writing on a background thread.

QueryPreferences is your entire persistence engine for PhotoGallery.

Now that you have a way to easily store and access the user’s most recent query, update PhotoGalleryViewModel to read and write the query from shared preferences as necessary. Read the query when the ViewModel is first created and use the value to initialize mutableSearchTerm. Write the query whenever mutableSearchTerm is changed.

Listing 26.12  Persisting query in shared preferences (PhotoGalleryViewModel.kt)

class PhotoGalleryViewModel : ViewModel() {
class PhotoGalleryViewModel(private val app: Application) : AndroidViewModel(app) {
    ...
    init {
        mutableSearchTerm.value = "planets"QueryPreferences.getStoredQuery(app)
        ...
    }

    fun fetchPhotos(query: String = "") {
        QueryPreferences.setStoredQuery(app, query)
        mutableSearchTerm.value = query
    }
}

Your ViewModel needs a context to use the QueryPreferences functions. Changing the parent class of PhotoGalleryViewModel from ViewModel to AndroidViewModel grants PhotoGalleryViewModel access to the application context. It is safe for PhotoGalleryViewModel to have a reference to the application context, because the app context outlives the PhotoGalleryViewModel.

Next, clear the stored query (set it to "") when the user selects the Clear Search item from the overflow menu.

Listing 26.13  Clearing a stored query (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        ...
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.menu_item_clear -> {
                photoGalleryViewModel.fetchPhotos("")
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
    ...
}

Last, but not least, update PhotoGalleryViewModel to fetch interesting photos when the query is cleared, rather than searching based on an empty search term.

Listing 26.14  Fetching interesting photos when the query is blank (PhotoGalleryViewModel.kt)

class PhotoGalleryViewModel(private val app: Application) : AndroidViewModel(app) {
    ...
    init {

        mutableSearchTerm.value = QueryPreferences.getStoredQuery(app)

        galleryItemLiveData =
                Transformations.switchMap(mutableSearchTerm) { searchTerm ->
                    if (searchTerm.isBlank()) {
                        flickrFetchr.fetchPhotos()
                    } else {
                        flickrFetchr.searchPhotos(searchTerm)
                    }
                }
    }
    ...
}

Search should now work like a charm. Run PhotoGallery and try searching for something fun like “unicycle.” See what results you get. Then fully exit out of the app using the Back button. Heck, even reboot your phone. When you relaunch your app, you should see the results for the same search term.

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

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