3

DataStore Tutorial For Android: Getting Started

 3 years ago
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.
Home Android & Kotlin Tutorials

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ć Dec 21 2020 · Article (30 mins) · Intermediate

Version

JetpackDataStore-feature.png

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.

Note: This tutorial assumes you know the basics of Android development. If you’re new to Android development, check out this Android Tutorial for Beginners. You should also be familiar with the basics of Kotlin Coroutines and Flow.

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:

Project structure tree

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 StudioPreferences. On a PC, go to FileSettings. Then go to EditorGeneralAuto 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.

Note: You can use not-null assertion here because the value will always initialize in 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.

Note: For the full list of drawbacks and a more detailed comparison, check out this Android developers blog post.

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.

Note: You’ll get an error at this step because you didn’t override all of the methods from the interface. Don’t worry: You’ll fix this in the next step.

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:

  1. On the first line, you access the data of DataStore. This property returns a Flow. Then you call catch() from the Flow API to handle any errors.
  2. 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 of Preferences. If the exception isn’t IOException, you rethrow it or handle it in a way that works for you.
  3. Finally, map() returns a Flow which contains the results of applying the given function to each value of the original Flow. In your case, you get the data by using a certain key, the PreferencesKeys.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 return false 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 {} in toggleNightMode()
  • 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.

Note: 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:

  1. To write data to the Prefs DataStore you call edit(), which is a suspend and extension function on DataStore.
  2. 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.
  3. When called, this function suspends the current coroutine until the data persists to disk.
  4. When this process finishes, DataStore.data reflects the change and you get a notification about the changes you subscribed to.
  5. 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:

Note: You can install an Android Studio plugin which will enable syntax highlighting for proto files. To install the plugin, go to Plugins in AS preferences and search for Protocol Buffer Editor. Select it and click Install. Android Studio might also prompt you to install the plugin once you open the proto file, so that’s an easier way to do it.

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 BuildMake 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 TODOs 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.

  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.

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 rating

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK