DataStore Tutorial For Android: Getting Started
source link: https://www.raywenderlich.com/18348259-datastore-tutorial-for-android-getting-started
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.
DataStore Tutorial For Android: Getting Started
In this tutorial you’ll learn how to read and write data to Jetpack DataStore, a modern persistance solution from Google.
Version
DataStore is Google’s new and improved solution for persisting simple pieces of data by using either key-value pairs or protocol buffers for storing typed objects. It does so using Kotlin Coroutines and Flow to make all the transactions asynchronous, making all the data storing and fetching operations more performant and safe! It’s part of the Jetpack set of tools, so it’s also known as the Jetpack DataStore.
In this tutorial, you’ll learn how to:
- Store simple key-value pairs to the Jetpack DataStore.
- Store more complex, typed data to the Jetpack DataStore.
- Migrate existing data from Shared Preferences to the Jetpack DataStore.
- and what Protocol Buffers are.
Along the way, you’ll build an app that shows and filters a list of courses and supports dark mode.
Getting Started
Download the materials by clicking the Download Materials button at the top or bottom of the tutorial. Then open the starter project in Android Studio 4.1 or later and look through its content.
Once the project opens and syncs you’ll see this package structure:
This project uses ViewModel and LiveData Android Architecture Components, Model-View-ViewModel, or MVVM, architecture and Hilt for dependency injection.
Build and run. You’ll see a screen with the list of predefined courses, some filtering options on the top and a theme change option in the options menu.
Before adding any Kotlin code, configure Android Studio to insert import
statements automatically.
Enabling Auto Import
Enabling Auto Import saves you from adding every individual import
. If you already have Auto Import set, you can skip this paragraph and move to Implementing Theme Change.
If you’re on a Mac, go to Android Studio ▸ Preferences. On a PC, go to File ▸ Settings. Then go to Editor ▸ General ▸ Auto Import.
Under the Kotlin subheading, find Add unambiguous imports on the fly and Optimize imports on the fly (for current project). Check them both. Finally, click OK to save the settings.
With all that set, it’s time to dive into the coding.
Implementing Theme Change
You’ll start by implementing the theme change functionality and storing the current theme in SharedPreferences.
In learningcompanion/presentation open CoursesViewModel.kt. Add the following below the class declaration:
private val _darkThemeEnabled = MutableLiveData<Boolean>() val darkThemeEnabled: LiveData<Boolean> = _darkThemeEnabled init { _darkThemeEnabled.value = sharedPrefs.isDarkThemeEnabled() }
Here you create MutableLiveData
that will hold the theme information, called _darkThemeEnabled
. Since you don’t want to allow changes to this value from outside the file, you make it private.
Then, you create a public, immutable value you’ll observe in CoursesActivity.kt, named darkThemeEnabled
. This approach is common in Android, where you have an underscored property that is private and one that’s public, without an underscore.
And in the init
block you set LiveData
to the value stored in SharedPreferences
.
Now find toggleNightMode()
. Replace the comment inside launch()
with:
val darkThemeEnabled = _darkThemeEnabled.value!! sharedPrefs.setDarkThemeEnabled(!darkThemeEnabled) _darkThemeEnabled.value = !darkThemeEnabled
With this code, you find out if the dark theme is enabled. Then, you toggle that value by changing it and storing it in SharedPreferences
. Finally, you set the current value to LiveData
which you’ll observe in the Activity
.
init
.This is a great opportunity for you to learn how to migrate data from SharedPreferences
to Jetpack DataStore. Don’t be so impatient: You’ll do that soon. :]
For now, it’s time to observe and react to theme changes in CoursesActivity
.
Observing Theme Changes
You implemented a dark/light mode toggle logic in CoursesViewModel.kt. Now it’s time to observe the changes and change the theme accordingly.
In learningcompanion/ui/view open CoursesActivity.kt. Find subscribeToData()
and add the following at the bottom:
viewModel.darkThemeEnabled.observe(this) { nightModeActive -> this.nightModeActive = nightModeActive val defaultMode = if (nightModeActive) { AppCompatDelegate.MODE_NIGHT_YES } else { AppCompatDelegate.MODE_NIGHT_NO } AppCompatDelegate.setDefaultNightMode(defaultMode) }
In this code block you receive and process the darkThemeEnabled
value by calling observe(this)
. Every time you tap the theme change icon, it changes and updates the value. Then, you use the value to set the theme accordingly, using AppCompatDelegate.setDefaultNightMode(defaultMode)
.
Build and run. Click the change theme icon and see your app go dark. Now close and reopen the app to confirm the theme persisted.
This was a small introduction to set up the SharedPreferences
functionality. With that set, it’s time to explore and migrate to the Jetpack DataStore.
Introducing Jetpack DataStore
As you learned in the introduction, Jetpack DataStore is a solution for data persistence that lets you store key-value pairs or typed objects by using protocol buffers. And it does it all asynchronously, using Kotlin Coroutines and Flow!
You can choose from two implementations:
- Preferences DataStore uses keys to read and write data in a similar way to Shared Preferences.
- Proto DataStore stores data as objects of a custom data type. When using Proto DataStore, you have to define a schema using protocol buffers.
But why would you switch from using SharedPreferences
? Take a look at the differences between the Jetpack DataStore and SharedPreferences
.
Comparing Jetpack DataStore and SharedPreferences
Almost every Android developer has used the simple and intuitive Shared Preferences API. It lets you quickly store and retrieve simple pieces of data. While useful, the truth is, it has several drawbacks.
The biggest drawbacks when using SharedPreferences
include:
- Lack of a fully asynchronous API
- Lack of main thread safety
- No type safety
Fortunately, Google built the Jetpack DataStore to address these issues. Because Flow powers it, Jetpack DataStore has an asynchronous API and main thread safety by default. All the work automatically moves to Dispatchers.IO
under the hood, so you don’t have to worry about freezing your app while storing data.
Flow also provides safety from runtime exceptions and can signal errors. Later, you’ll see how easy it is to handle errors.
Now it’s time to migrate your SharedPreferences
to Preferences DataStore.
Migrating SharedPreferences to Preferences DataStore
First, open the app-level build.gradle file and verify you have the following dependency:
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05"
This dependency lets you use the Prefs DataStore API.
Now you’ll create an abstraction for Prefs DataStore.
Creating an Abstraction for Prefs DataStore
In the project pane on the left, navigate to learningcompanion. Then create a new prefsstore package. In the newly created package, create an Kotlin interface called PrefsStore
and a Kotlin class called PrefsStoreImpl
.
You’ll see a structure like this:
Now open PrefsStore.kt and add:
fun isNightMode(): Flow<Boolean> suspend fun toggleNightMode()
To import Flow
, select the import from the kotlinx.coroutines.flow package.
You created an interface as an abstraction layer for all of your interactions with the Preferences DataStore. Then you added functons to the interface to represent your DataStore operations. It contains isNightMode()
that returns a Flow
. The Flow
will represent the app setting that tells you if the night mode is on or off.
You also create toggleNightMode()
with the suspend modifier to change the night mode option when the user taps on the theme icon. You added a suspend modifier because the function will contain another suspend function later.
It’s time to write the interface implementation!
Creating Prefs DataStore
Open PrefsStoreImpl.kt and update the class name with a constructor and @Inject
:
class PrefsStoreImpl @Inject constructor( @ApplicationContext context: Context) : PrefsStore { }
Here you provide the context you’ll use to create a DataStore. @Inject
lets you use Context
here. @ApplicationContext
and @Inject
are annotations from Hilt, which let you provide the app-level context to your components.
Now, add a constant above the class name:
private const val STORE_NAME = "learning_data_store"
You’ll use this constant as the DataStore name in the next step.
Then, add the following code inside the class:
private val dataStore = context.createDataStore( name = STORE_NAME, migrations = listOf(SharedPreferencesMigration(context, PREFS_NAME)) )
Here, you add code for creating an instance of DataStore
using createDataStore()
. Then you pass in the constant value for name
and a list of migrations you want to run upon creation. You use a built-in SharedPreferencesMigration()
to create a migration. This will move all the data from your SharedPreferences
to the DataStore.
To add the missing import, press Option-Return on macOS or Alt-Enter on Windows. Import createDataStore()
without the Serializer
parameter and SharedPreferencesMigration
from androidx.datastore.preferences.
That’s it! It’s that easy to migrate your data from SharedPreferences
to DataStore
. :]
Now that you have your DataStore, it’s time to learn how to read data from it.
Reading Data From Prefs DataStore
In PrefsStoreImpl.kt, at the end of the file, add:
private object PreferencesKeys { val NIGHT_MODE_KEY = preferencesKey<Boolean>("dark_theme_enabled") }
This piece of code creates a Kotlin object
which holds the keys you’ll use to read and write data.
Below your dataStore
value, add:
override fun isNightMode() = dataStore.data.catch { exception -> // 1 // dataStore.data throws an IOException if it can't read the data if (exception is IOException) { // 2 emit(emptyPreferences()) } else { throw exception } }.map { it[PreferencesKeys.NIGHT_MODE_KEY] ?: false } // 3
Here’s a code breakdown:
- On the first line, you access the
data
ofDataStore
. This property returns aFlow
. Then you callcatch()
from the Flow API to handle any errors. - In the lambda block, you check if the exception is an instance of
IOException
. If it is, you catch the exception and return an empty instance ofPreferences
. If the exception isn’tIOException
, you rethrow it or handle it in a way that works for you. - Finally,
map()
returns aFlow
which contains the results of applying the given function to each value of the originalFlow
. In your case, you get the data by using a certain key, thePreferencesKeys.NIGHT_MODE_KEY
.If the key isn’t set when you try to read the data it returns
null
. You use the Elvis operator to handle this and returnfalse
instead.
Now implement toggleNightMode()
from the interface to avoid errors. Add isNightMode()
below:
override suspend fun toggleNightMode() { }
This code is only a declaration of the method you’ll implement later. So leave the body empty for now.
Next, you’ll observe values from Prefs DataStore.
Observing Values From Prefs DataStore
In the previous step, you wrote the code to read the value from the store, but you didn’t use it. Now, you’ll collect the flow in your ViewModel
.
Open CoursesViewModel.kt. Then remove sharedPrefs
from the constructor and replace it with:
private val prefsStore: PrefsStore
With this code, Hilt library injects the instance for you.
Hooray! You’re not using Shared Preferences anymore so remove everything related to it. Delete:
init
- Everything inside
viewModelScope.launch {}
intoggleNightMode()
- Both
darkThemeEnabled
and_darkThemeEnabled
values
You need to make one more change before Hilt can inject the instance.
Open StoreModule.kt. Uncomment @Binds
and bindPrefsStore()
. While outside of the scope for this tutorial, this code told Hilt how to provide the instances.
Now that you have access to the store, go back to CoursesViewModel.kt. Below the class declaration add:
val darkThemeEnabled = prefsStore.isNightMode().asLiveData()
asLiveData()
lets you convert Flow
to LiveData
.
Flow
won’t emit any values until you subscribe to LiveData
. You can also call collect()
to get the data directly from Flow
without converting it to LiveData
.Build and run. The app should look like before, but now it’s reading from the DataStore instead of SharedPreferences
.
This also proves your migration worked! Now it’s time to write the data to DataStore and toggle the night mode for the app.
Writing Data to DataStore
You finally prepared the code to store the current theme value into the DataStore.
Now, open PrefsStoreImpl.kt. Find toggleNightMode()
and add the following code:
dataStore.edit { it[PreferencesKeys.NIGHT_MODE_KEY] = !(it[PreferencesKeys.NIGHT_MODE_KEY] ?: false) }
Here’s a code breakdown:
- To write data to the Prefs DataStore you call
edit()
, which is a suspend and extension function onDataStore
. - It saves data transactionally in an atomic, read-modify-write operation. Atomic means you don’t have to worry about threading, as all operations are safe and there are no race conditions.
- When called, this function suspends the current coroutine until the data persists to disk.
- When this process finishes,
DataStore.data
reflects the change and you get a notification about the changes you subscribed to. - To change the value, you obtain the current value by using
PreferencesKeys.NIGHT_MODE_KEY
. You invert and store it again.
To complete this step, open CoursesViewModel.kt. Then locate toggleNightMode()
and add the following code to launch()
:
prefsStore.toggleNightMode()
Here you call the method you implemented to toggle the current theme.
Build and run. Change the theme and then restart the app. You’ll notice the theme persists.
Congratulations! You successfully implemented the theme change functionality by reading and writing to Prefs DataStore.
Next, you’ll take a look at Proto DataStore, to store more complex types of data.
Introducing Proto DataStore
Proto DataStore uses protocol buffers to serialize data. Protocol buffers are Google’s language-neutral and platform-neutral mechanism for serializing structured data.
You define how you want your data structured once. Then you use special, generated source code to write and read your structured data to and from various data streams while using a variety of languages.
This tutorial only covers the code you need to implement filtering. For more information, visit this protocol buffers tutorial.
The first step to using a Proto DataStore is to prepare your Gradle files. Let’s do that!
Preparing Gradle for Proto DataStore
To work with Proto DataStore gradle files need:
- The Protobuf plugin
- The Protobuf and Proto DataStore dependencies
- Protobuf configuration
The protobuf plugin and dependencies are already in the project. Open the app-level build.gradle file and below plugins sections on the top, paste:
protobuf { protoc { artifact = "com.google.protobuf:protoc:3.10.0" } generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } }
Sync the project. You need all of these changes to enable code generation for the files you’ll write in the next section.
Creating Proto Files
Before you jump into the filtering option’s implementation, you need to create a file in which you define proto objects.
Switch to Project view in the Project pane on the left side of Android Studio. Create a proto directory in app/src/main. Inside the new directory, create a file named filter_options.proto.
Your structure will look like this:
Now that you have your proto file, you’ll define the proto objects.
Defining Proto Objects
To implement filtering, you need an object that holds filter data. >You’ll persist this object in Proto DataStore. You’ll describe how you want this object to look, and the Proto Buffer plugin will generate the code for you.
Open filter_options.proto and add:
syntax = "proto3"; option java_package = "com.raywenderlich.android.learningcompanion.data"; option java_multiple_files = true; message FilterOption { enum Filter { NONE = 0; BEGINNER = 1; ADVANCED = 2; COMPLETED = 3; BEGINNER_ADVANCED = 4; BEGINNER_COMPLETED = 5; ADVANCED_COMPLETED = 6; ALL = 7; } Filter filter = 1; }
The first line signals you’re using proto3 syntax. To learn more check out this protocol buffers documentation.
java_package
specifies where you want the compiler to put generated files. java_multiple_files
set to true
means the code generator will create a separate file for each top-level message.
In protobufs, you define every structure using a message keyword followed by its name. You define each member, or field, of the structure inside that message
. To define a field, you specify a type, name and unique number.
You can also put enums in a message. When defining enums, the first value always needs a unique number set to 0. Setting it to 0 tells the compiler you want this to be the default value.
For this use-case, you define eight enum values for eight possible filtering combinations. After you create Filter
enum, the last line of code above defines a field of type Filter
in FilterOption message
.
Build the project now by selecting Build ▸ Make Project to generate the classes you described. Nothing will change in the app at this point.
Now it’s time to create the serializer.
Creating a Serializer
To tell DataStore how to read and write the data type you define in filter_options.proto, you need Serializer
.
First, in java/learningcompanion create a new package called protostore which will hold all of the code related to Proto DataStore. Create a new Kotlin file in this package called FilterSerializer. Inside the new file add:
class FilterSerializer : Serializer<FilterOption> { override fun readFrom(input: InputStream): FilterOption { try { return FilterOption.parseFrom(input) } catch (e: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", e) } } override fun writeTo(t: FilterOption, output: OutputStream) { t.writeTo(output) } override val defaultValue: FilterOption = FilterOption.getDefaultInstance() }
Import InvalidProtocolBufferException
from com.google.protobuf.
To create FilterSerializer
you implement the Serializer
and override its two functions: readFrom()
and writeTo()
. These two simple functions serialize and deserialize the objects you want to read and write.
The protobuf code generator generates parseFrom()
which you can use to deserialize objects. Similarly, it generates writeTo()
which you can use to serialize FilterOption
and write it to an OutputStream
. And finally, return the defaultValue
if there’s no data on disk.
Next, you’ll prepare the ProtoStore.
Preparing ProtoStore
Now that you’ve defined your filter object in a proto file and implemented serialization and deserialization mechanisms, it’s time to create an abstraction for the Proto DataStore. In protostore create a new Kotlin file called ProtoStore. Inside this file add:
interface ProtoStore { val filtersFlow: Flow<FilterOption> suspend fun enableBeginnerFilter(enable: Boolean) suspend fun enableAdvancedFilter(enable: Boolean) suspend fun enableCompletedFilter(enable: Boolean) }
This interface exposes the currently selected filter through filtersFlow
. It also exposes three methods that let you enable or disable each of the filtering options. You mark each method with suspend
because you’ll have to call another suspend function in the implementation.
Now you’ll create the Proto DataStore instance.
Creating Proto DataStore
In protostore, create a new file called ProtoStoreImpl. Open the file and add:
class ProtoStoreImpl @Inject constructor( @ApplicationContext private val context: Context) : ProtoStore {}
Here you create a class that implements ProtoStore
and uses Hilt to inject the Context
. When you create the class, Android Studio gives you an error saying you need to implement all of the methods from the interface.
Put your cursor on the class name and press Option-Return in macOS or Alt-Enter in Windows. In the pop-up that appears, select Implement members. Then, select all methods and press OK.
Leave the generated TODO
s for now. You’ll fix them in a moment.
To create a Proto DataStore, add the following code right below the class definition:
private val dataStore: DataStore<FilterOption> = context.createDataStore( fileName = "courses.pb", serializer = FilterSerializer() )
Now import createDataStore()
with the Serializer
parameter.
This code creates a new DataStore
by using createDataStore()
. You pass in the name of a file where you’ll save data and a serializer you created in the previous step.
It’s finally time for you to save your selected filters.
Storing Filter Options
In ProtoStoreImpl.kt navigate to enableBeginnerFilter()
. Replace the TODO
with:
dataStore.updateData { currentFilters -> val currentFilter = currentFilters.filter val changedFilter = if (enable) { when (currentFilter) { FilterOption.Filter.ADVANCED -> FilterOption.Filter.BEGINNER_ADVANCED FilterOption.Filter.COMPLETED -> FilterOption.Filter.BEGINNER_COMPLETED FilterOption.Filter.ADVANCED_COMPLETED -> FilterOption.Filter.ALL else -> FilterOption.Filter.BEGINNER } } else { when (currentFilter) { FilterOption.Filter.BEGINNER_ADVANCED -> FilterOption.Filter.ADVANCED FilterOption.Filter.BEGINNER_COMPLETED -> FilterOption.Filter.COMPLETED FilterOption.Filter.ALL -> FilterOption.Filter.ADVANCED_COMPLETED else -> FilterOption.Filter.NONE } } currentFilters.toBuilder().setFilter(changedFilter).build() }
This piece of code might look scary, but it’s not as difficult as it looks. Only the first and last lines are important for the DataStore. The rest of the code uses enum values to cover all possible combinations of the selected filters.
- On the first line, you call
updateData()
which expects you to pass in a suspending lambda. You get the current state ofFilterOption
in the parameter. - To update the value, in the last line you transform the current
Preferences
object to a builder, set the new value and build it.
You need to do something similar for the other two filters. Navigate to enableAdvancedFilter()
. Replace TODO
with:
dataStore.updateData { currentFilters -> val currentFilter = currentFilters.filter val changedFilter = if (enable) { when (currentFilter) { FilterOption.Filter.BEGINNER -> FilterOption.Filter.BEGINNER_ADVANCED FilterOption.Filter.COMPLETED -> FilterOption.Filter.ADVANCED_COMPLETED FilterOption.Filter.BEGINNER_COMPLETED -> FilterOption.Filter.ALL else -> FilterOption.Filter.ADVANCED } } else { when (currentFilter) { FilterOption.Filter.BEGINNER_ADVANCED -> FilterOption.Filter.BEGINNER FilterOption.Filter.ADVANCED_COMPLETED -> FilterOption.Filter.COMPLETED FilterOption.Filter.ALL -> FilterOption.Filter.BEGINNER_COMPLETED else -> FilterOption.Filter.NONE } } currentFilters.toBuilder().setFilter(changedFilter).build() }
Then locate enableCompletedFilter()
and replace TODO
with:
dataStore.updateData { currentFilters -> val currentFilter = currentFilters.filter val changedFilter = if (enable) { when (currentFilter) { FilterOption.Filter.BEGINNER -> FilterOption.Filter.BEGINNER_COMPLETED FilterOption.Filter.ADVANCED -> FilterOption.Filter.ADVANCED_COMPLETED FilterOption.Filter.BEGINNER_ADVANCED -> FilterOption.Filter.ALL else -> FilterOption.Filter.COMPLETED } } else { when (currentFilter) { FilterOption.Filter.BEGINNER_COMPLETED -> FilterOption.Filter.BEGINNER FilterOption.Filter.ADVANCED_COMPLETED -> FilterOption.Filter.ADVANCED FilterOption.Filter.ALL -> FilterOption.Filter.BEGINNER_ADVANCED else -> FilterOption.Filter.NONE } } currentFilters.toBuilder().setFilter(changedFilter).build() }
You prepared everything for storing the current filters. Now all you have to do is call these methods when the user selects a filter. Again, all this code does is update the filter within the Proto DataStore. It does so by comparing what the current filter is and changing the new filter according to what you enabled or disabled.
You can also store filtrs in a list of selected options, a bitmask and more, to make the entire process easier, but this is manual approach that’s not the focus of the tutorial. What’s important is how you update the data usign updateData()
and how you save the new filter values using setFilter()
and the builder.
Now, open CoursesViewModel.kt. Add the following property to the constructor:
private val protoStore: ProtoStore
By doing this, you tell Hilt to inject this instance for you.
Next, in enableBeginnerFilter()
add the following line to viewModelScope.launch
:
protoStore.enableBeginnerFilter(enable)
Here you invoke the appropriate method from the interface for every selected filter.
Now, add the next line to the same block in enableAdvancedFilter()
:
protoStore.enableAdvancedFilter(enable)
Then, in enableCompletedFilter()
add the following to viewModelScope.launch
:
protoStore.enableCompletedFilter(enable)
After you call all methods using the interface, open StoreModule.kt. Uncomment the rest of the commented code.
Well done! You successfully added everything for storing the current filter value to the DataStore. However, you won’t be able to see any changes in the app yet because you still need to observe this data to make your UI react to them.
Reading Filter Options
Open ProtoStoreImpl.kt. In filtersFlow
, replace the generated TODO
with:
dataStore.data.catch { exception -> if (exception is IOException) { exception.printStackTrace() emit(FilterOption.getDefaultInstance()) } else { throw exception } }
Here, you retrieve data from the Proto DataStore the same way you did for Prefs DataStore. However, here you don’t call map()
because you’re not retrieving a single piece of data using a key. Instead, you get back the entire object.
Now, go back to CoursesViewModel.kt. First, uncomment the last line in this file:
data class CourseUiModel(val courses: List<Course>, val filter: FilterOption.Filter)
Then, below the CoursesViewModel
class declaration add:
private val courseUiModelFlow = combine(getCourseList(), protoStore.filtersFlow) { courses: List<Course>, filterOption: FilterOption -> return@combine CourseUiModel( courses = filterCourses(courses, filterOption), filter = filterOption.filter ) }
In this piece of code, you use combine()
which creates a CourseUiModel
. Furhtermore, by using the original course list provided from getCourseList()
and the protoStore.filtersFlow
you combine the courses list with the filter option.
You filter the data set by calling filterCourses()
and return the new CourseUiModel
. CourseUiModel
also holds the currently selected filter value which you use to update the filter Chips
in the UI.
filterCourses()
doesn’t exist yet so it gives you an error. To fix it, add the following code below courseUiModelFlow
:
private fun filterCourses(courses: List<Course>, filterOption: FilterOption): List<Course> { return when (filterOption.filter) { FilterOption.Filter.BEGINNER -> courses.filter { it.level == CourseLevel.BEGINNER } FilterOption.Filter.NONE -> courses FilterOption.Filter.ADVANCED -> courses.filter { it.level == CourseLevel.ADVANCED } FilterOption.Filter.COMPLETED -> courses.filter { it.completed } FilterOption.Filter.BEGINNER_ADVANCED -> courses.filter { it.level == CourseLevel.BEGINNER || it.level == CourseLevel.ADVANCED } FilterOption.Filter.BEGINNER_COMPLETED -> courses.filter { it.level == CourseLevel.BEGINNER || it.completed } FilterOption.Filter.ADVANCED_COMPLETED -> courses.filter { it.level == CourseLevel.ADVANCED || it.completed } FilterOption.Filter.ALL -> courses // There shouldn't be any other value for filtering else -> throw UnsupportedOperationException("$filterOption doesn't exist.") } }
It looks complicated, but there’s not much going on in this method. You pass in a list of courses you’re filtering using the provided filterOption
. Then you return the filtered list. You return the filtered list by comparing the current filterOption
with courses’ levels.
The last piece of the puzzle is to create a public value, which you’ll observe from the CoursesActivity
.
Put the following code below darkThemeEnabled
:
val courseUiModel = courseUiModelFlow.asLiveData()
As before, you convert Flow
to LiveData
and store it to a value.
Finally, you need to add the code to react to filter changes and update the UI accordingly. You’re almost there! :]
Reacting To Filter Changes
Now you need to update the course list. Open CoursesActivity.kt. Navigate to subscribeToData()
and replace the first part of the function where you observe the courses with the following:
viewModel.courseUiModel.observe(this) { adapter.setCourses(it.courses) updateFilter(it.filter) }
Here, you observe courseUiModel
and update RecyclerView
with the new values by calling setCourses(it.courses)
. updateFilter()
causes the error because it’s commented out. After you uncomment the method, you’ll see those errors disappear!
Build and run. Apply some filters to the list and then close the app. Reopen it and notice it saved the filters.
Congratulations! You successfully implemented the Jetpack DataStore to your app.
Where to Go From Here?
You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
In this Jetpack DataStore tutorial you learned how to:
- Create Prefs DataStore and Proto DataStore.
- Migrate data from
SharedPreferences
to Jetpack DataStore. - Read and write to both Proto and Prefs DataStore.
- Implement theme changes.
- Add filters to your data set.
If you want to learn more about persisting data in Android, check out Saving Data On Android . Or, if you want to learn more about Flow, check out Kotlin Flow for Android: Getting Started.
Hopefully, you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below! Happy data storing! :]
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.
Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!
Add a rating for this content
Sign in to add a ratingRecommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK