Store key-value data on disk
If we have a small set of key values we want to save, we can use the shared_preferences plugin.
Often we have to write native platform integrations to store data for both platforms. Fortunately, the shared_preferences plugin is available for this purpose. The Shared Preferences plugin wraps NSUserDefaults on iOS and SharedPreferences on Android to provide persistent storage for simple data.
Establish
Before we start, we need to add the shared_preferences plugin to our pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
shared_preferences: "<newest version>"
save data
To persist key-value data, we can use the SharedPreferences class. To save the data, we call the set method. Note that data is persisted asynchronously. If we want to be notified when data is saved, use the commit() function.
// obtain shared preferences
SharedPreferences prefs = await SharedPreferences.getInstance();
// set new value
prefs.setInt('counter', counter);
read data
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
In the above example, we load data from the counter key and return 0 if it doesn't exist .
remove data
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove('counter');
Setter and getter methods are available for all primitive classes.
supported types
While using a key-value store is very simple and convenient, it has some limitations:
- Only primitive types can be used: int , double , bool , string and string list
- It is not intended to store large amounts of data, so it is not suitable as an application cache.
For more information on Shared Preferences on Android, visit the Shared Preferences documentation on the Android Developers website .
example
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of our application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Shared preferences demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Shared preferences demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
void initState() {
super.initState();
_loadCounter();
}
//Loading counter value on start
_loadCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_counter = (prefs.getInt('counter') ?? 0);
});
}
//Incrementing counter after click
_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_counter = (prefs.getInt('counter') ?? 0) + 1;
setState(() {
_counter;
});
prefs.setInt('counter', _counter);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Test support
We can populate SharedPreferences with initial values in our tests by running the following code :
const MethodChannel('plugins.flutter.io/shared_preferences')
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'getAll') {
return <String, dynamic>{}; // set initial values here if desired
}
return null;
});
The above code should be placed in a test file under the test folder.
read and write files
In some cases, reading and writing files to disk can be very convenient. This can be used to persist data across application launches or download data from the internet and save it for later offline use.
In order to save the file to disk, we need to use the path_provider plugin in combination with the dart:io library.
route
- Find the correct local path
- Create a reference to the file location
- write data to file
- read data from file
1. Find the correct local path
In this example, we will display a counter. When the counter changes, we need to write the data on disk so that it can be read again when the application loads. So we need to ask: where should we store this data?
The path_provider plugin provides a platform-agnostic way to access common locations on the device filesystem. The plugin currently supports access to two system file locations:
- Temporary Directory: A temporary directory (cache) that the system can clear at any time. On iOS, this corresponds to the value returned by NSTemporaryDirectory() . On Android, this is the value returned by getCacheDir() .
- Documents directory : The application's directory for storing files that only it can access. The system clears the directory only when the application is removed. On iOS, this corresponds to NSDocumentDirectory . On Android, this is the AppData directory.
In our case, we want to store information in the documents directory! We can find the path to the documents directory like this:
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
2. Create a reference to the file location
Once we know where to store the file, we need to create a reference to the full location of the file. We can do this using the File class from the dart:io library .
Future<File> get _localFile async {
final path = await _localPath;
return new File('$path/counter.txt');
}
3. Write data to file
Now that we have a File to work with, we can use it to read and write data! First, we write some data to a file. Since we're using a counter, we'll just store integers as strings.
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file
return file.writeAsString('$counter');
}
4. Read data from file
Now we have some data on disk and we can read it! Again, we'll use the File class to do this.
Future<int> readCounter() async {
try {
final file = await _localFile;
// Read the file
String contents = await file.readAsString();
return int.parse(contents);
} catch (e) {
// If we encounter an error, return 0
return 0;
}
}
test
In order to test the code that interacts with the file, we need to mock the call to the MethodChannel . MethodChannel is the class that Flutter uses to communicate with the host platform.
In our tests, we were unable to interact with the filesystem on the device. We need to interact with our test environment's filesystem!
To simulate method calls, we can provide a setupAll function in our test file. This function will run before the test execution.
setUpAll(() async {
// Create a temporary directory to work with
final directory = await Directory.systemTemp.createTemp();
// Mock out the MethodChannel for the path_provider plugin
const MethodChannel('plugins.flutter.io/path_provider')
.setMockMethodCallHandler((MethodCall methodCall) async {
// If we're getting the apps documents directory, return the path to the
// temp directory on our test environment instead.
if (methodCall.method == 'getApplicationDocumentsDirectory') {
return directory.path;
}
return null;
});
});
complete example
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(
new MaterialApp(
title: 'Reading and Writing Files',
home: new FlutterDemo(storage: new CounterStorage()),
),
);
}
class CounterStorage {
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get _localFile async {
final path = await _localPath;
return new File('$path/counter.txt');
}
Future<int> readCounter() async {
try {
final file = await _localFile;
// Read the file
String contents = await file.readAsString();
return int.parse(contents);
} catch (e) {
// If we encounter an error, return 0
return 0;
}
}
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file
return file.writeAsString('$counter');
}
}
class FlutterDemo extends StatefulWidget {
final CounterStorage storage;
FlutterDemo({Key key, @required this.storage}) : super(key: key);
@override
_FlutterDemoState createState() => new _FlutterDemoState();
}
class _FlutterDemoState extends State<FlutterDemo> {
int _counter;
@override
void initState() {
super.initState();
widget.storage.readCounter().then((int value) {
setState(() {
_counter = value;
});
});
}
Future<File> _incrementCounter() async {
setState(() {
_counter++;
});
// write the variable as a string to the file
return widget.storage.writeCounter(_counter);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text('Reading and Writing Files')),
body: new Center(
child: new Text(
'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}