Picture this: You’re browsing recipes and find one you like. You’re in a hurry and want to bookmark it to check it later. Can you build a Flutter app that does that? You sure can! Read on to find out how.
In this chapter, your goal is to learn how to use the shared_preferences plugin to save important pieces of information to your device.
You’ll start with a new project showing two tabs at the bottom of the screen for two views: Recipes and Groceries.
The first screen is where you’ll search for recipes you want to prepare. Once you find a recipe you like, just bookmark it, and the app will add the recipe to your Bookmarks page. It will also add all the ingredients you need to your shopping list. You’ll use a web API to search for recipes and store the ones you bookmark in a local database.
The completed app will look something like this:
This shows the Recipes tab with the results you get when searching for Pasta. It’s as easy as typing in the search text field and tapping the Search icon. The app stores your search term history in the combo box to the right of the text field.
When you tap a card, you’ll see something like this:
To save a recipe, just tap the Bookmark button. When you tap on the Bookmarks selector, you’ll see that the recipe has been saved:
If you don’t want the recipe anymore, swipe left or right, and you’ll see a delete button that allows you to remove it from the list of bookmarked recipes.
The Groceries tab shows the ingredients you need to make the recipes you’ve bookmarked.
You’ll build this app over the next few chapters. In this chapter, you’ll use shared_preferences to save simple data like the selected tab and also to cache the searched items in the Recipes tab.
By the end of the chapter, you’ll know:
What shared preferences are.
How to use the shared_preferences plugin to save and retrieve objects.
Note: Feel free to explore the entire app. There is a lot there to explore and learn that isn’t covered in the book. Copy any code you like for your projects.
Now that you know what your goal is, it’s time to jump in!
Getting Started
Open the starter project for this chapter in Android Studio. Open the pubspec.yaml file and click pub get, then run the app.
Notice the two tabs at the bottom — each will show a different screen when you tap it. Only the Recipes screen currently shows any UI. It looks like this:
App Libraries
The starter project includes quite a few libraries in pubspec.yaml:
auto_size_text: Useful library for ensuring text fits in the given space.
flutter_adaptive_scaffold: Library changing your UI based on changing sizes. Useful for the desktop, web and folding phones.
desktop_window: Library for the desktop app for setting the window size.
path: Library for handling files.
cached_network_image: Download and cache the images you’ll use in the app.
flutter_slidable: Build a widget that lets the user slide a card left and right to perform different actions, like deleting a saved recipe.
platform: For accessing platform-specific information.
freezed_annotation: Part of the freezed library. Generates useful JSON and related utility functions.
flutter_svg: Load SVG images without the need to use a program to convert them to vector files.
flutter_riverpod: State management library. You’ll learn more about this library in Chapter 13, “Managing State”.
Now that you’ve looked at the libraries take a moment to think about how you save data before you begin coding your app.
Saving Data
There are three primary ways to save data to your device:
Write formatted data, like JSON, to a file.
Use a library or plugin to write simple data to a shared location.
Use a SQLite database.
Writing data to a file is simple, but it requires you to handle reading and writing data in the correct format and order.
You can also use a library or plugin to write simple data to a shared location managed by the platform, like iOS and Android. This is what you’ll do in this chapter.
You can save the information to a local database for more complex data. You’ll learn more about that in Chapter 15, “Saving Data Locally”.
Saving Small Bits of Data
Why would you save small bits of data? Well, there are many reasons to do this. For example, you could save the user ID when the user has logged in — or if the user has logged in at all. You could also save the onboarding state or data the user has bookmarked to consult later.
Note that this simple data saved to a shared location is lost when the user uninstalls the app.
The shared_preferences Plugin
shared_preferences is a Flutter plugin that allows you to save data in a key-value format so you can easily retrieve it later. Behind the scenes, it uses the aptly named SharedPreferences on Android and the similar UserDefaults on iOS.
For this app, you’ll learn to use the plugin by saving the search terms the user entered and the tab currently selected.
One of the great things about this plugin is that it doesn’t require any setup or configuration. Just create an instance of the plugin, and you’re ready to fetch and save data.
Note: The shared_preferences plugin gives you a quick way to persist and retrieve data, but it only supports saving simple properties like strings, numbers, and Boolean values.
In later chapters, you’ll learn about alternatives you can use when you want to save complex data.
Be aware that shared_preferences is not a good fit to store sensitive data. To store passwords or access tokens, check out the Android Keystore for Android and Keychain Services for iOS, or consider using the flutter_secure_storage plugin.
To use shared_preferences, you first need to add it as a dependency. Open pubspec.yaml and underneath the flutter_svg library, add the following:
shared_preferences: ^2.2.0
Make sure you indent it the same as the other libraries.
Now, click the Pub Get button to get the shared_preferences library.
You can also run pub get from the command line:
flutter pub get
How Does it Work?
The shared_preferences library uses the system’s API’s to store data into a file. These are small bits of information like integers, strings or Booleans. It has three main sets of function calls:
setXXX: Set methods save the data of that specific data type.
getXXX: Get methods retrieve the data of that specific data type.
clear(): This method deletes all saved data.
remove(): This method removes a specific value.
All of these methods except the clear() method use a key to access an item. By giving the library a unique key, you can store, retrieve and delete specific items. Here’s an example:
final CURRENT_USER_KEY = 'CURRENT_USER_KEY';
final sharedPrefs = await SharedPreferences.getInstance();
sharedPrefs.setString(CURRENT_USER_KEY, '1011442433');
...
sharedPrefs.remove(CURRENT_USER_KEY);
In this example, you get an instance of the shared preference library and then set a string using that key. Later on, you remove that item if the user logged out, for example.
There are several other interesting methods:
containsKey(): Returns true if the key exists.
getBool(), getDouble(), getInt(): Methods to retrieve specific types.
setBool(), setDouble(), setInt(): Methods to store specific types.
There aren’t a lot of methods, but it’s a very useful library.
You’re now ready to store data. You’ll start by saving the searches the user makes so they can easily select them again in the future.
Running Code in the Background
To understand the code you’ll be adding next, you need to know a bit about running code in the background.
Most modern UI toolkits have a main thread that runs the UI code. Any code that takes a long time needs to run on a different thread or process so it doesn’t block the UI. Dart uses a technique similar to JavaScript to achieve this. The language includes these two keywords:
async
await
async marks a method or code section as asynchronous. You then use the await keyword inside that method to wait until an asynchronous process finishes in the background.
Saving UI States
You’ll use shared_preferences to save a list of saved searches in this section. Later, you’ll also save the tab that the user has selected so the app always opens to that tab.
You’ll start by preparing your search to store that information.
Adding Shared Preferences as a Provider
This app uses the Riverpod library to provide resources to other parts of the app. Chapter 13, “Managing State” covers Riverpod in more detail. For now, you want to create an instance of the SharedPreferences library on startup and provide it to other parts of the app. To do so, open up lib/providers.dart and import shared preferences library:
Then replace // TODO Add Shared Pref Provider with the following:
final sharedPrefProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
This creates a Riverpod Provider for our shared preference. Notice how we throw a UnimplementedError. This is because you’ll provide it in the main.dart file.
Open up main.dart. Add the shared preferences library and providers import:
Create an instance of the SharedPreferences library. Notice the await keyword. This will wait until the instance is created.
Riverpod requires a ProviderScope above the app where you’ll provide providers. These allow you to make functionalities like shared_preferences available to other parts of the app.
Override the sharedPrefProvider value with the shared pref you just created.
Because the main function has the async keyword, you can await getting an instance of SharedPreferences. By using overrideWithValue(), you replace the unimplemented exception with a real value. ProviderScope will be discussed more in Chapter 13, “Managing State” but is required for Riverpod to run.
Next, you’ll add an entry to the search list.
Adding an Entry
First, you’ll change the UI so that when the user presses the search icon, the app will add the search entry to the search list.
Open lib/ui/recipes/recipe_list.dart, locate // TODO: Add imports and replace it with:
import '../../providers.dart';
That imports the provider’s file.
Next, you’ll give each search term a unique key. Find // TODO Add Search Index Key and replace it with the following:
Checks the input value to make sure it’s not empty.
Tell the system to update the widgets by calling setState().
Clear the current search list and reset the currentCount, currentStartPosition and currentEndPosition.
Check to ensure the search text hasn’t already been added to the previous search list.
Add the search item to the previous search list.
Save the new list of previous searches.
You used a text field with a drop-down menu to show the list of previous text searches. That’s a row with a TextField and a CustomDropDownMenuItem. The menu item shows the search term and an icon on the right. It will look something like this:
Tapping the X will delete the corresponding entry from the list.
Testing the App
It’s time to test the app. You’ll see something like this:
The arrow button displays a menu when tapped and calls the method onSelected() when the user selects a menu item.
Enter a food item like pasta and you hit the search button. Then make sure that the app adds your search entry to the drop-down list.
Don’t worry about errors — that happens when no data exists. Your app should look like this when you tap the drop-down arrow:
Now, stop the app by clicking the red stop button.
Run the app again and tap the drop-down button. The pasta entry is there. It’s time to celebrate. :]
The next step is to use the same approach to save the selected tab.
Note: If you’re testing this on the web, you may notice that the drop-down menus are empty. This is because Android Studio will use random port numbers for the web, and this will cause different values to be shown. To fix this, you need to start the web with the same port number each time. You can do that by adding the --web-port launch parameter.
This will ensure you’ll see the same list each time.
Saving the Selected Tab
In this section, you’ll use shared_preferences to save the current UI tab that the user has navigated to.
Open lib/ui/main_screen.dart and add the following import:
You’ll use this constant for the selected index preference key.
Next, add this in the saveCurrentIndex() method by replacing // TODO Save Current Index with this:
final prefs = ref.read(sharedPrefProvider);
prefs.setInt(prefSelectedIndexKey, _selectedIndex);
Here, you:
ref.read extracts the shared preferences as usual.
Save the selected index as an integer.
Now, find and replace // TODO Get Current Index with this:
// 1
final prefs = ref.read(sharedPrefProvider);
// 2
if (prefs.containsKey(prefSelectedIndexKey)) {
// 3
setState(() {
final index = prefs.getInt(prefSelectedIndexKey);
if (index != null) {
_selectedIndex = index;
}
});
}
With this code, you:
Get the shared preferences reference.
Check if a preference for your current index already exists.
Get the current index and update the state accordingly.
Now, hot reload the app and select either the first or the second tab.
Go to the Groceries tab and quit the app. Run it again to make sure the app uses the saved index to go to the Groceries tab when it starts.
At this point, your app should show a list of previously searched items and also take you to the last selected tab when you start the app again. Here’s what it will look like:
Congratulations! You’ve saved the state for both the current tab and any previous searches the user made.
Key Points
There are multiple ways to save data in an app: to files, in shared preferences and to a SQLite database.
Shared preferences are best used to store simple, key-value pairs of primitive types like strings, numbers and Booleans.
An example of when to use shared preferences is to save the tab a user is viewing, so the next time the user starts the app, they’re brought to the same tab.
The async/await keyword pair lets you run asynchronous code off the main UI thread and then wait for the response. An example is getting an instance of SharedPreferences.
The shared_preferences plugin shouldn’t be used to hold sensitive data. Instead, consider using the flutter_secure_storage plugin.
Where to Go From Here?
In this chapter, you learned how to persist simple data types in your app using the shared_preferences plugin.
In the next chapter, you’ll continue building the same app and learn how to serialize JSON in preparation for getting data from the internet. See you there!