Android Jetpack Component Series:
Android Jetpack Component (1) LifeCycle
Android Jetpack Component (2) Navigation
Android Jetpack Component (3) ViewModel
Android Jetpack Component (IV) LiveData
Android Jetpack Component (V) Room
Android JetPack Component (6) DataBinding
Android Jetpack Component (7) Paging
Android Jetpack Component (8) WorkManager
The last time the epidemic is close to me, the 10th day of isolation, the 8th day of home office, I hope the epidemic will pass soon and end isolation ✊.
First Language
Data persistence refers to which instantaneous data in memory is stored on a storage device to ensure that data is not lost even when the mobile phone or computer is powered off.
There are three main ways to achieve data persistence in Android. That is, file storage, SharedPreferences storage, and database storage. SharedPreferences uses key-value pairs to store lightweight data, is simple to use, and will be cleaned up after uninstallation without data residue. However, SharedPreferences also has many drawbacks. It operates on disk I/O, which can cause performance problems, cause ANR, and is inefficient in multi-threaded scenarios, storage delay. Storing large data such as json or html frequently causes GC, resulting in interface carton. SharedPreferences used in project development encountered data cache delay. Later, Tencent was used MMKV.
Now, Google is introducing the DataStore to replace Shared Preferences and overcome most of their shortcomings.
Jetpack DataStore is a data storage solution that allows you to use Protocol Buffer Stores key-value pairs or typed objects. The DataStore uses the Kotlin protocol and Flow to store data in an asynchronous, consistent transaction fashion.
Contrast
DataStore provides two different implementations: Preferences DataStore and roto DataStore.
- Preferences DataStore is implemented by classes DataStore and References, which use keys to store and access data. This implementation does not require a predefined architecture and does not guarantee type safety.
- Proto DataStore stores data on disk as an instance serialization of a custom data type. This implementation requires that you use Protocol Buffers To define the schema, but to ensure type safety.
Protocol Buffers (ProtocolBuffer/ protobuf) is a data description language developed by Google Corporation. It is similar to XML in that it can serialize structured data and can be used for data storage, communication protocols, etc. Protocol buffers have many advantages over XML in serializing structured data:
-
Simpler.
-
Data description files only need 1/10 to 1/3 of the original.
-
The resolution speed is 20 to 100 times faster than before.
-
Reduces ambiguity.
-
Generated data access classes that are easier to use in programming.
Below is a comparison of two different implementations of SharedPreferences and DataStore by Google.
rely on
// Preferences DataStore implementation "androidx.datastore:datastore-preferences:1.0.0" // Proto DataStore implementation "androidx.datastore:datastore-core:1.0.0"
Use
In both implementations, the DataStore stores preferences in a file unless otherwise specified, and all data operations are performed on Dispatchers.IO.
Preferences DataStore
Establish
Create a Datastore <Preferences>instance using a property delegate created by the Preferences DataStore. Calling the instance once at the top level of the kotlin file gives you access to the instance through this property in all the rest of the application. This makes it easier to keep the DataStore as a single unit.
val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
read
Since Preferences DataStore does not use a predefined schema, you must define a key for each value you need to store in the DataStore <Preferences>instance using the corresponding key type function. For example, to define a key for an int value, use intPreferencesKey(). Then, read the content using the DataStore.data property.
val USER_INFO = intPreferencesKey("user_info") dataStore.data.collect { val i = it.toPreferences()[USER_INFO] tvContent.text=i.toString() }
Write in
Preferences DataStore provides an edit() function for transactionally updating data in a DataStore. The transform parameter of the function accepts blocks of code in which you can update values as needed. All the code in the transform block is treated as a single transaction, so look at the source code to see.
dataStore.edit { settings -> val currentCounterValue = settings[USER_INFO] ?: 0 settings[USER_INFO] = currentCounterValue + 1 }
Proto DataStore
Proto DataStore implementations use DataStore and Protocol Buffer Keep typed objects on disk.
Import
- Import the plugins plug-in. Add the following code to the build.gradle of your app.
plugins { id "com.android.application" id "kotlin-android" id "com.google.protobuf" version "0.8.12" }
- Add dependencies.
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
- Configure the protoc command, equivalent to dependencies.
protobuf { protoc { // //Download protoc from repository The version number here needs to be the same as the version dependent on com.google.protobuf:protobuf-javalite:xxx artifact = 'com.google.protobuf:protoc:3.10.0' } generateProtoTasks { all().each { task -> task.builtins { java { option "lite" } } } } // Default build directory $buildDir/generated/source/proto Change build location through generatedFilesBaseDir generatedFilesBaseDir = "$projectDir/src/main" }
- Set the proto file location.
android { sourceSets { main { proto { // The default path to the proto file is src/main/proto // You can modify the location of the proto file through srcDir srcDir 'src/main/proto' } } } }
- Compile the project.
Create a new folder proto in the app/src/main directory, then create a new file of type. proto UserPrefs under the folder proto, write the proto file and its fields, and rebuild the project.
// Fixed, also proto2 syntax="proto3"; // Format: Package name +. + File name option java_package = "com.yhj.kotlincomponent.protobuf"; //You can generate separate.java files for each generated class option java_multiple_files = true; message Settings { int32 count = 1; }
It is recommended here to install the Protocol Buffer Editor plug-in, which is Protocol Buffer Files provide editor support. Grammar highlighting, editor enhancements, and so on, make debugging very easy.
For proto3 syntax, use tips, reference Google proto3 Tutorial , explain in detail.
Once the build is complete, you can see that the protobuf file directory is generated in the app\src\maindebug directory, which contains the Settings, SettingsOrBuilder, and UserPrefs files.
Establish
Define a class that implements Serializer<T>, where T is the type defined in the proto file. This serializer class tells DataStore how to read and write your data type. Be sure to add a default value for the serializer to use when no files have been created.
Use the attribute delegation created by the dataStore to create an instance of the DataStore <T> where T is the type defined in the proto file. Calling the instance once at the top level of your Kotlin file gives you access to the instance through this property delegation in all the rest of the application. The filename parameter tells DataStore which file to use to store data, and the serializer parameter tells DataStore the name of the serializer class defined above.
object SettingsSerializer : Serializer<Settings> { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream ) = t.writeTo(output) } val Context.settingsDataStore: DataStore<Settings> by dataStore( fileName = "settings.pb", serializer = SettingsSerializer )
Write in
Proto DataStore provides an updateData() function to update stored objects transactionally. UpdateData () provides you with the current state of the data, as an instance of a data type, and transactionally updates the data in atomic read-write-modify operations.
settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setCount(currentSettings.count + 1) .build() }
read
Use DataStore.data to get stored data.
settingsDataStore.data.collect { Log.e("yhj", it.count.toString(), ) }
Migrate SharedPreferences
When creating a DataStore, the preferences DataStore parameter contains the produceMigrations parameter, which is used to migrate SharedPreferences. A read or write operation is required for the DataStore to merge automatically. When the migration is successful, the original SharedPreferences file is deleted. Proto DataStore is used the same way.
val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", produceMigrations = { con -> listOf(SharedPreferencesMigration(con, "app_cache")) })
principle
Create a Datastore <Preferences>instance using a property delegate created by the Preferences DataStore.
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> { return INSTANCE ?: synchronized(lock) { if (INSTANCE == null) { val applicationContext = thisRef.applicationContext INSTANCE = PreferenceDataStoreFactory.create( corruptionHandler = corruptionHandler, migrations = produceMigrations(applicationContext), scope = scope ) { applicationContext.preferencesDataStoreFile(name) } } INSTANCE!! } }
A file was created to write key-value pairs to disk in the subdirectory of applicationContext.filesDir+datastore/ File suffix name is.preferences_pb,
public fun Context.preferencesDataStoreFile(name: String): File = this.dataStoreFile("$name.preferences_pb") public fun Context.dataStoreFile(fileName: String): File = File(applicationContext.filesDir, "datastore/$fileName")
Data is read directly through dataStore.data, and data is written through dataStore.edit, which is also actually written through dataStore.updateData.
public interface DataStore<T> { public val data: Flow<T> public suspend fun updateData(transform: suspend (t: T) -> T): T }
public suspend fun DataStore<Preferences>.edit( transform: suspend (MutablePreferences) -> Unit ): Preferences { return this.updateData { // It's safe to return MutablePreferences since we freeze it in // PreferencesDataStore.updateData() it.toMutablePreferences().apply { transform(this) } } }
summary
Preferences DataStore is relatively simple and Proto DataStore is more complex than the two implementations of DataStore.
DataStore overcomes many of SharedPreference's shortcomings and is highly recommended by Google, so it's time to say goodbye to SharedPreference and hug Jetpack DataStore.