How to Save Data to LocalStorage and SharedPreferences in Flutter
Paul Halliday
Posted on April 27, 2020
In this article we're going to investigate how we can create a simple integration with the localstorage
and shared_preferences
plugin inside of our Flutter applications. We'll be creating a StorageRepository
and StorageService
as their own package so we can include them in our other project(s) easily.
Project Setup
Let's create a new Flutter package:
# Create a new package with the --template flag
$ flutter create --template=package my_storage
# Open in editor
$ cd my_storage && code .
We can then head over to pubspec.yaml
and add the localstorage
package:
dependencies:
flutter:
sdk: flutter
localstorage: ^3.0.1+4
Using localstorage
Now that we've got created package and installed localstorage
, we can make an abstraction over localstorage
in the event that we want to swap this out with another persistence library in the future.
For now, we'll be concerned with two methods: getAll
and save
:
/// i_local_storage_repository.dart
abstract class ILocalStorageRepository {
Future getAll(String key);
Future<void> save(String key, dynamic item);
}
We can now create an implementation of this:
import 'package:localstorage/localstorage.dart';
import 'package:my_storage/i_local_storage_repository.dart';
class LocalStorageRepository implements ILocalStorageRepository {
final LocalStorage _storage;
LocalStorageRepository(String storageKey)
: _storage = LocalStorage(storageKey);
@override
Future getAll(String key) async {
await _storage.ready;
return _storage.getItem(key);
}
@override
Future<void> save(String key, dynamic value) async {
await _storage.ready;
return _storage.setItem(key, value);
}
}
Great. We've now have a LocalStorageRepository
which we can use to save and get data. We still don't want to interface with this repository directly, so we can create a LocalStorageService
:
/// local_storage_service.dart
import 'package:flutter/foundation.dart';
import 'package:my_storage/i_local_storage_repository.dart';
class LocalStorageService {
LocalStorageService(
{@required ILocalStorageRepository localStorageRepository})
: _localStorageRepository = localStorageRepository;
ILocalStorageRepository _localStorageRepository;
Future<dynamic> getAll(String key) async {
return await _localStorageRepository.getAll(key);
}
Future<void> save(String key, dynamic item) async {
await _localStorageRepository.save(key, item);
}
}
This takes in an injected ILocalStorageRepository
and allows us to call the contract. Now, providing we use the LocalStorageService
directly, we could swap out localstorage
for something different (like shared_preferences
but our usage would remain the same).
Persisting Counter State
We can see this in action by persisting counter state if we create a new Flutter project:
# New Flutter project
$ flutter create storage_counter
# Open in editor
$ cd my_counter && code .
We can then add our my_storage
package in the pubspec.yaml
by pointing it to the directory, GitHub repo, or other:
dependencies:
flutter:
sdk: flutter
my_storage:
path: '../my_storage'
In our typical application we'll have a service layer which we can use to call out to our LocalStorageService
and our preferred method of persistence. Here's an example of our small CounterService
which is able to get the current stored counter state, increment, and decrement:
/// lib/services/counter_service.dart
import 'package:flutter/foundation.dart';
import 'package:my_storage/i_local_storage_repository.dart';
import 'package:my_storage/local_storage_service.dart';
class CounterService {
final LocalStorageService _localStorageService;
final String _countKey = "count";
CounterService({
@required ILocalStorageRepository localStorageRepository,
}) : _localStorageService =
LocalStorageService(localStorageRepository: localStorageRepository);
Future<int> getCount() async {
return await _localStorageService.getAll(_countKey) ?? 0;
}
Future<int> incrementAndSave(int count) async {
count += 1;
await _localStorageService.save(_countKey, count);
return count;
}
Future<int> decrementAndSave(int count) async {
count -= 1;
await _localStorageService.save(_countKey, count);
return count;
}
}
We can use this if we update our main.dart
to contain a new CounterPage
at counter_page.dart
:
import 'package:flutter/material.dart';
import 'package:storage_counter/pages/counter_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: CounterPage(title: "Storage Counter"),
);
}
}
Inside of our CounterPage
we use a FutureBuilder
to get the current count, and otherwise, display a loading indicator. To update our count we get a new count from our CounterService
which also saves this to our persistence layer:
import 'package:flutter/material.dart';
import 'package:my_storage/local_storage_repository.dart';
import 'package:storage_counter/services/counter_service.dart';
class CounterPage extends StatefulWidget {
CounterPage({Key key, this.title}) : super(key: key);
final String title;
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter;
CounterService _counterService;
@override
void initState() {
super.initState();
_counter = 0;
_counterService = CounterService(
localStorageRepository: LocalStorageRepository("counter.json"));
}
Future<void> _incrementCounter() async {
final int _newCount = await _counterService.incrementAndSave(_counter);
setState(() {
_counter = _newCount;
});
}
Future<void> _decrementCounter() async {
final int _newCount = await _counterService.decrementAndSave(_counter);
setState(() {
_counter = _newCount;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
FutureBuilder<int>(
future: _counterService.getCount(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
_counter = snapshot.data;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: _decrementCounter,
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
IconButton(
icon: Icon(Icons.add),
onPressed: _incrementCounter,
),
],
);
}
return CircularProgressIndicator();
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Swapping out LocalStorage for SharedPreferences
What if we wanted to change our persistence layer? How about using SharedPreferences
instead? That's easy enough because of the work we've already done.
Add shared_preferences
to your pubspec.yaml
on the my_storage
package we created earlier:
dependencies:
flutter:
sdk: flutter
localstorage: ^3.0.1+4
shared_preferences: ^0.5.6+3
Creating a SharedPreferencesRepository
We can then create a SharedPreferencesRepository
which uses SharedPreferences
instead:
import 'package:my_storage/i_local_storage_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesRepository implements ILocalStorageRepository {
@override
Future getAll(String key) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getInt(key);
}
@override
Future<void> save(String key, item) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setInt(key, item);
}
}
Notice how we're implementing our ILocalStorageRepository
here, which contains our two key methods. Here are a couple of points to consider at this stage:
- The name
ILocalStorageRepository
could lend itself to being too tied to thelocalstorage
package if we looked at it from this lense. I feel it's still general enough at this stage, but it's certainly a consideration. - Our
ILocalStorageRepository
contract is not representative of theSharedPreferences
implementation, as we're usinggetInt
andsetInt
for this demonstration. Consider making this more general to account for other data type(s) if changing your persistence layer is likely required in the near future.
Let's swap out our LocalStorageRepository
for the SharedPreferencesRepository
in our CounterPage
:
class _CounterPageState extends State<CounterPage> {
int _counter;
CounterService _counterService;
@override
void initState() {
super.initState();
_counter = 0;
_counterService = CounterService(
localStorageRepository: SharedPreferencesRepository(),
);
}
//
}
If we try and increment our counter, you'll notice that things work exactly how they did before:
We can even display this in another way... using both persistence layers at once!
import 'package:flutter/material.dart';
import 'package:my_storage/local_storage_repository.dart';
import 'package:my_storage/shared_preferences_repository.dart';
import 'package:storage_counter/services/counter_service.dart';
class CounterPage extends StatefulWidget {
CounterPage({Key key, this.title}) : super(key: key);
final String title;
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _sharedPreferencesCounter;
int _localStorageCounter;
CounterService _sharedPreferencesCounterService;
CounterService _localStorageCounterService;
@override
void initState() {
super.initState();
_localStorageCounter = 0;
_sharedPreferencesCounter = 0;
_sharedPreferencesCounterService = CounterService(
localStorageRepository: SharedPreferencesRepository(),
);
_localStorageCounterService = CounterService(
localStorageRepository: LocalStorageRepository("counter.json"),
);
}
Future<void> _incrementCounter(bool useSharedPrefs) async {
final int _newCount = useSharedPrefs
? await _sharedPreferencesCounterService
.incrementAndSave(_sharedPreferencesCounter)
: await _localStorageCounterService
.incrementAndSave(_localStorageCounter);
_updateCount(useSharedPrefs, _newCount);
}
Future<void> _decrementCounter(bool useSharedPrefs) async {
final int _newCount = useSharedPrefs
? await _sharedPreferencesCounterService
.decrementAndSave(_sharedPreferencesCounter)
: await _localStorageCounterService
.decrementAndSave(_localStorageCounter);
_updateCount(useSharedPrefs, _newCount);
}
void _updateCount(bool useSharedPrefs, int count) {
setState(() {
useSharedPrefs
? _sharedPreferencesCounter = count
: _localStorageCounter = count;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
_buildSharedPreferencesCounter(),
_buildLocalStorageCounter()
],
),
),
);
}
FutureBuilder<int> _buildSharedPreferencesCounter() {
return FutureBuilder<int>(
future: _sharedPreferencesCounterService.getCount(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
_sharedPreferencesCounter = snapshot.data;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () => _decrementCounter(true),
),
Text(
'$_sharedPreferencesCounter',
style: Theme.of(context).textTheme.headline4,
),
IconButton(
icon: Icon(Icons.add),
onPressed: () => _incrementCounter(true),
),
],
);
}
return CircularProgressIndicator();
},
);
}
FutureBuilder<int> _buildLocalStorageCounter() {
return FutureBuilder<int>(
future: _localStorageCounterService.getCount(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
_localStorageCounter = snapshot.data;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () => _decrementCounter(false),
),
Text(
'$_localStorageCounter',
style: Theme.of(context).textTheme.headline4,
),
IconButton(
icon: Icon(Icons.add),
onPressed: () => _incrementCounter(false),
),
],
);
}
return CircularProgressIndicator();
},
);
}
}
Here's our output:
Yup. Totally not necessary, but a fun experiment nonetheless.
I hope you've found this useful! I'd love to hear your thoughts.
Code:
https://github.com/PaulHalliday/my_storage
https://github.com/PaulHalliday/storage_counter
Posted on April 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.