Skip to content
Nate Ebel edited this page Jul 21, 2022 · 8 revisions

πŸ–₯ Lab 13: Saving User Data Using DataStore

Let's save the last selected note category and default to that whenever the CreateNoteFragment is displayed. To do this, we will work with another Android Architecture Component; Data Store.

  • Data Store is a library for storing key/value pairs, or typed objects locally on the device.
  • Preferences Data Store is the version that works with key/value pairs and is an evolution of Android's Shared Preferences apis.
  • Proto Data Store works with typed data using Google's Protobuf library. It allows us to save/retreive complex typed objects rather than simple key/value data.
  • Protocol Buffers are a platform-agnostic solution for serializing data. They allow developers to define data models in a generic way, and then generate language-specific implementations of those for a number of supported languages including Kotlin.
  • As we work with Data Store, we'll once again use Kotlin Flows to observe changes in our saved values. This allows us to build reactive flows that automatically respond to changes in the values we observe from Data Store.

Β 

πŸ“ Objectives

  1. Integrate Proto DataStore into the project. This should already be done if you're using the starter code.
    1. Apply the com.google.protobuf plugin to your project (see hints)
    2. Add the DataStore dependency to your project (see hints)
    3. Add protobuf{} config block app/build.gradle (see hints)

Whether using the starter code, or setting up on your own, complete the following steps before moving on to instruction 1.

  • When you check out the starter code, re-sync gradle
  • Once you re-sync Gradle, rebuild the project

  1. Add the Data Store dependency implementation "androidx.datastore:datastore:1.0.0" to app/build.gradle

  2. Define protobuf message to represent the default note category

    1. Create a protobuf file named DefaultCategory.proto file at app/src/main/proto/DefaultCategory.proto
    2. Within DefaultCategory.proto, define a DefaultCategory proto message with a single string category = 1; field (see hints)
  3. Prepare DataStore for interaction

    1. Create a new package named datastore
    2. Create DefaultCategorySerializer class that extends Serializer<DefaultCategory> (see hints)
    3. Create defaultCategoryDataStore extension property (see hints)
    4. This extension property is a convenience for getting access to the instance of Data Store. In a production-ready app, this would likely be done through a dependency injection solution
  4. Create a DefaultCategoryRepository interface (see hints)

    1. Should have a property named defaultCategory with type Flow<DefaultCategory>
    2. Should have a suspending function named updateDefaultCategory that takes a String parameter category.
    3. This interface will serve as an abstraction around accessing our saved category. This makes testing easier because we don't have a hard dependency on the DataStore-related code. This also gives us the option to swap out the storage implementation for another solution if needed down the line.
  5. Create a class DataStoreCategoryRepository that implements DefaultCategoryRepository (see hints)

    1. This will be the actual implementation that uses Data Store to save/retrieve values
    2. The class should have a single constructor parameter private val context: () -> Context
    3. Implement defaultCategory using a computed property get() = context().defaultCategoryDataStore.data
    4. Implement updateDefaultCategory() to use our extention property defaultCategoryDataStore to update the currently saved DefaultCategory
  6. Pass an instance of DefaultCategoryRepository to CreateNoteViewModel

    1. Add a constructor property to CreateNoteViewModel of type DefaultCategoryRepository
    2. CreateNoteViewModelFactory should take an instance of DefaultCategoryRepository and pass it to the view model constructor
    3. In CreateNoteFragment, create an instance of DataStoreCategoryRepository and pass it to the instance of CreateNoteViewModelFactory
  7. Add methods to CreateNoteViewModel for handling the DefaultCategory

    1. Add a method suspend fun saveSelectedCategory(selectedIndex: Int) that calls categoryRepository.updateDefaultCategory(CATEGORIES[selectedIndex])
    2. Add a method fun indexForCategory(category: String): Int = CATEGORIES.indexOf(category)
  8. Update category spinner to respond to selection and to set default value

    1. In CreateNoteFragment.onCreateView(), add a OnItemSelectedListener to your spinner
    2. In onItemSelected launch a coroutine and call viewModel.saveSelectedCategory(position)
    3. In CreateNoteFragment.onViewCreated() collect cateogryRepository.defaultCategory and call binding.categorySpinner.setSelection(viewModel.indexForCategory(category.category))

Β 

πŸ–₯ Lab 13 Hints: Saving User Data Using DataStore

Β 

πŸ’‘ Helpful Resources

Β 

πŸ’‘ How do I setup protobuf code generation to work with DataStore

This bit isn't obvious from the documentation, and isn't part of the main focus of this workshop, so feel free to follow these setups at the start of this lab. If you're using the starter branch, this should already be done for you.

First, you need to add the following plugin

// within app/build.gradle

plugins {
    ...
    id "com.google.protobuf" version "0.8.12"
}

Next, you need to add the dependency for the plugin

// within app/build.gradle

dependencies {
    ...
    implementation  "com.google.protobuf:protobuf-javalite:3.11.0"
}

Finally, add the following after the dependencies{} block

// within app/build.gradle

/**
 * Sets up protobuf code generation so we can generate DataStore models from our protobuf messages
 */
protobuf {
  protoc {
    artifact = "com.google.protobuf:protoc:3.11.0"
  }

  generateProtoTasks {
    all().each { task ->
      task.builtins {
        java {
          option 'lite'
        }
      }
    }
  }
}

Β 

πŸ’‘ Which DataStore dependency do I need?

For this lab, you should only need implementation "androidx.datastore:datastore:1.0.0"

Β 

πŸ’‘ What should DefaultCategory.proto look like?

syntax = "proto3";

option java_package = "dev.goobar.androidstudyguide.protos";   <-- update this with the package name of your project
option java_multiple_files = true;

message DefaultCategory {
  string category = 1;
}

Β 

πŸ’‘ How do I implement DefaultCategorySerializer?

You'll need to implement the datastore.core.Serializer interface with a type value of DefaultCategory.

object DefaultCategorySerializer : Serializer<DefaultCategory> {
  override val defaultValue: DefaultCategory = DefaultCategory.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): DefaultCategory {
    try {
      return DefaultCategory.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override suspend fun writeTo(t: DefaultCategory, output: OutputStream) = t.writeTo(output)
}

Β 

πŸ’‘ How do I interact with my DataStore?

A convenient way of interacting with a DataStore is to create an extension property on the Context or Activity class. This lets us interact with DataStore any time we have access to a ContextorActivity.

val Context.defaultCategoryDataStore: DataStore<DefaultCategory> by dataStore(fileName = "notes.pb", serializer = DefaultCategorySerializer)

Β 

πŸ’‘ How to implement DefaultCategoryRepository

interface DefaultCategoryRepository {
  val defaultCategory: Flow<DefaultCategory>
  suspend fun updateDefaultCategory(category: String)
}

class DataStoreCategoryRepository(private val context: () -> Context) : DefaultCategoryRepository {

  override val defaultCategory: Flow<DefaultCategory>
    get() = context().defaultCategoryDataStore.data

  override suspend fun updateDefaultCategory(category: String) {
    context().defaultCategoryDataStore.updateData { defaultCategory ->
      defaultCategory.toBuilder().setCategory(category).build()
    }
  }

}

Β 

πŸ’‘ How do I respond to Spinner selection?

Spinners are a form of AdapterView and therefore support setting a OnItemSelectedListener to be notified anytime a user selects an item

Check out the documentation for more info

Clone this wiki locally