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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Surviving Configuration Changes in Android
20 mins
- Getting Started
- Saving UI State in Instance State Bundles
- Using ViewModel to Store UI State
- Observing LiveData Changes
- Understanding Room Library
- Looking Into Data Entities
- Understanding Data Access Objects
- Exploring Database Class
- Saving a Search Term
- Reading Data From the App Database
- Managing Configuration Changes Yourself
- Where to Go From Here?
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:
When you rotate the app, it preserves the list of books:
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 Editor ▸ General ▸ Auto Import ▸ Kotlin. 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 Studio ▸ Preferences ▸ Editor ▸ General ▸ Auto Import ▸ Kotlin.
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:
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:
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:
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:
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:
- Listens to changes in the
bookItems
LiveData
object. - 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:
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.