arrow-left arrow-right brightness-2 chevron-left chevron-right facebook-box facebook loader magnify menu-down rss-box star twitter-box youtube-box twitter white-balance-sunny window-close
How to Save Data to LocalStorage and SharedPreferences in Flutter
6 min read

How to Save Data to LocalStorage and SharedPreferences in Flutter

How to Save Data to LocalStorage and SharedPreferences in Flutter

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:

  1. The name ILocalStorageRepository could lend itself to being too tied to the localstorage 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.
  2. Our ILocalStorageRepository contract is not representative of the SharedPreferences implementation, as we're using getInt and setInt 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