6

Surviving Configuration Changes in Android [FREE]

 1 year ago
source link: https://www.raywenderlich.com/33044382-surviving-configuration-changes-in-android
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Home Android & Kotlin Tutorials

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 Jun 27 2022 · Article (20 mins) · Intermediate

Version

SurvivingConfigurationChangesInAndroid-feature.png

An Android device consistently changes configurations. This could be a screen orientation change, keyboard availability changes or a user switching to multi-window mode. During a configuration change, Android recreates existing activities to reload resources for the new configuration. To properly handle restarting an activity, it is important to restore the activity to its previous state.

In this tutorial, you’ll build BookHub App. This app allows users to search for books using the author’s name or the book title. Along the way, you’ll learn about:

  • Saving and restoring activity state using instance state bundles.
  • Using ViewModel to store UI state.
  • Saving data in local persistent storage.
  • Manually handling configuration changes.
Note: This tutorial assumes you’re familiar with the activity lifecycle in Android. To learn about the activity lifecycle, check out the Android Activity lifecycle tutorial. If you’re completely new to Kotlin or Android development, check out Kotlin for Android: An Introduction and Android Development Series for beginners tutorials before you start.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Open Android Studio and import the starter project.

Build and run the project. You’ll see this screen:

A screen showing an input field, button and text field to show the number of books fetched from remote API.

The screen has an input field for entering an author’s name or a book title to search for books. It also has a search button with a magnifying glass icon and a label to show the number of books returned from a remote API, now showing no results.

Enter an author’s name and tap the search button. The app updates the result label with the number of books returned from the API:

A screen with text field updated with the number of books returned from the remote API.

Rotate the app, though, and you’ll see a different number:

A screen in landscape orientation

Whoops! The books count got lost. When you rotated the screen, the app lost the count data because it recreated the activity to adapt to the new orientation. Your mission in this tutorial is to liven up the app while persisting data across configuration changes.

Check out the project structure:

project structure.

The project has the following packages:

  • data: This contains logic for accessing data sources such as the app database or remote APIs.
  • ui: The ui package has SearchFragment that holds an input field for entering the author’s name or book title and SearchHistoryFragment for showing the search terms the user enters. It also has BookViewModel that holds UI-related data. Fragment classes are responsible for displaying data to the user.
  • repository: Its classes receive user actions like tapping a button. Then, they forward user requests to the data layer. The repository classes also receive data from the data layer — for instance, the list of books received from the remote API. Then, they forward the data received to the UI layer.

You’ll work across various classes in these packages as you build BookHub App.

Grab your coffee! It’s gonna be amazing.

Saving UI State in Instance State Bundles

When the system recreates the UI, you can save the UI state in an instance state bundle using onSaveInstanceState() lifecycle callback.

Instance state is a collection of key-value pairs stored in a Bundle object. It’s the data the app uses to restore UI controllers like an activity or a fragment to their previous state.

You’ll implement onSaveInstanceState() callback to save the value of bookscount when a user rotates the screen.

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

override fun onSaveInstanceState(outState: Bundle) {
 super.onSaveInstanceState(outState)
 outState.putInt(bookCountKey, booksCount)
}

Here, you’re storing the value of booksCount in a Bundle object using bookCountKey key. The app calls onSaveInstanceState() immediately before calling onPause() to save the UI state.

To restore the saved state, you’ll fetch the stored value in the instance state bundle.

Replace // TODO 2 with this:

private fun restoreCount(savedInstanceState: Bundle?) {
 // 1
 booksCount = savedInstanceState?.getInt(bookCountKey) ?: 0
 // 2
 searchBinding.bookCountTextView.text = booksCount.toString()
}

Here’s what the code above does:

  1. Retrieves the value you stored in the instance state bundle using bookCountKey key and assigns it to booksCount.
  2. Restores the count pulled from saved state back to bookCountTextView.

To call the method that retrieves the value stored in the state bundles, add this just above setTextChangedListener() in onCreateView():

restoreCount(savedInstanceState)

Build and run. Enter your favorite book title and tap the search button. You’ll see the following screen:

A screen showing number of books returned when user searches alchemist in portrait orientation.

When you rotate the screen, the app retains the book count:

A screen showing number of books returned when user searches Nora Roberts in landscape orientation.

Congratulations! :]

onSaveInstanceState() serializes data to the disk. Serialization consumes a lot of memory when serializing large data sets or complex data structures such as bitmaps. This process happens in the main thread. Long-running serialization can cause UI jank during a configuration change. Therefore, you should store only primitives like Integer and small, simple objects like String in instance state bundles.

Note: Starting from Android 7.0, if you put too much data on a bundle you can encounter a TransactionTooLargeException. This is another good reason to not process big data on a bundle.

BookHub App fetches a list of books from a remote API. How do you save the list of books to prevent the app from making another API call to fetch the books in case of a configuration change? In the following section, you’ll learn how to store data in a ViewModel to address this.

Using ViewModel to Store UI State

The Android team introduced ViewModel and LiveData classes to help save state during configuration changes.

A ViewModel stores and manages UI-related data in a lifecycle-conscious manner. Simply put, it allows data to survive configuration changes. A ViewModel remains in the memory until, the Lifecycle it is scoped to, goes away completely. For an activity, this means when it finishes; for a fragment when it’s when it’s detached.

The diagram below shows the lifetime of a ViewModel next to the lifecycle of the activity it’s associated with:

ViewModel lifetime in relation to lifecycle of an activity is is scopped in

LiveData is an observable data-holder class. It’s lifecycle aware. It only notifies UI components that are in an active state when data changes.

Open BookViewModel.kt.

BookViewModel class extends ViewModel. It has three methods:

  • getBooks() to fetch books from a remote API.
  • saveSearchTerm() that saves search terms entered by the user in the app database.
  • getUserSearches() to retrieve search terms saved in the app database.

To add a LiveData object that saves the list of books fetched from the remote API, replace // TODO 3 with the following:

private val items = MutableLiveData<List<Item>?>()
val bookItems get() = items

Here, you’ve added a LiveData object that will store a list of books received from a remote API.

To store books returned from the API in items, your LiveData object, replace the getBooks() method with the following:

fun getBooks(searchTerm: String) {
 viewModelScope.launch(Dispatchers.IO) {
   // 1
   val booksInfo = bookRepository.getBookInfo(searchTerm)
   val books = booksInfo?.items
   // 2
   items.postValue(books)
 }
}

Here’s what’s happening in the code above:

  1. Calls getBookInfo() to fetch books from a remote API. The method returns a list of books.
  2. Stores the list of books received from the API in items.

To show the list of books in the UI, you’ll add an observer for bookItems.

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.

Managing Configuration Changes Yourself

The <activity> element in AndroidManifest.xml has an optional attribute named android:configChanges. This attribute allows developers to lists configuration changes that the activity will handle itself in code.

The most commonly used values are:

  • orientation: To prevent an activity from restarting when the screen orientation changes.
  • screenSize: It also prevents activity restarts when orientation changes, but only for Android 3.2 and above.
  • screenLayout: This is necessary to detect changes that can be triggered by devices such as foldable phones and convertible Chromebooks.
  • keyboardHidden: It prevents activity from restarting when the keyboard availability changes.

You’ll declare MainActivity to handle configuration when screen orientation changes.

Open AndroidManifest.xml. In the <activity> element, add the following attribute:

android:configChanges="orientation|screenSize"

The <activity> element will look like the code below:

<activity
 android:name=".ui.MainActivity"
 android:configChanges="orientation|screenSize"
 android:exported="true"
 android:theme="@style/SplashTheme">
 <intent-filter>
   <action android:name="android.intent.action.MAIN" />
   <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
</activity>

When you declare the activity to handle configuration changes, the app does not restart the activity. Instead, it calls onConfigurationChanged() callback to determine resources required for the new configuration. For instance, the Search button’s color changes to pink when in landscape orientation and to orange when in portrait orientation. You’ll override onConfigurationChanged() to implement these changes.

In SearchFragment.kt, replace // TODO 16 with the following:

override fun onConfigurationChanged(newConfig: Configuration) {
  super.onConfigurationChanged(newConfig)
  context?.let {
    val colorPink = ContextCompat.getColor(it, R.color.pink)
    val colorOrange = ContextCompat.getColor(it, R.color.colorAccent)
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
      searchBinding.searchBtn.setBackgroundColor(colorPink)
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
      searchBinding.searchBtn.setBackgroundColor(colorOrange)
    }
  }
}

You may see errors due to missing imports. Add the following import statement in the imports at the top of the file:

import android.content.res.Configuration

In onConfigurationChanged() method above, you’re checking the user’s phone orientation and applying the correct color to the search button.

Build and run. When in portrait orientation, the button color is orange:

A screen in portrait orientation showing list of  books returned from the remote API when user enters ’Sheryl SandBerg’. The Search button is orange.

Rotate the phone to a landscape orientation. The search button’s color changes to pink:

A screen in landscape orientation showing list of  books returned from the remote API when user enters ’Sheryl SandBerg’ The Search button is pink

Congratulations! You’ve created the BookHub App. You’ve also learned different ways of managing configuration changes for your Android apps.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

To learn more about saving state, check out Jetpack Saved State for ViewModel tutorial. To learn more about saving data on Android, check out the book Saving Data on Android. The book covers saving data both locally — in your app — and remotely. From the book, you’ll learn about modern synchronization mechanisms to keep your app always up to date as well. To learn more about architecting your app check the guide to app architecture.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK