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ć.

4.2 (9) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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.

  1. On the first line, you call updateData() which expects you to pass in a suspending lambda. You get the current state of FilterOption in the parameter.
  2. 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.

List of completed courses with filter

Congratulations! You successfully implemented the Jetpack DataStore to your app.