Flutter Apps

13.1 Data Persistence

# Shared Preferences

SharedPreferences in Flutter are a lot like LocalStorage in the browser. You create a String key for each value you want to save and then save a value associated with that key.

The primary differences are:

  • that you don't have to worry about other apps or webpages interfering with your key names because all the values are connected to your app and the code that you are writing.
  • When you save or retrieve the value, you need to know what the datatype is for the information that you are saving.
  • The read write operations for SharedPreferences are asynchronous so we need to use Future and possibly async and await.

To use SharedPreferences we need to import the dart package shared_preferences from pub.dev (opens new window).

Add shared_preferences: ^2.0.13 to your pubspec.yaml dependencies list and run flutter pub get (if VSCode does not run it for you).

Then you can import it at the top of any file that needs it.

import 'package:shared_preferences/shared_preferences.dart';

//inside an async function we will access the SharedPreferences storage for our app
final prefs = await SharedPreferences.getInstance();
1
2
3
4

Here is the shared_preferences reference (opens new window).

Once you have a reference to the SharedPreferences storage instance for your app then you can start saving data. The data that you save must be one of the following types: boolean, string, integer, double, or List of Strings.

//To SAVE values use these methods
// Save an integer value to 'counter' key.
await prefs.setInt('counter', 10);
// Save an boolean value to 'repeat' key.
await prefs.setBool('repeat', true);
// Save an double value to 'decimal' key.
await prefs.setDouble('decimal', 1.5);
// Save an String value to 'action' key.
await prefs.setString('action', 'Start');
// Save an list of strings to 'items' key.
await prefs.setStringList('items', <String>['Earth', 'Moon', 'Sun']);
1
2
3
4
5
6
7
8
9
10
11

To read the values from your SharedPreferences instance

// Try reading data from the 'counter' key. If it doesn't exist, returns null.
final int? counter = prefs.getInt('counter');
// Try reading data from the 'repeat' key. If it doesn't exist, returns null.
final bool? repeat = prefs.getBool('repeat');
// Try reading data from the 'decimal' key. If it doesn't exist, returns null.
final double? decimal = prefs.getDouble('decimal');
// Try reading data from the 'action' key. If it doesn't exist, returns null.
final String? action = prefs.getString('action');
// Try reading data from the 'items' key. If it doesn't exist, returns null.
final List<String>? items = prefs.getStringList('items');
1
2
3
4
5
6
7
8
9
10

To remove an entry from your SharedPreferences instance, use the remove method.

// Remove data for the 'counter' key.
final success = await prefs.remove('counter');
1
2

It is worth noting that the write and delete methods are asynchronous and require the await keyword, but the read methods do not.

But, what about my big chunk of JSON, you ask? What if I have an array of numbers plus a whole host of other properties that I need to save?

The answer comes from the fact that JSON is already a String. If the data came from an API as a JSON String then you can simply use the await prefs.setString() method.

If you have data that you have gathered from other locations, like a form, and you want to save that information in SharedPreferences, then you can put all your information into a Map and convert that Map into a JSON String using the dart:convert package.

import 'package:shared_preferences/shared_preferences.dart';
import 'data:convert';

String convertToJsonAndSave (String key, String name, int id, String email) async{
  //pass in the values you want to save as a JSON string
  //plus a key to use for your saved data in SharedPreferences
  //access the Shared Preferences instance for your app
  final prefs = await SharedPreferences.getInstance();

  //build the map to hold the parameters passed to the function
  Map<String, dynamic> data = {
    'id': id,
    'name': name,
    'email': email,
  };

  //convert the Map to a JSON string
  String jsonStr = json.encode(data);
  //write the String to SharedPreferences
  await prefs.setString(key, jsonStr);
  //return the saved string in case it is needed elsewhere
  return jsonStr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Shared Preferences Solution

Here is the repo showing examples of connecting logins with SharedPreferences (opens new window). The example repo shows logging in, getting a token, saving the token in SharedPreferences, and navigating between screens when logged in or logged out. The login and navigating steps are split into separate buttons just to show the effects on the screens. In a real app you would be combining this functionality into a single button click.

# Random Numbers in Dart

In dart, when you want to create random values then you need to import the dart:math package. After that you will use the Random() class constructor to create a random value generator. You can optionally pass a unique seed value to the Random constructor. With the generator created you can call nextInt() or nextDouble() or nextBool() to get a random integer, double, or boolean value. For nextInt and nextDouble you can pass in a max value. The random number will be from zero, up to but not including your max value.

If you want to improve the randomness you can use the named constructor Random.secure() instead.

import 'dart:math';

//pick a random name from a list.
List<String> names = ['Tony', 'Robert', 'SuCheng', 'Steve', 'David'];

int seed = 3661;
int index = Random(seed).nextInt(names.length);
String randomName = name[index];
1
2
3
4
5
6
7
8

# JS Array map and filter method Equivalencies

In Javascript, we have a number of Array methods that we use to manipulate arrays of objects that we get from API calls that return JSON. Dart has many methods on it's List object that do the same things. Dart Reference for List objects (opens new window).

In Javascript, the two most common methods are map and filter. map returns a new Array that has the same length as the original. It allows us to modify the objects that are inside, usually reducing the size of the original objects. The filter method allows us to remove one or more unwanted objects from the original array and create a new modified Array. Both methods achieve this by calling a function on each of the elements in the Array and returning a new element (for map) or true | false (for filter) to decide whether to keep the old element.

//map example creating a new Array with new objects
//that are a copy of the old objects with a new property
let newArr = oldArr.map((element) => {
  let newElement = { ...element };
  newElement.newProp = 'hello';
  return newElement;
});
//filter example creating a new Array that copies the old Array except
//that the element with the id 5 has been removed.
let idToMatch = 5;
let newArr = oldArr.filter((element) => element.id != idToMatch);
1
2
3
4
5
6
7
8
9
10
11

And here are the dart equivalents to the JS methods above, using map and where.

List newArr = oldArr.map((element) {
  Map<String, dynamic> newElement = {...element}; //destructuring works here too
  newElement['newProp'] = 'hello';
  return newElement;
}).toList();

int idToMatch = 5;
List newArr = oldArr.where((element) => element['id'] != idToMatch).toList();
1
2
3
4
5
6
7
8

NB: The dart methods must have the .toList method at the end to create the new list.

Both dart List objects and Javascript Array objects have a forEach method too.

# Path Provider

Getting the location of files that are saved on the device requires knowing about two folders - the temp folder and the app document directory. The pub.dev package path_provider can get us these two locations. We will also need the dart:io package to be imported.

import 'dart:io';
import 'package:path_provider/path_provider.dart';

void _getFolders() {
  Directory tempDir = await getTemporaryDirectory();
  Directory appDocDir = await getApplicationDocumentDirectory();
  String tempPath = tempDir.path;
  String appDocPath = appDocDir.path;
}
1
2
3
4
5
6
7
8
9

A useful application of the path and path_provider packages is the ability to take pictures and save them in an app folder. The Flutter Cookbook section has a recipe on how to do this. Cookbook Recipe for Camera (opens new window).

# Advice for Final Project

Final Project and Git

  • One partner will create the private repo and then invite their partner as a collaborator.
  • Remember to use GitHub as the way to share code with your partner.
  • Remember to set up the Settings > Branches > rules to require pull requests when merging into main.
  • Since only one person will own the repo. The owner will be responsible for doing the merge of every pull request.
  • The main branch is the starting point. Each time you want to add to the project, create your own new branch with git checkout -b myNewBranch.
  • When your code for a feature is ready, do a pull request and merge it back into the main branch on GitHub.

# What to do this week

TODO

Things to do before next week

Last Updated: 3/30/2022, 10:04:36 AM