8.
SharedPreferences & ViewModels
Written by Darryl Bayliss
In the previous chapter, you set up a RecyclerView. In this chapter, you’ll update Listmaker to create, save, and delete lists. You’ll also learn about two new topics. SharedPreferences and ViewModels. SharedPreferences are a simple way to save data in your app, whilst ViewModels provide a way to manage data shown on screen in a way that respects the lifecycle of your app.
By the end of the chapter, you’ll know:
- What SharedPreferences are.
- How to use SharedPreferences to save and retrieve objects.
- What ViewModels are and how to use them in your apps.
Getting started
If you’re following along with your own project, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the Listmaker app inside the starter folder.
The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.
With the Listmaker project open in Android Studio, run the project using a device or emulator.
In this chapter, you want to begin creating lists. A good way to do that is by providing a button for your users. You’re going to add a particular button called a Floating Action Button, better known as a FAB. You use a FAB to highlight an important action on the screen, it’s part of a design language called Material Design. Don’t worry if you don’t know what this is, you’ll learn more about Material Design in chapter 12.
Open main_activity.xml, and in the Palette window, select Buttons.
Click and drag a FloatingActionButton onto the layout. A new window will appear, asking you to pick a Resource. A resource, in this case, is the image you want to show on your button.
In the search textfield along the top, type ic_menu_add. As you type, the resources will filter the images with the name in the textfield. One image will be left.
Click the image, then click OK in the bottom right. The FAB will appear in the layout.
It’s going to be hard for users to reach that button in the top left corner, so let’s move it to the bottom right of the screen. In the Attributes window, scroll all the way down to layout_gravity field and select the bottom and right checkboxes. Finally, set the layout_marginBottom and layout_marginRight to 8dp. This gives the button some space away from the edge of the screen.
Finally, change the id of the FAB to fabButton and you have your FAB all setup. In the next section, you’ll put it to use.
Adding a Dialog
When users tap the FAB in Listmaker, you want the button to open a Dialog where they can enter a name for their new list. A dialog is a small window that appears over the screen, to inform the user about something and maybe even prompt them for information. Your Dialog will contain labels to prompt users for information.
Rather than hard-coding these prompt strings, you’ll add these strings to strings.xml. This keeps the strings for Listmaker in one place, making it easier to update the strings or to support another language in the future.
Open strings.xml and add the following strings:
<string name="name_of_list">What is the name of your list?</string>
<string name="create_list">Create</string>
Next, open MainActivity.kt. At the bottom of the file. Add a method to create an AlertDialog to get the name of the list from the user:
private fun showCreateListDialog() {
// 1
val dialogTitle = getString(R.string.name_of_list)
val positiveButtonTitle = getString(R.string.create_list)
// 2
val builder = AlertDialog.Builder(this)
val listTitleEditText = EditText(this)
listTitleEditText.inputType = InputType.TYPE_CLASS_TEXT
builder.setTitle(dialogTitle)
builder.setView(listTitleEditText)
// 3
builder.setPositiveButton(positiveButtonTitle) { dialog, _ ->
dialog.dismiss()
}
// 4
builder.create().show()
}
With this method, you:
-
Retrieve the strings you defined in strings.xml for use in the Dialog.
-
Create an
AlertDialog.Builderto help construct the Dialog. AnEditTextView is created as well to serve as the input field for the user to enter the name of the list.The
inputTypeof theEditTextis set toTYPE_CLASS_TEXT. Specifying the input type gives Android a hint as to what the most appropriate keyboard to show is. In this case, a text-based keyboard, since you want the list to have a name.The title of the Dialog is set by calling
setTitle. You also set the content View of the Dialog. In this case theEditTextView, by callingsetView. -
Add a positive button to the Dialog; this tells the Dialog a positive action has occurred and something should happen.
You pass in
positiveButtonTitleas the label for the button and implement anonClickListener. For now, you dismiss the Dialog. You’ll handle the resulting actions behind the button in the next section. -
Finally, you instruct the Dialog Builder to create the Dialog and display it on the screen.
Now that you have code to show the Dialog, you need to call it when the user taps the FAB. First, you need to acquire the binding for the Activity, then set setOnClickListener of the FAB so it knows what to do inside onCreate. At the top of MainActivity.kt, add a property to store the binding:
private lateinit var binding: MainActivityBinding
Then in onCreate, setup the binding and the onClickListener:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment.newInstance())
.commitNow()
}
binding.fabButton.setOnClickListener {
showCreateListDialog()
}
}
Run the app and tap on the FAB in the bottom-right of the screen. You’ll see the Create List Dialog appear as expected.
Try typing in a name for the list, then click Create. Nothing will happen, but that’s okay. Next, you need to add code to handle the creation of the list inside the onClickListener when the positive button is tapped for the Dialog.
You’ll do this later on in the chapter. For now, though, you’ve done everything you can here. Your next task is to define what a list looks like in Listmaker. You’ll do that in the next part.
Creating a list
Start by creating a new package in your project. This package will hold your data models for the app.
In the Project navigator, Right-click com.raywenderlich.listmaker. In the options that appear, select New ▸ Package:
A floating textfield will appear with the app package added. Type in models and with press enter. Your new package will appear in the project on the left.
Next, Right-click com.raywenderlich.listmaker again. In the options that appear, select New ▸ Kotlin Class/File.
A popup window will appear again, this time with different options. Name the new Kotlin file TaskList, then change the kind to Class and press enter.
Android Studio creates and displays the new class. Next, add a primary constructor to TaskList.kt so it can be given a name and a list of associated tasks:
class TaskList(val name: String, val tasks: ArrayList<String> = ArrayList()) {
}
Next, you need a way to save the list to the device. You can do this by using SharedPreferences.
SharedPreferences allows you to save key-value pairs to a device, that you can retrieve later. If you need a way to save small sets of data in your app quickly, you should consider using SharedPreferences.
Behind the scenes, SharedPreferences writes key-value pairs to a single file. You can configure it to write to multiple files for more complex apps. You can also allow other apps to access your apps’ SharedPreferences store if you think other apps have a valid reason to access your data.
Note: SharedPreferences is a quick way to persist and retrieve data. However, it isn’t perfect.
SharedPreferences only supports saving simple properties and a hashset. This chapter uses the hashset to store lists, which doesn’t store the lists in order. You may notice your lists appearing in different orders as you use the app.
There are better alternatives to SharedPreferences when you have complex data needs, which you’ll learn about in later chapters.
To use SharedPreferences, you need to add a dependency to your project. A dependency is code that helps with a particular problem and saves you the time and effort from having to write the code yourself.
In the project navigator, open the build.gradle (Module: Listmaker.app) file. Take a moment to look at the dependencies block.
These are the dependencies Listmaker is using. You may notice ConstraintLayout is listed as a dependency, and a few others you may not understand.
That’s ok, what each of them is doesn’t matter yet. Just know that they are helping you build your app. For now, in the dependencies block, add the following line to inform your app you want to use the preference library:
implementation 'androidx.preference:preference-ktx:1.1.1'
At the top of the file, a message will appear. Informing you the file has changed.
Click the Sync Now button to the right. Android Studio will begin to download the dependency and make sure it’s available for your app to use.
With SharedPreferences setup, it’s time to use it. You need a class to manage the lists Listmaker creates. Fortunately, there’s already one available to use. In the form of a ViewModel.
ViewModels
ViewModels in Android serve one purpose. To manage the data that’s shown on screen in a way that respects the lifecycle of your app. What does that mean?
If you recall in Chapter 4: Debugging, one of the bugs TimeFighter had was the score and timer resetting to 0 when the device rotated. The reason for this is the Activity was recreated when a rotation happened, causing the data stored in that Activity to be lost. This is normal behavior for Android, and your Activity may be recreated for a range of reasons. A change in the device language for example.
This isn’t ideal for developers though, you need that data to be able to keep the screen consistent for your users. You could use savedInstanceState like in TimeFighter, but it’s only suitable for storing simple values. Things get more difficult when you want to retain entire objects. Fortunately, the engineers at Google have provided a solution called ViewModels.
With ViewModels, your data is kept separate from the Activity. This is good because if the Activity ever had to recreate itself, the data still exists somewhere and can be reused by the Activity. No data loss worries here!
ViewModels also make it easier to share data between screens. Especially screens built using Fragments. You’ll see this in action in the next few chapters.
For now, let’s setup your ViewModel. In Android Studio, open MainViewModel.kt. Update it to look like the following:
// 1
class MainViewModel(private val sharedPreferences: SharedPreferences) : ViewModel() {
// 2
lateinit var onListAdded: (() -> Unit)
// 3
val lists: MutableList<TaskList> by lazy {
retrieveLists()
}
// 4
private fun retrieveLists(): MutableList<TaskList> {
val sharedPreferencesContents = sharedPreferences.all
val taskLists = ArrayList<TaskList>()
for (taskList in sharedPreferencesContents) {
val itemsHashSet = ArrayList(taskList.value as HashSet<String>)
val list = TaskList(taskList.key, itemsHashSet)
taskLists.add(list)
}
return taskLists
}
// 5
fun saveList(list: TaskList) {
sharedPreferences.edit().putStringSet(list.name, list.tasks.toHashSet()).apply()
lists.add(list)
onListAdded.invoke()
}
}
Here’s what’s going on, step by step:
-
You updated the constructor to store a
SharedPreferencesproperty. This allows you to write key-value pairs to SharedPreferences. -
You add a lambda called
onListAdded. Used to inform other interested classes when a list is added to the app. -
Add a property called
lists, that is lazily created. What this means is until you call the property, the property is empty. Once you call the property, the property will be populated by callingretrieveLists(). This is a handy way to avoid querying for unnecessary data until you need it. -
The
retrieveLists()method gets all the savedTaskLists from SharedPreferences. Looping through them and recreating TaskList objects from the HashSets. Since SharedPreferences can only store simple items and sets, you will need to use sets to save a list of items. -
The
saveList()method takes aTaskListparameter, which is saved to sharedPreferences as a set of Strings. You use the name of the list as the key and convert the tasks inTaskListto a HashSet to use as a value. Since HashSet is a Set, it ensures unique values in the list. It also updates the list’s property, making sure it is kept up to date with the latest data. Finally, you invoke the onListAdded lambda to let interested classes know about the new list.
With the ViewModel ready, the next thing to do is to use it in your UI classes.
Hooking up the UI to the ViewModel
Open MainActivity.kt and initialize a property to hold the ViewModel:
private lateinit var viewModel: MainViewModel
Next, update the Activity onCreate to create the ViewModel. This code can be placed at the very top of the method, after super.oncreate(savedInstanceState) and before the binding code:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this,
MainViewModelFactory(PreferenceManager.getDefaultSharedPreferences(this)))
.get(MainViewModel::class.java)
... rest of the method code below ...
}
Here, you use an object called ViewModelProvider. This is a store holding a reference to ViewModels for a particular Scope. Think of a scope as the lifetime of a ViewModel. How long that lifetime lasts depends on the UI component the ViewModel is attached to. In this case, it’s attached to MainActivity. You can see that by the use of this in the first parameter.
The next parameter MainViewModelFactory is important. ViewModels by default, don’t expect to have properties in their constructors. In order for them to do so, a ViewModelFactory needs to be created and passed into ViewModelProvider. This allows it to understand how to construct ViewModels that require properties. This factory accepts an instance of SharedPreferences, used to save and retrieve lists. You’ll create this factory shortly.
The final parameter MainViewModel::class.java, specifies what type of ViewModel should be retrieved from the Provider. You want to retrieve your MainViewModel, passing in the class makes sure that happens.
There’s one other place that needs to add the ViewModelProvider, that’s MainFragment.kt. Why does it need to know about it you ask? The short story is it allows you to share the same ViewModel between different parts of your UI code. You’ll see how it works shortly.
Open MainFragment.kt, then update the onActivityCreated(savedInstanceState: Bundle?) method to look like the following:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(requireActivity(),
MainViewModelFactory(PreferenceManager.getDefaultSharedPreferences(requireActivity())))
.get(MainViewModel::class.java)
}
You may notice the code looks very similar, except for the requireActivity() method calls. Recall that ViewModelProvider is a store of ViewModels, scoped to a particular UI component. Since MainViewModel is scoped to MainActivity, you want to make sure you retrieve the exact same ViewModel if it is available. You do that by passing in MainActivity again.
Don’t worry if this doesn’t make too much sense for now. You’ll learn more about the relationship between Fragments and Activities in the next few chapters.
With the ViewModel setup, let’s create the MainViewModelFactory. In com.raywenderlich.listmaker.ui.main, create a new class called MainViewModelFactory. Then, update the class to resemble the following:
// 1
class MainViewModelFactory(private val sharedPreferences: SharedPreferences) : ViewModelProvider.Factory {
// 2
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(sharedPreferences) as T
}
}
Here’s what the code does:
-
You add a constructor for the factory to receive an instance of SharedPreferences. This is used to create
MainViewModel. You also implement theViewModelProvider.Factoryinterface. -
You override the
create(modelClass: Class<T>)method from the interface. The method returns an instance ofMainViewModelthat uses the SharedPreferences field within its constructor.
With the factory created. Your project should be in a buildable state again. Run the app again, just to make sure that everything is ok.
The next step is to begin creating lists and making them show up in the RecyclerView.
Showing Real Lists in the RecyclerView
Open MainActivity.kt. Then, in the positive button onClickListener for the AlertDialog. Add a new line underneath the dialog dismissal to create a new list in MainViewModel.
builder.setPositiveButton(positiveButtonTitle) { dialog, _ ->
dialog.dismiss()
viewModel.saveList(TaskList(listTitleEditText.text.toString()))
}
Next, open MainFragment.kt. Add the following code to the bottom of onActivityCreated(savedInstanceState: Bundle?).
val recyclerViewAdapter = ListSelectionRecyclerViewAdapter(viewModel.lists)
binding.listsRecyclerview.adapter = recyclerViewAdapter
viewModel.onListAdded = {
recyclerViewAdapter.listsUpdated()
}
Here, you’re creating the Adapter once the ViewModel is available. Then, you assign the adapter to the RecyclerView using the ViewBinding. Finally, you use the onListAdded lambda to listen for added lists. When a list is added, recyclerViewAdapter.listsUpdated() is called. You’ll create this shortly. Remove the lines in onCreateView() that create and assign the RecyclerViewAdapter. You no longer need these.
Open ListSelectionRecyclerViewAdapter.kt and update the class definition to accept a MutableList of TaskList in its primary constructor:
class ListSelectionRecyclerViewAdapter(private val lists : MutableList<TaskList>) : RecyclerView.Adapter<ListSelectionViewHolder>() {
Find onBindViewHolder() and replace the holder.binding.itemString.text line to use the list to populate the ViewHolder instead of the static array of strings:
override fun onBindViewHolder(holder: ListSelectionViewHolder, position: Int) {
holder.binding.itemNumber.text = (position + 1).toString()
holder.binding.itemString.text = lists[position].name
}
Modify getItemCount() to get the size of lists:
override fun getItemCount(): Int {
return lists.size
}
Finally, Add a new method called addList() to let the adapter know you have a new list to display. Add the following code to the bottom of the Adapter class:
fun listsUpdated() {
notifyItemInserted(lists.size-1)
}
You call notifyItemInserted() to inform the Adapter that you updated the data source, which updates the RecyclerView. In this case, the data source is the ArrayList passed into the ListSelectionRecyclerViewAdapter, and any necessary ViewHolders are created to populate each View with the right data for each position.
With that done, remove the listTitles array at the top of the ListSelectionRecyclerViewAdapter since you no longer need it.
Run the app, tap the FAB to display the Create List Dialog, and give the list a name.
Tap Create and the new task list appears in the RecyclerView.
You’re not quite done — there are a few things left to check. Does the list stick around after you stop and restart the app?
Click the Stop button in Android Studio; it’s the big red square in the toolbar at the top.
Your device stops running the app and goes back to the home screen. Once again, run the app from Android Studio, and the list returns.
With this test done, you can be certain the app persists the list to SharedPreferences and loads it when relaunched. Great job!
Note: Just to remind you, you may notice the order of the list titles changing as the app relaunches. This highlights one of the issues when using SharedPreferences.
SharedPreferences is only a key-value store; it doesn’t order your data.
For this example, SharedPreferences is a great option to store and read data quickly. However, as your needs become more complex, you should consider other methods of storage that adhere to order; these are explained later in the book.
One last thing. Because you’re using a ViewModel, your list should remain on the screen. Even if the device rotates. Rotate your device or emulator. What happens?
The list is still there!
Key Points
Listmaker is beginning to look like a usable app and you’ve covered some important topics. You now know:
-
What SharedPreferences are and how to use them.
-
What ViewModels are and the benefits they provide.
-
How to use a Dialog to prompt the user for more information.
-
How to update the rows of a RecyclerView as new data comes in.
Where to go from here?
SharedPreferences is the simplest way to persist values in an Android app, so it’s worth keeping in your toolbox. The next step is to let users add items to their lists, which is exactly what you’ll do in the next chapter!