-
Notifications
You must be signed in to change notification settings - Fork 0
Lab 13
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.
Β
- Integrate Proto DataStore into the project. This should already be done if you're using the starter code.
- Apply the
com.google.protobuf
plugin to your project (see hints) - Add the DataStore dependency to your project (see hints)
- Add
protobuf{}
config blockapp/build.gradle
(see hints)
- Apply the
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
-
Add the Data Store dependency
implementation "androidx.datastore:datastore:1.0.0"
toapp/build.gradle
-
Define protobuf message to represent the default note category
- Create a protobuf file named
DefaultCategory.proto
file atapp/src/main/proto/DefaultCategory.proto
- Within
DefaultCategory.proto
, define aDefaultCategory
proto message with a singlestring category = 1;
field (see hints)
- Create a protobuf file named
-
Prepare DataStore for interaction
- Create a new package named
datastore
- Create
DefaultCategorySerializer
class that extendsSerializer<DefaultCategory>
(see hints) - Create
defaultCategoryDataStore
extension property (see hints) - 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
- Create a new package named
-
Create a
DefaultCategoryRepository
interface (see hints)- Should have a property named
defaultCategory
with typeFlow<DefaultCategory>
- Should have a suspending function named
updateDefaultCategory
that takes aString
parametercategory
. - 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.
- Should have a property named
-
Create a class
DataStoreCategoryRepository
that implementsDefaultCategoryRepository
(see hints)- This will be the actual implementation that uses Data Store to save/retrieve values
- The class should have a single constructor parameter
private val context: () -> Context
- Implement
defaultCategory
using a computed propertyget() = context().defaultCategoryDataStore.data
- Implement
updateDefaultCategory()
to use our extention propertydefaultCategoryDataStore
to update the currently savedDefaultCategory
-
Pass an instance of
DefaultCategoryRepository
toCreateNoteViewModel
- Add a constructor property to
CreateNoteViewModel
of typeDefaultCategoryRepository
-
CreateNoteViewModelFactory
should take an instance ofDefaultCategoryRepository
and pass it to the view model constructor - In
CreateNoteFragment
, create an instance ofDataStoreCategoryRepository
and pass it to the instance ofCreateNoteViewModelFactory
- Add a constructor property to
-
Add methods to
CreateNoteViewModel
for handling theDefaultCategory
- Add a method
suspend fun saveSelectedCategory(selectedIndex: Int)
that callscategoryRepository.updateDefaultCategory(CATEGORIES[selectedIndex])
- Add a method
fun indexForCategory(category: String): Int = CATEGORIES.indexOf(category)
- Add a method
-
Update category spinner to respond to selection and to set default value
- In
CreateNoteFragment.onCreateView()
, add aOnItemSelectedListener
to your spinner - In
onItemSelected
launch a coroutine and callviewModel.saveSelectedCategory(position)
- In
CreateNoteFragment.onViewCreated()
collectcateogryRepository.defaultCategory
and callbinding.categorySpinner.setSelection(viewModel.indexForCategory(category.category))
- In
Β
Β
Β
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'
}
}
}
}
}
Β
For this lab, you should only need implementation "androidx.datastore:datastore:1.0.0"
Β
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;
}
Β
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)
}
Β
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
Contextor
Activity.
val Context.defaultCategoryDataStore: DataStore<DefaultCategory> by dataStore(fileName = "notes.pb", serializer = DefaultCategorySerializer)
Β
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()
}
}
}
Β
Spinners
are a form of AdapterView
and therefore support setting a OnItemSelectedListener
to be notified anytime a user selects an item