When you start developing mobile apps, one of the first real needs that arises is quickly save data to the deviceThe user's email address, whether they've already viewed the onboarding, their game level, a session token... These are small pieces of data, but fundamental to ensuring a smooth experience and preventing the app from seeming "amnesic" every time it's opened.
In Android, and also in other environments like Flutter, this type of lightweight storage is usually addressed with mechanisms of key-value pairs in local storageFor years, SharedPreferences have been the standard, and today their "natural evolution" in Android is DataStore. Let's take a detailed look at how they work, how to use them effectively, how alternatives like SharedPreferences integrate with Flutter, and when it's worth upgrading to more powerful solutions.
What is local key-value storage and what is it used for?
The pattern shared by SharedPreferences in Android, DataStore, and the SharedPreferences package in Flutter is simple: they are based on store information as key-value pairs on disk, persistently, without needing to build a complete database or deal with SQL.
This type of storage is designed for small amounts of dataThese are frequently accessed, non-complex relationships: app settings, user preferences, flags indicating whether a certain screen has already been displayed, form values we want to remember, etc. In classic Android, these materialize as XML files under the hood, and in iOS, as UserDefaults; Flutter abstracts this so that the same code works across multiple platforms, integrating solutions for internal and external storage and the cloud.
In exchange for this simplicity, there are clear limitations: There are no complex queries, no joins, and no indexes.It's not the place to store large lists, long histories, or relational information. For that, you need databases like Room/SQLite or solutions like Hive, depending on the technology you use.
SharedPreferences on Android: the classic fast storage tool
In the Android framework, SharedPreferences is the historical API for read and write small persistent data in a system-managed XML file. Each preference file is associated with a name and contains multiple key-value pairs that you can easily query and modify from your code.
A SharedPreferences object represents a specific preferences file It provides methods for getting and storing basic data types: strings, integers, booleans, floats, or sets of strings. It's ideal for things like a user's email address, the state of a configuration option, or the last level reached in a game.
Caution: DataStore is the recommended modern solution
Although SharedPreferences is still available and working, Google has already clearly indicated that DataStore is the recommended option for modern key-value storage in Android. DataStore supports Kotlin's coroutines and flow, offering a more secure API in terms of concurrency, atomic operations, and a clear model for observing data changes.
While SharedPreferences can cause problems in contexts with concurrent access or blockages on the main thread if commit() is abused, DataStore It solves many of these shortcomings. with a design geared towards asynchronicity and reactive workflows. Later we'll see how DataStore fits into an app's architecture and in which cases migration is even advisable.
How to obtain and manage a SharedPreferences file in Android
To work with SharedPreferences on Android, you first need access or create the preferences fileThe framework offers several options, depending on your needs:
- getSharedPreferences(name, mode)This is used when you want to manage multiple preference files, each identified by a name. You can call it from any instance of Context.
- getPreferences(mode)Designed to use a single preference file per Activity. You don't need to specify a name; it's associated with that activity by default.
A common pattern is to construct a filename that be unique to your applicationFor example, by combining the applicationId with a suffix, like «com.example.myapp.PREFERENCE_FILE_KEY»This way you avoid conflicts with other apps and keep the structure clear.
The usual opening mode is MODE_PRIVATE, so that only your app should have access to that file. The old MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE modes are deprecated since API 17 and, since Android 7.0, would throw a SecurityException if used. If you need to share files with other apps, the correct approach is to use a FileProvider with FLAG_GRANT_READ_URI_PERMISSIONno to global preferences.
In the specific case that you are saving global app settings using the Preferences library (settings screens), the most common practice is to use getDefaultSharedPreferences(), which gives you access to the default preferences file for the entire application, integrating with the configuration UI components.
Writing to SharedPreferences: Editor, apply() and commit()
To modify values in a SharedPreferences file, the flow always passes through a SharedPreferences.Editor objectYou obtain it by calling edit() on the SharedPreferences instance, adding or updating the key-value pairs you are interested in, and finally, confirming the change with apply() or commit().
The most commonly used methods for writing in the editor are putString(), putInt(), putBoolean(), putFloat() and similar variations. Each one receives a key (String) and a value of the corresponding type. Once all the modifications have been made in memory, you choose how to persist them:
- apply()This option updates the SharedPreferences object in memory immediately and saves the changes to disk asynchronously. It does not block the main thread and is the recommended option in most cases.
- commit(): writes to disk synchronously and returns a boolean indicating whether the save was successful. Because it blocks the thread, It's not a good idea to bring it up in the main thread. if it is used frequently.
In addition to basic write operations, SharedPreferences supports removal of a specific key with remove() and complete file deletion with clear(), always through the editor. These operations are also consolidated with apply() or commit().
Reading in SharedPreferences: secure access with default values
Reading data from SharedPreferences is even more straightforward: you call getString(), getInt(), getBoolean(), getFloat(), getLong() or to the equivalent method depending on the type, passing the key and a default value. If the key does not exist in the file, the method returns precisely that default value.
This approach avoids exceptions if you have key queries that haven't been created yet, and allows you to define a reasonable initial state (for example, an empty string for text, 0 for an integer, false for a boolean). You can also retrieve all keys with getAll() or getStringSet() for string sets.
Practical example in Android: remembering the user's email
A very typical use case for SharedPreferences in Android is remember the last data entered by the user in a form. Imagine a simple screen with an EditText field for the email address and a button that saves and closes the app. Every time it's opened again, you want the field to be automatically filled with the latest email address.
The flow would be: in onCreate(), after loading the layout, you get the reference to the EditText and an instance of SharedPreferences using getSharedPreferences("data", MODE_PRIVATE)Next, you retrieve the stored string with getString("mail", "") and assign it to the EditText. If the file doesn't already exist, Android creates it and returns the empty string as the initial value.
When the button is pressed, you create an Editor by calling edit() on the same SharedPreferences instance, and save the current content of the EditText with putString("mail", fieldValue)You confirm with `commit()` or `apply()` and end the Activity. The next time you start the app, the email will already be entered in the field.
Another example on Android: a mini address book with a custom password
SharedPreferences can also be used for slightly more creative scenarios, such as set up a simple agenda where the key is the person's name and the value is a block of text containing their data.
The interface can consist of two EditText fields (name and data) and two buttons (save and recall). In the method associated with the save button, you get the text from both fields, open the preferences file with getSharedPreferences("agenda", MODE_PRIVATE), create the Editor, and call putString(name, data) and confirm with commit(). Optionally, display a Toast message notifying that the data has been saved.
To retrieve it, you read the name, reopen the preferences, and call getString(name, ""). If the resulting string is empty, you know that There are no tickets available for that person.Therefore, you can notify it with a Toast. If there is content, you load it into the second EditText to display it on the screen. It's a very basic solution, but it illustrates how SharedPreferences allows you to handle flexible key-value data without needing a database.
More elaborate designs: encapsulate SharedPreferences and use patterns
As an app grows, it's advisable to stop accessing SharedPreferences from just anywhere and encapsulate the persistence logic in dedicated classes. A typical approach in Android with Kotlin is to create a Prefs class that receives a Context and internally handles read and write operations.
In that class, you can define constants for the filename and for each key (for example, PREFS_NAME and SHARED_NAME), as well as properties with custom getters and setters. A very practical example is defining a property name In your getter, use `prefs.getString(SHARED_NAME, "")` and in your setter, use `prefs.edit().putString(SHARED_NAME, value).apply()`. If you wanted to save an integer, you would use `putInt` and `getInt` in the same way.
To avoid creating Prefs instances haphazardly, it's common to take advantage of a class that extends ApplicationFrom there, in onCreate(), you can instantiate Prefs with applicationContext and expose it as an object accessible throughout the project using a companion object. This companion object acts as a singleton accessible from any Activity or fragment, through something like SharedApp.prefs.name.
Once this infrastructure is set up, the code for the screens is much cleaner: in an Activity you only need to worry about read and assign values from SharedApp.prefs, without repeating the SharedPreferences creation logic or the magic keys on each site.
View visibility and preference-based logic
A very instructive example of practical use is to create a screen that display a “guest” or “profile” view This depends on whether a name is saved in the preferences. You can use two methods for this: showGuest() and showProfile().
In showProfile(), you display a TextView with a greeting that includes the name stored in SharedPreferences, make a delete button visible, and hide the EditText and the save button. In showGuest(), you do the opposite: you show the text field and the save button, and hide the TextView and the delete button.
This is where another important Android concept comes in: the visibility of the viewsThere are three possible states:
- VISIBLEThe component is visible and occupies space in the layout.
- INVISIBLEThe component is not visible, but it still occupies its space, useful for maintaining alignments.
- GONEThe component is removed from the layout for space purposes; it is not visible and leaves no gap.
The screen logic can be completed with a `configView()` method, which decides whether to display `guest` or `profile` based on the result of `isSavedName()`, a function that checks whether the name preference is empty. The button listeners simply... update the value in SharedPreferences (they save the name or empty it) and call configView() again to refresh the interface.
DataStore on Android: the next step in modern storage
As already mentioned, SharedPreferences works, but it has some significant limitations at the level of concurrency, atomicity, and performance on the main threadTo solve this, Google proposes DataStore as a new local key-value storage solution.
DataStore offers two main variants: Preferences DataStorewhich maintains the key-value approach without a fixed scheme, and Proto DataStorewhich uses Protobuf to define a more robust typed schema, allowing for clear data validation and structuring. Both integrate deeply with Kotlin coroutines and Flow, offering safe asynchronous operations that do not block the interface.
Among the advantages of DataStore over SharedPreferences are the following: atomic operations, native concurrency management, and the ability to observe changes in real time With Flow and support for migrating data from SharedPreferences, DataStore is a much more natural fit for reactive architectures in scenarios where you need to observe live preferences from the UI or domain layer.
Best practices with DataStore and migrations from SharedPreferences
If your app already uses SharedPreferences but you want to modernize it, DataStore makes it easy. scheduled migrations from existing preference files, avoiding losing user data and complementing them with strategies of BackupIt is recommended to plan these migrations so that they are executed only once and transparently.
Other best practices with DataStore include always performing writes on appropriate threads (thanks to coroutines this is easy), handling errors and exceptions with reasonable retry policies, and encrypt sensitive data before persisting them. For relational or large volumes of information, it is still recommended to use Room or other databases, reserving DataStore for configurations, preferences, and simple data.
In clean architectures, the ideal is to expose access to the DataStore through repositories that offer Flows to the domain layer, instead of accessing it directly from Activities. This makes the code easier to test and maintain, and you can replace the storage implementation in tests without affecting the rest of the app.
When to choose SharedPreferences/DataStore and when to use a database
A rule of thumb for deciding is to assess the size and structure of the data. Use SharedPreferences or DataStore when the data that is simple, small and without complex relationshipssuch as user preferences, app settings, flag states, or small temporary tokens.
If you want to store large lists, histories, relationships between entities, or constantly growing data, the most sensible thing to do is rely on a database such as Room/SQLite on Android or Hive/SQLite on Flutter. It's also important to consider security: neither SharedPreferences nor DataStore are appropriate places to store passwords in plain text or private keys without additional encryption.
SharedPreferences in Flutter: Lightweight cross-platform persistence
In the Flutter ecosystem, there is an official package called shared_preferences that offers a persistent key-value storage mechanism very similar to native Android, but abstracted to work on different platforms (Android, iOS, web, desktop…).
On Android, it typically uses XML internally; on iOS and macOS, it integrates with UserDefaults; on the web, it relies on localStorage; and on Windows/Linux, it uses local storage via FFI. For Flutter developers, all of this is transparent, as they always work with the same API in Dart.
This is a perfect solution for save data such as user ID, email, session status, configuration preferences, or small flags regarding whether certain content has been displayed. For more complex tasks, the Flutter ecosystem itself recommends using databases (for example, sqflite) or options like Hive.
Installation and basic use of SharedPreferences in Flutter
To use SharedPreferences in Flutter, simply add the dependency to the pubspec.yaml file within the dependencies section, specifying the corresponding version, and run the command to obtain packagesNo additional configuration is required.
Read and write operations are asynchronous, as they access the device's storage system. First, you obtain an instance of SharedPreferences with SharedPreferences.getInstance() Using await, you then have at your disposal a set of get and set methods very similar to that of Android.
To read data, there are methods like getString(), getInt(), getBool(), getDouble(), getStringList(), and a generic get(), in addition to getKeys() to retrieve all keys. To write data, you use setString(), setInt(), setBool(), setDouble(), and setStringList(). Passing null as a value to a setter is equivalent to... remove key with remove().
Practical examples of get/set and object handling in Flutter
With a SharedPreferences instance in Flutter, you can save simple data with very direct calls, such as prefs.setString('name', 'Carlos')To read them, simply invoke getString('name'), getInt('age') or getBool('logged in').
If you need to store more complex objects (for example, a Person class with name and age), a common tactic is to serialize them to JSON and store them as StringYou can have a function that receives an object, converts it to a map or directly to JSON, and then saves it with `prefs.setString(key, json.encode(value)`. To retrieve it, you read the string, do `json.decode`, and reconstruct the object.
It's important not to overuse this pattern for very large structures. Writing and reading heavy JSON in SharedPreferences It is neither efficient nor comfortableFor simple profiles or small user data sets, it works well; if you want to store large collections, it's best to switch to a database or solutions designed for that purpose.
Session management and real-world examples in Flutter
A very common scenario in apps built with Flutter is needing the user Stay logged in even if you close the appThis can be resolved by saving a minimal set of keys in SharedPreferences: user identifier, email, a flag indicating whether logged in, and optionally, an authentication token.
Typical logic includes three functions: one to save the session (storing the keys and setting `is_logged_in` to `true`), another to check at app startup if the user is still logged in (reading the boolean or returning `false` if it's `null`), and another to log out (removing `user_id`, `user_email`, and setting `is_logged_in` to `false`). With this, The app can skip the login screen when it starts up and take the user directly to its content.
In e-learning projects or online academies developed with Flutter, this approach is very common: SharedPreferences is used to remember the user's basic identity and session statusFor content (courses, progress, etc.) more robust mechanisms are used, sometimes synchronized with a backend via REST APIs or GraphQL.
Best practices and limitations of SharedPreferences in Flutter
To avoid problems and potential bottlenecks, it's best to assume that SharedPreferences in Flutter is designed for small, frequently accessed dataIt is not a good idea to store huge lists or very complex structures in JSON format within a single value.
Security also needs to be considered: although SharedPreferences is not directly accessible to the user in production, it is not designed as such. secure storage for passwords or sensitive keysFor those cases, it is better to use solutions such as secure storage that integrate with the operating system's keystore.
On the web, remember that the package relies on localStorage and has its inherent limitations: reduced maximum size, potential removal by the browser, etc. It's best to assume that SharedPreferences It's not a foolproof warehouse and that critical information must also be on the backend.
Professional services for designing and modernizing local storage
In professional environments, choosing the right way and where to store data is key to the app's success. be scalable, secure and easy to maintainCompanies specializing in custom development, with experience in native Android, Flutter, cloud architectures and cybersecurity, can help you design that persistence layer taking into account not only performance, but also the protection of information.
Expert support is especially useful when Migrate from SharedPreferences to DataStoreDefine which data should go to DataStore and which to Room, or decide how to integrate the mobile part with BI dashboards (for example, using Power BI) and artificial intelligence agents that process events and metrics generated in the app.
If your goal is to modernize local storage on Android, leverage DataStore as a base for preferences and settings, combine it with secure cloud services, automate workflows with AI agents, and obtain dashboards that leverage your app's usage data, it's highly recommended. to approach the design of this layer with a global vision and not just as an isolated technical detail.
In short, SharedPreferences and its modern variants like DataStore, as well as the SharedPreferences package in Flutter, offer a very agile way to implement fast local storage for key-value pairs; when used well, they allow you to remember user states, manage lightweight sessions, save preferences and optimize the user experience without setting up complex infrastructures, while for relational or large volume data it is still essential to rely on well-designed databases and backend services.
