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. By Luka Kordić.
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
DataStore Tutorial For Android: Getting Started
30 mins
- Getting Started
- Enabling Auto Import
- Implementing Theme Change
- Observing Theme Changes
- Introducing Jetpack DataStore
- Comparing Jetpack DataStore and SharedPreferences
- Migrating SharedPreferences to Preferences DataStore
- Creating an Abstraction for Prefs DataStore
- Creating Prefs DataStore
- Reading Data From Prefs DataStore
- Observing Values From Prefs DataStore
- Writing Data to DataStore
- Introducing Proto DataStore
- Preparing Gradle for Proto DataStore
- Creating Proto Files
- Defining Proto Objects
- Creating a Serializer
- Preparing ProtoStore
- Creating Proto DataStore
- Storing Filter Options
- Reading Filter Options
- Reacting To Filter Changes
- Where to Go From Here?
Storing Filter Options
In 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.