- How to store data locally in an Android app
- Ways to store data
- Using Shared Preferences
- Using internal storage
- External Storage
- SQLite database
- The new way of storing data in Android — Jetpack DataStore
- SharedPreferences
- Implementation
- Preferences DataStore
- Why should you use Preferences DataStore instead of SharedPreferences?
- Implementation
- Migration from SharedPreferences
- SharedPreferences vs DataStore
- Why use Room?
- Implementation
- When should you use DataStore instead of Room?
- Proto Datastore
- Key features:
- Implementation
- Language Guide (proto3) | Protocol Buffers | Google Developers
- This guide describes how to use the protocol buffer language to structure your protocol buffer data, including .proto…
- Using DataStore With Kotlin Serialization
- Up till now we’ve shared how to use DataStore with Protos or Preferences. Under the hood both DataStore versions use…
- Default value
- Room vs DataStore
- Wrapping up
How to store data locally in an Android app
Almost every app we use or develop has to store data for one purpose or another. It’s not all the same data, either — some apps need access to settings, images, and much more. The big question is how to manage this data so that your device can grab only what it needs. Luckily for developers, Android is full of ways to store data, and we’re here to run you through how they work.
For this article, we’ll discuss the different data storage techniques available to Android developers, along with sample code to get you started or to refresh your memory.
Ways to store data
Using Shared Preferences
Shared Preferences is the way to go if you’re saving primitive data as key-value pairs. It requires a key, which is a String, and the corresponding value for the said key. The value can be any of the following: a boolean, float, int, long, or another string.
Your Android device stores each app’s Shared Preferences inside of an XML file in a private directory. Apps can also have more than one Shared Preferences file, and they’re ideally used to store app preferences.
Before you can store data with shared preferences, you must first get a SharedPreferences object. There are two Context methods that you can use to retrieve a SharedPreferences object.
For when your app will have a single preferences file, and
for when your app could have multiple preferences files, or if you prefer to name your SharedPreferences instance.
On getting the SharedPreferences object, you then access its Editor using the edit() method. To actually add a value, use the Editor’s putXXX() method, where XXX is one of Boolean, String, Float, Long, Int, or StringSet. You can also remove a key-value preference pair with remove().
Finally, make sure to call the Editor’s commit() method after putting or removing values. If you don’t call commit, your changes will not be persisted.
For our sample app, we allow the user to specify a SharedPreferences filename. If the user specifies a name, we request for the SharedPreferences with that name; if not, we request the default SharedPreference object.
Unfortunately, there is no way to get a single list of all SharedPreferences files stored by your app. Instead, you will need a static list or access to the SharedPreferences name if you’re storing more than one file.
You could also save your SharedPreferences names in the default file. If you need to store user preferences, you may want to use the PreferenceActivity or PreferenceFragment command. Just remember that they both use Shared Preferences, too.
Using internal storage
There are plenty of times where you may need to persist data, but you find Shared Preferences too limiting. For example, you may need to persist objects or images in Java. You might also need to persist your data logically with the file system hierarchy. This is where internal storage comes in. It is specifically for when you need to store data on the file system, but you don’t want other apps or users to have access.
This data storage is so private, in fact, that it’s deleted from the device as soon as you uninstall your app.
Using internal storage is similar to saving with any other file system. You can get references to File objects, and you can store data of virtually any type using a FileOutputStream. What sets it apart is the fact that its contents are only accessible by your app.
To get access to your internal file directory, use the Context getFilesDir() method. To create (or access) a directory within this internal file directory, use the getDir(directoryName, Context.MODE_XXX) method. The getDir() method returns a reference to a File object representing the specified directory, creating it first if it doesn’t exist.
In the sample above, if the user-specified filename is empty, we get the base internal storage directory. If the user specifies a name, we get the named directory, creating first if needed.
To read files, use your preferred file reading method. For our example, we read the complete file using a Scanner object. To read a file that’s directly within your internal storage directory (not in any subdirectory), you can use the openFileInput(fileName) method.
Similarly, to access a file for writing directly within the Internal Storage directory, use the openFileOutput(fileName) method. To save files, we use the FileOutputStream write.
As you can see in the image above, the file path is in a folder not accessible by the file manager or other apps. The only exception to this will be if you have a rooted device.
External Storage
Google has made a few key changes to external storage, beginning with Android 10 and continuing in Android 11. To give users better control over their files and cut down on clutter, apps now have scoped access to external storage by default. This means that they can tap into the specific directory on external storage and the media that the app creates.
For more information about requesting scoped directory access, check out this Android developer tutorial.
If your app tries to access a file that it did not create, you will have to permit it to do so every single time. Data you store outside of select folders will also disappear if you delete your app.
Apps are expected to store files in one of two app-specific locations designed for the app’s specific persistent files and cached files, respectively. To access these locations, the app must verify the storage is available (which is not guaranteed, as it is for internal storage). The volume’s state can be queried using:
If MEDIA_MOUNTED is returned, that means you can read and write files to external storage. You will find a number of predefined directories that should aid with logical storage and prevent clutter. These include the likes of DIRECTORY_DOCUMENTS and DIRECTORY_MOVIES.
You can read a full explanation of how to use scoped storage here.
SQLite database
Finally, Android provides support for apps to use SQLite databases for data storage. The databases you create remain specific to your app and can only be accessed inside your app. Of course, you should have at least some knowledge of SQL before you attempt to store data with an SQLite database.
We’ll discuss each of these in turn, and we use data binding techniques for our sample code. Android provides complete support for SQLite databases. The recommended way of creating SQLite databases is to subclass the SQLiteOpenHelper class and override the onCreate() method. For this sample, we create a single table.
Источник
The new way of storing data in Android — Jetpack DataStore
In mobile applications some data has to be persisted to make the application startup faster, reduce network traffic or handle data completely offline. In this article, I would like to demonstrate the opportunities of how to store data in your Android application, especially with Jetpack DataStore. To introduce this I am going to give a walkthrough about the four mainly used approaches. The solutions that will be covered in the article are:
- SharedPreferences
- Preferences DataStore
- Room
- Proto DataStore
Besides the summary of current storage solutions, I will focus on the differences between SharedPrefrences, Room and DataStore. Regarding the DataStore, I added the implementation steps for both Preferences DataStore, and Proto DataStore.
SharedPreferences
If you have to store key-value pairs in your app, e.g.: user’s settings, IDs, the simplest way to implement this solution is the SharedPreferences API. A SharedPreferences object points to a file containing key-value pairs and provides simple methods to read and write them. Each SharedPreferences file is managed by the framework and can be private or shared across applications.
Implementation
Add required gradle dependency:
Saving user information to SharedPreferences:
Retrieving user information from SharedPreferences:
In the case of my application, which demonstrates a simple user data saving with the usage of Kotlin coroutines, this solution is not easy to use with Flow.
Unfortunately, SharedPreferences runs its read/write operations, except apply() function, on the main thread. Due to this disadvantage, and the lack of Kotlin Flow, and LiveData support, I would like to recommend a more sophisticated option, the Preferences DataStore.
Preferences DataStore
Preferences DataStore aims to replace SharedPreferences. The concept behind Preference DataStore is quite similar to SharedPreferences. It uses key-value pairs to store data and also does not provide type safeness. If you’re currently using SharedPreferences to store data, consider migrating to DataStore instead.
Why should you use Preferences DataStore instead of SharedPreferences?
- DataStore is safe to call on the UI thread because it uses Dispatchers.IO under the hood
- You do not have to use apply() or commit() functions to save the changes
- Handles data updates transactionally
- Exposes a Flow representing the current state of data
Implementation
1. Need to add this line into app-level build.gradle file’s dependencies
2. Create the DataStore instance
To create a Preferences DataStore, we can use the preferencesDataStore delegate. The delegate will ensure that we have a single instance of DataStore with that name in our application.
3. Create keys for the key part of the key-value pairs ( preferencesKeys )
4. To save data into DataStore
The edit() function is a suspend function, so it needs to be called from CoroutineContext .
Inside the lambda we have access to MutablePreferences , so we can change the value under the specified key.
Migration from SharedPreferences
With the usage of SharedPreferencesMigration we have the opportunity to migrate the “old fashioned” SharedPrefrences data to DataStore. Related/suggested article in this topic: Working with Preferences DataStore — Codelabs step by step guide — Step 7
SharedPreferences vs DataStore
- It is built on Kotlin Coroutines and Flow. It exposes the preference values using Flow.
- You don’t need to manually switch to a background thread
- DataStore is safe from runtime exceptions and has error handling support. SharedPreferences throws parsing errors as runtime exceptions.
- In both implementations, DataStore saves the preferences in a file and performs all data operations on Dispatchers.IO unless specified otherwise.
In my opinion, the following table is the best way to highlight the differences between the two key-value pair based storage approaches, and the Proto DataStore:
Room is designed to store and handle non-trivial amounts of structured data locally. Under the hood Room is a persistence library, which provides an abstraction layer over SQLite. The three major components of the library are:
The following diagram illustrates the connection between these major components:
Why use Room?
- Part of Jetpack
- Verifying SQL queries at compile time
- It can be integrated with LiveData , RxJava , and Coroutine Flow easily
- Reduces the amount of boilerplate code
Implementation
Add required Gradle dependencies:
Create the DAO with the Room SQL queries:
Room row insertion:
When should you use DataStore instead of Room?
If you use Room to save only one user’s data, you need to create a database with a table, and also need to implement the query and insert functions. It sounds really inconvenient, so in this case, the Proto DataStore can be the corresponding approach.
Proto Datastore
The aim of Proto DataStore is really similar to Preferences DataStore, but the previous one is able to store objects with custom data types. Unlike the Preferences DataStore it does not use key-value pairs, just returns the generated object in a Flow . The generated file’s type and structure depend on the schema of the .protoc file.
Key features:
- Provides type safety out of the box
- Able to define list inside protoc schema with repeated marked fields
- Requires to learn a new serialization mechanism, which depends on Protobuf, but it is worth the effort
- Protobuf is faster than XML and JSON serialization formats
Implementation
1. Add the following gradle dependencies to your app-level build.gradle file
If you need to use only Proto DataStore with typed objects you do not need to add the preferences version of the datastore dependency (androidx.datastore:datastore-preferences:1.0.0-beta01) .
2. Add protobuf to plugins in build.gradle
3. Add protobuf configuration to build.gradle
4. Add your proto file to project
In my case, I need to define the schema, which contains the user’s first name, last name, and birthday. So I need to add two string fields, and one 64-bit integer field ( int64 ). Birthday is stored in Long format, and that was the reason for using a 64-bit integer field.
You need to place your .proto file under app/src/main/proto folder. I gave user_preference.proto name to my file.
If you want to be familiar with the syntax of the protocol buffers, you should check this documentation:
Language Guide (proto3) | Protocol Buffers | Google Developers
This guide describes how to use the protocol buffer language to structure your protocol buffer data, including .proto…
5. Create the DataStore Serializer
This serializer class tells DataStore how to read and write your data type. Kotlin Serialization supports multiple formats including JSON and Protocol buffers.
As I mentioned before you have an option to use JSON instead of Protocol buffers. In my opinion, JSON is more readable and understandable, but the most common usage of DataStore depends on protocol buffers. Related, and recommended article, if you want to start a JSON based DataStore:
Using DataStore With Kotlin Serialization
Up till now we’ve shared how to use DataStore with Protos or Preferences. Under the hood both DataStore versions use…
6. Create the DataStore
7. To save data into DataStore
Default value
First of all, if you want to read an empty DataStore, which contains only standard field types (e.g.: string, int32, enum ), it will return the protobuf’s generated object with pre-initialized default values. In this case, your integers will be zero, and the strings will be empty strings.
If you import google/protobuf/wrappers.proto into your protobuf file, which depends on proto3 syntax you will be able to add fields with nullable types, e.g.: google.protobuf.StringValue first_name = 1;
Room vs DataStore
If you have a need for partial updates, referential integrity, or large/complex datasets, you should consider using Room instead of DataStore. DataStore is ideal for small or simple datasets and does not support partial updates or referential integrity.
Suppose that your data structure is not large/complex, and need to store only one/few rows in a table, e.g: one user’s data. In this scenario, the usage of the Room can be a little bit of overkill. So if you have to store only a small bunch of data in your database, you have only one or few tables, and you don’t want to use the benefits of Room, you should choose DataStore.
The most crucial disadvantage of DataStore against Room is that, it does not support partial updates: if any field is modified, the whole object will be serialized and persisted to disk. If you want partial updates, consider the Room API (SQLite).
Wrapping up
In summary, DataStore is ideal to use with Kotlin, and especially with coroutines. Don’t worry if you already have an application, which contains only the standard SharedPreferences solution. The migration is provided by the DataStore API.
Because of the fact that DataStore handles data migration, guarantees data consistency, and handles data corruption, it is worth changing from SharedPreferences to DataStore.
The examples of the article were implemented originally in my POC application, which aims to demonstrate the implementation of these four ways of storage. If you are interested in the deeper technical/coding part of the topic, feel free to check the application on Github:
Источник