— Dart, Flutter, Flutter Tutorials — 2 min read
If you need to persist data in Flutter you have a few different options available to you. One of the most popular and simplest options is the shared_preferences
package. In this tutorial I'll show you how you can use it in conjunction with the example counter app.
The shared_preferences package has been around for a long time. You can use it to persist simple key-value pairs or lists of Strings. Under the hood it uses SharedPreferences on Android and UserDefaults on iOS. It should only really be used for simple data because when you read values it loads everything - it's not like an SQLite database that you can perform queries on(and pull a limited set of results). Because of this, as your data grows the time it takes to load it from disk will increase. That being said, you can convert complex objects into JSON and store them as a String - it's possible. In the end you need to make a decision based on how much data you will save and what type of data it is.
You can install shared_preferences
by adding the following line to your pubspec.yaml
file under 'dependencies':
1shared_preferences: ^2.0.6
Alternatively you can use the following command:
1flutter pub add shared_preferences
For the purposes of this tutorial I'll be working with the example counter app that you get when you create a new Flutter project, so if you're going to follow along you should create a new Flutter project using the following command:
1flutter create counter_prefs
Like I said above, after creating a new project your main.dart file should have this Widget inside it:
1class MyHomePage extends StatefulWidget {2 MyHomePage({Key? key, required this.title}) : super(key: key);3
4 // This widget is the home page of your application. It is stateful, meaning5 // that it has a State object (defined below) that contains fields that affect6 // how it looks.7
8 // This class is the configuration for the state. It holds the values (in this9 // case the title) provided by the parent (in this case the App widget) and10 // used by the build method of the State. Fields in a Widget subclass are11 // always marked "final".12
13 final String title;14
15 @override16 _MyHomePageState createState() => _MyHomePageState();17}18
19class _MyHomePageState extends State<MyHomePage> {20 int _counter = 0;21
22 void _incrementCounter() {23 setState(() {24 // This call to setState tells the Flutter framework that something has25 // changed in this State, which causes it to rerun the build method below26 // so that the display can reflect the updated values. If we changed27 // _counter without calling setState(), then the build method would not be28 // called again, and so nothing would appear to happen.29 _counter++;30 });31 }32
33 @override34 Widget build(BuildContext context) {35 // This method is rerun every time setState is called, for instance as done36 // by the _incrementCounter method above.37 //38 // The Flutter framework has been optimized to make rerunning build methods39 // fast, so that you can just rebuild anything that needs updating rather40 // than having to individually change instances of widgets.41 return Scaffold(42 appBar: AppBar(43 // Here we take the value from the MyHomePage object that was created by44 // the App.build method, and use it to set our appbar title.45 title: Text(widget.title),46 ),47 body: Center(48 // Center is a layout widget. It takes a single child and positions it49 // in the middle of the parent.50 child: Column(51 // Column is also a layout widget. It takes a list of children and52 // arranges them vertically. By default, it sizes itself to fit its53 // children horizontally, and tries to be as tall as its parent.54 //55 // Invoke "debug painting" (press "p" in the console, choose the56 // "Toggle Debug Paint" action from the Flutter Inspector in Android57 // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)58 // to see the wireframe for each widget.59 //60 // Column has various properties to control how it sizes itself and61 // how it positions its children. Here we use mainAxisAlignment to62 // center the children vertically; the main axis here is the vertical63 // axis because Columns are vertical (the cross axis would be64 // horizontal).65 mainAxisAlignment: MainAxisAlignment.center,66 children: <Widget>[67 Text(68 'You have pushed the button this many times:',69 ),70 Text(71 '$_counter',72 style: Theme.of(context).textTheme.headline4,73 ),74 ],75 ),76 ),77 floatingActionButton: FloatingActionButton(78 onPressed: _incrementCounter,79 tooltip: 'Increment',80 child: Icon(Icons.add),81 ), // This trailing comma makes auto-formatting nicer for build methods.82 );83 }84}
When you run the app there will be an 'Increment' button that will increase the counter and show you how many times you have pushed the button. The goal of this tutorial will be to save the counter's value so that it will persist when you reload or restart the app.
First of all make sure that you have imported the shared_preferences
package:
1import 'package:shared_preferences/shared_preferences.dart';
We're going to have to load the value of the counter when the app starts up(or MyHomePage appears). We can do this in the initState
method of _MyHomePageState. We're going to wrap this logic in a microtask
to make sure there are no layout rendering issues:
1@override2void initState() {3 super.initState();4 Future.microtask(() async {5 final prefs = await SharedPreferences.getInstance();6 final value = prefs.getInt('counter_value') ?? 0;7 setState(() {8 _counter = value;9 });10 });11}
The above method will get an instance of SharedPreferences and then retrieve the value for the 'counter_value' key. The result can be null so I've added ?? 0
to the end, this basically means 'give me 0 if the result is null'.
Next we're going to modify the existing _incrementCounter
method inside _MyHomePageState. We need to bump the value of the existing _counter variable, save the new result with SharedPreferences and then update the UI with setState
.
1Future<void> _incrementCounter() async {2 final value = _counter + 1;3 final prefs = await SharedPreferences.getInstance();4 await prefs.setInt('counter_value', value);5
6 setState(() => _counter = value);7}
Now when you press the FloatingActionButton the count will still increase, but if you reload the app or restart it the count from the last session will be persisted.
Now all that's left to do is add a reset button so the saved value can be reset for the counter. Let's add it to the existing Column
widget inside _MyHomePageState:
1<Widget>[2 Text(3 'You have pushed the button this many times:',4 ),5 Text(6 '$_counter',7 style: Theme.of(context).textTheme.headline4,8 ),9 // The reset button10 ElevatedButton(11 onPressed: () async {12 final prefs = await SharedPreferences.getInstance();13 await prefs.remove('counter_value');14
15 final value = prefs.getInt('counter_value') ?? 0;16 setState(() => _counter = value);17 },18 child: Text('Reset'),19 )20]
When we push the new button it will get an instance of SharedPreferences
and then remove the value for the 'counter_value' key(using the remove
method). After that we will reload the value again and update the UI with the setState
method.
Here's what the UI looks like after all changes have been made:
And here's the complete code for main.dart:
1import 'package:flutter/material.dart';2import 'package:shared_preferences/shared_preferences.dart';3
4void main() {5 runApp(MyApp());6}7
8class MyApp extends StatelessWidget {9 // This widget is the root of your application.10 @override11 Widget build(BuildContext context) {12 return MaterialApp(13 title: 'Flutter Demo',14 theme: ThemeData(15 // This is the theme of your application.16 //17 // Try running your application with "flutter run". You'll see the18 // application has a blue toolbar. Then, without quitting the app, try19 // changing the primarySwatch below to Colors.green and then invoke20 // "hot reload" (press "r" in the console where you ran "flutter run",21 // or simply save your changes to "hot reload" in a Flutter IDE).22 // Notice that the counter didn't reset back to zero; the application23 // is not restarted.24 primarySwatch: Colors.blue,25 ),26 home: MyHomePage(title: 'Flutter Demo Home Page'),27 );28 }29}30
31class MyHomePage extends StatefulWidget {32 MyHomePage({Key? key, required this.title}) : super(key: key);33
34 // This widget is the home page of your application. It is stateful, meaning35 // that it has a State object (defined below) that contains fields that affect36 // how it looks.37
38 // This class is the configuration for the state. It holds the values (in this39 // case the title) provided by the parent (in this case the App widget) and40 // used by the build method of the State. Fields in a Widget subclass are41 // always marked "final".42
43 final String title;44
45 @override46 _MyHomePageState createState() => _MyHomePageState();47}48
49class _MyHomePageState extends State<MyHomePage> {50 int _counter = 0;51
52 @override53 void initState() {54 super.initState();55 Future.microtask(() async {56 final prefs = await SharedPreferences.getInstance();57 final value = prefs.getInt('counter_value') ?? 0;58 setState(() {59 _counter = value;60 });61 });62 }63
64 Future<void> _incrementCounter() async {65 final value = _counter + 1;66 final prefs = await SharedPreferences.getInstance();67 await prefs.setInt('counter_value', value);68
69 setState(() => _counter = value);70 }71
72 @override73 Widget build(BuildContext context) {74 // This method is rerun every time setState is called, for instance as done75 // by the _incrementCounter method above.76 //77 // The Flutter framework has been optimized to make rerunning build methods78 // fast, so that you can just rebuild anything that needs updating rather79 // than having to individually change instances of widgets.80 return Scaffold(81 appBar: AppBar(82 // Here we take the value from the MyHomePage object that was created by83 // the App.build method, and use it to set our appbar title.84 title: Text(widget.title),85 ),86 body: Center(87 // Center is a layout widget. It takes a single child and positions it88 // in the middle of the parent.89 child: Column(90 // Column is also a layout widget. It takes a list of children and91 // arranges them vertically. By default, it sizes itself to fit its92 // children horizontally, and tries to be as tall as its parent.93 //94 // Invoke "debug painting" (press "p" in the console, choose the95 // "Toggle Debug Paint" action from the Flutter Inspector in Android96 // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)97 // to see the wireframe for each widget.98 //99 // Column has various properties to control how it sizes itself and100 // how it positions its children. Here we use mainAxisAlignment to101 // center the children vertically; the main axis here is the vertical102 // axis because Columns are vertical (the cross axis would be103 // horizontal).104 mainAxisAlignment: MainAxisAlignment.center,105 children: <Widget>[106 Text(107 'You have pushed the button this many times:',108 ),109 Text(110 '$_counter',111 style: Theme.of(context).textTheme.headline4,112 ),113 ElevatedButton(114 onPressed: () async {115 final prefs = await SharedPreferences.getInstance();116 await prefs.remove('counter_value');117
118 final value = prefs.getInt('counter_value') ?? 0;119 setState(() => _counter = value);120 },121 child: Text('Reset'),122 )123 ],124 ),125 ),126 floatingActionButton: FloatingActionButton(127 onPressed: _incrementCounter,128 tooltip: 'Increment',129 child: Icon(Icons.add),130 ), // This trailing comma makes auto-formatting nicer for build methods.131 );132 }133}
What I have showcased above is a very, very basic implementation of shared_preferences
using the sample counter app. When you're creating an app that you will have to maintain(possibly for years to come?) you should use a more complex implementation that includes utility classes(or repositories) and dependency injection. I mostly use Riverpod and Provider for state management and dependency injection in my own projects. I also recommend writing unit tests, but that is a topic for another day.