Surviving Configuration Changes in Android

Learn how to survive configuration changes by handling your activities or fragment recreation the right way using either ViewModels, persistent storage, or doing it manually! By Beatrice Kinya.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Observing LiveData Changes

Open SearchFragment.kt and replace onSearchBtnClicked() with the following:

private fun onSearchBtnClicked() {
 with(searchBinding) {
   searchBtn.setOnClickListener {
     hideKeyboard(searchBinding.root)
     progressIndicator.visibility = View.VISIBLE
     bookViewModel.getBooks(searchTerm)
     // TODO 4
   }
 }
}

In the code above, when the user taps the search button, the app calls getBooks() to fetch the list of books from a remote API.

To register an observer on bookItems, replace // TODO 5 with the following:

private fun observeBooks() {
 bookViewModel.bookItems.observe(viewLifecycleOwner) { books ->
   with(searchBinding) {
     progressIndicator.visibility = View.GONE
     if (books != null) {
       booksAdapter = BooksAdapter(books = books)
       booksRecyclerView.adapter = booksAdapter
     }
   }
 }
}

observe() takes a lifecycle owner object. LifeCycleOwner is a class that has an Android lifecycle, like an activity or a fragment. Here, you passed viewLifeCycleOwner that represents the Fragment‘s View lifecycle.

To call the method you’ve added, replace // TODO 9 with the following:

observeBooks()

onCreateView() is the right place to observe LiveData objects because:

  • It ensures the app doesn’t make redundant calls from onResume() callback of an activity or a fragment.
  • It ensures the activity displays data as soon as it enters an active state.

Build and run. Enter a book title and tap the search button. The app will show a list of books related to the search terms:

A screen in portrait orientation showing list of  books returned from the remote API when user enters 'Rise of Magicks'.

When you rotate the app, it preserves the list of books:

A screen in landscape orientation showing list of  books returned from the remote API when user enters 'Rise of Magicks'.

Well done! You’ve learned how to save state in instance state bundles and ViewModel class. Your app UI state can survive configuration changes.

However, the app will lose all its data when the user completely leaves the app. You should use instance state or ViewModel class to save transient data. To persist data long after the user completely leaves the app, the Android framework provides persistent storage mechanisms such as databases or shared preferences. In the following sections, you’ll learn how to save search terms entered by the user in the app database.

Understanding Room Library

In Android, you can store structured data in an SQLite database using the Room library. There are three major components when working with Room:

  • Data Entities: Entity classes represent tables in the app database.
  • Data Access Objects: DAOs provide methods to query, insert, delete or update data in the app database.
  • Database class: This class holds the app database. It also defines database configurations.

Next, you’ll learn how you use them. Let’s start with entity classes.

Looking Into Data Entities

Open UserSearch.kt.

UserSearch is an entity class annotated with @Entity. Each attribute represents a column in the database table. The UserSearch entity has two columns: id and searchTerm. Optionally, you can provide the table name.

You’ve learned about the structure of an entity class. Next, you’ll learn about Data Access Objects (DAOs).

Understanding Data Access Objects

Open UserSearchDao.kt.

UserSearchDao is a DAO interface annotated with @Dao. You’ll add a method to save search terms to user_search table.

Replace // TODO 6 with the following:

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveSearchTerm(userSearch: UserSearch): Long

Add any missing imports by pressing Option-Enter on Mac or Alt-Enter on PC.

If you want the IDE to take care of missing imports the next time you copy/paste some code, you can enable auto-imports as follows:

  • For Windows or Linux, go to File and select Settings. Then, go to EditorGeneralAuto ImportKotlin. Change insert imports on paste to Always. Mark Add unambiguous imports on the fly option as checked .
  • For Mac, do the same thing in Android StudioPreferencesEditorGeneralAuto ImportKotlin.

saveSearchTerm() will create a row in user_search table. The method takes one parameter of type UserSearch. Parameters passed into @Insert method must either be an instance of an entity class annotated with @Entity or a list of entity class instances.

Now, you understand the working of DAO interfaces. Next, you’ll look at the database class.

Exploring Database Class

Open BookHubDatabase.kt.

BookHubDatabase is an abstract class that extends RoomDatabase. It is a database class. The class must be annotated with a @Database that includes an array that lists data entities associated with the database.

For each DAO class, you must define an abstract method that returns an instance of the DAO. See the following method in BookHubDatabase class:

abstract fun userSearchDao(): UserSearchDao

Now that you understand the different components of the Room library, you’ll implement methods that will call saveSearchTerm() to save search terms in the app database.

Saving a Search Term

Open BookRepositoryImpl.kt. Replace // TODO 7 with the following:

val userSearch = UserSearch(searchTerm = searchTerm)
dao.saveSearchTerm(userSearch)

The code above creates an instance of UserSearch. Then, it calls the DAO method to save the UserSearch instance.

Navigate to BookViewModel.kt and replace // TODO 8 with the following:

bookRepository.saveSearchTerm(searchTerm)

Here, you’re calling the saveSearchTerm() you implemented in BookRepository.

Open SearchFragment.kt and replace // TODO 4 with the following:

bookViewModel.saveSearchTerm(searchTerm)

From the code above, when the user taps the search button, the app calls the saveSearchTerm() you implemented in BookViewModel to save the search terms in the app database.

Build and run. Enter an author’s name and tap search. The app displays the lists of books:

A screen showing list of  books returned from the remote API when user enters ‘Brene Brown’.

In Android Studio, start app inspector. Select the running process in the drop-down. Expand book_hub_database and select user_search table. You’ll see the app saved the search terms you entered:

user_search table in BookHub app database showing a row with search term Brene Brown that the user entered.

Note: Ensure that your app is running on an emulator or connected device running API level 26 or higher to be able to use the app inspector feature of Android Studio.

You’ve learned how to save search terms entered by a user. Next, you’ll get the search terms from the database and show a user their search history.

Reading Data From the App Database

When you tap the Menu button — the three dots at the top of the screen — and select Search History, you’ll see a blank screen like this:

A blank screen with no search history.

You’ll populate this screen with the search terms you saved in the previous step.

Open UserSearchDao.kt. Replace // TODO 10 with the following:

@Query("SELECT * FROM user_searches")
suspend fun getUserSearches(): List<UserSearch>

If you see an import error, add the following import statement in the imports at the top of the file:

import androidx.room.Query

The code above gets all entries saved in the user_searches table and returns a list of UserSearch entities.

Next, you’ll add a method in BookRepositoryImpl class that will call getUserSearches() DAO method. Open BookRepositoryImpl.kt and replace return emptyList() //TODO 11 in getUserSearches() with the following:

return dao.getUserSearches()

In BookViewModel.kt, replace // TODO 12 with the following:

val searches = bookRepository.getUserSearches()
userSearches.postValue(searches)

The code above calls getUserSearches() repository method that returns a list of UserSearch entities. It then stores the list in userSearches LiveData object.

In SearchHistoryFragment.kt, replace // TODO 13 with the following:

private fun getSearchHistory() {
 bookViewModel.getUserSearches()
}

In getSearchHistory() method, you’re calling getUserSearches() method to get search terms saved in the app database.

To call the method you have implemented, add getSearchHistory() above observeSearchHistory() in the onCreateView() method:

getSearchHistory()

Build and run. Tap the Menu button and select Search History. You’ll see saved search terms:

A screen showing user serach history

When you tap an item in the search history, the app sends a request to the remote API to fetch the books. Then it shows the list of books returned. To achieve this, replace // TODO 14 with the following:

private fun observeBooks() {
 // 1
 bookViewModel.bookItems.observe(viewLifecycleOwner) { books ->
  with(historyBinding) {
   progressIndicatorHistory.visibility = View.GONE
   // 2
   goToMainScreen()
  }
 }
}

The code above:

  1. Listens to changes in the bookItems LiveData object.
  2. Navigates to main screen to display the list of books fetched from the remote API.

To call the method you’ve implemented, replace // TODO 15 with the following:

observeBooks()

Build and run. Navigate to the Search History screen. Tap an item in the search history list. The app will show a list of books fetched from the remote API:

A screen showing a list of books related to Brene Brown search term

You’ve learned how to save state across configuration changes and save data in persistent storage.

Sometimes, due to performance constraints, you may be unable to use any of the preferred mechanisms such as ViewModel or onSaveInstanceState(). If your app doesn’t require updating resources — taking advantage of automatic alternative resources handling — during a specific configuration change, you can prevent the app from restarting an activity when that change occurs.