Working with Different Data Storage Options (Shared Preferences, Key-Value Stores) in Flutter

In Flutter, handling data persistence is essential for creating robust and user-friendly applications. Flutter provides several options for data storage, each with its own use cases and capabilities. This post explores various data storage options in Flutter, including Shared Preferences and key-value stores like Hive, along with practical code examples to help you make informed decisions about which method to use in your projects.

Overview of Data Storage Options in Flutter

Flutter apps often need to store and retrieve data to provide a consistent user experience across sessions. Here are some common data storage options available:

  • Shared Preferences: Simple key-value storage for primitive data types.
  • Key-Value Stores (Hive, Sembast): More robust local databases for structured data.

Why Choose the Right Data Storage?

Choosing the appropriate data storage method depends on the following factors:

  • Data Complexity: Whether you’re storing simple values or complex objects.
  • Data Size: The amount of data you need to store.
  • Performance Requirements: How quickly you need to read and write data.
  • Data Security: Whether the data needs to be encrypted.

1. Shared Preferences in Flutter

Shared Preferences is a simple mechanism to store key-value pairs of primitive data types: booleans, numbers, strings, and dates. It’s ideal for storing app settings, user preferences, and other simple data that doesn’t require a complex structure.

How to Use Shared Preferences

Step 1: Add the Shared Preferences Package

First, add the shared_preferences package to your pubspec.yaml file:

dependencies:
  shared_preferences: ^2.2.2

Run flutter pub get to install the dependency.

Step 2: Storing Data Using Shared Preferences

You can save data using the SharedPreferences instance:

import 'package:shared_preferences/shared_preferences.dart';

Future<void> saveData() async {
  final prefs = await SharedPreferences.getInstance();
  prefs.setString('username', 'JohnDoe');
  prefs.setInt('theme', 1);
  prefs.setBool('isLoggedIn', true);
}
Step 3: Retrieving Data Using Shared Preferences

You can retrieve data similarly:

import 'package:shared_preferences/shared_preferences.dart';

Future<void> loadData() async {
  final prefs = await SharedPreferences.getInstance();
  final username = prefs.getString('username') ?? 'Guest'; // Provide a default value
  final theme = prefs.getInt('theme') ?? 0;
  final isLoggedIn = prefs.getBool('isLoggedIn') ?? false;

  print('Username: $username');
  print('Theme: $theme');
  print('Logged In: $isLoggedIn');
}
Example Usage in a Flutter App

Here’s a simple Flutter widget that uses Shared Preferences to toggle a theme and persist the setting:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SettingsScreen extends StatefulWidget {
  @override
  _SettingsScreenState createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  bool _darkMode = false;

  @override
  void initState() {
    super.initState();
    _loadSettings();
  }

  Future<void> _loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _darkMode = prefs.getBool('darkMode') ?? false;
    });
  }

  Future<void> _saveSettings() async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setBool('darkMode', _darkMode);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings'),
      ),
      body: ListTile(
        title: Text('Dark Mode'),
        trailing: Switch(
          value: _darkMode,
          onChanged: (value) {
            setState(() {
              _darkMode = value;
              _saveSettings();
            });
          },
        ),
      ),
    );
  }
}

Limitations of Shared Preferences

  • Limited to simple data types.
  • Not suitable for storing complex data structures or large amounts of data.
  • Synchronous read and write operations can cause UI blocking if not handled carefully.

2. Key-Value Stores (Hive) in Flutter

For more complex data storage needs, Flutter offers robust key-value store databases like Hive. Hive is a lightweight and fast NoSQL database written in pure Dart.

Why Choose Hive?

  • Fast: Written in pure Dart, optimized for performance.
  • Easy to Use: Simple API for reading and writing data.
  • Isar Alternatives Supports Encryption: Data can be encrypted for sensitive information.
  • Multiplatform: Works on all Flutter supported platforms (Android, iOS, Web, Desktop).

How to Use Hive

Step 1: Add the Hive Dependencies

Add the following dependencies to your pubspec.yaml file:

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.2

dev_dependencies:
  build_runner: ^2.4.8
  hive_generator: ^2.0.1

Run flutter pub get to install the dependencies.

Step 2: Initialize Hive

Initialize Hive in your main function:

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  runApp(MyApp());
}
Step 3: Define a Hive Object (Model)

To store custom objects, you need to create an adapter. First, define a class that represents your data:

import 'package:hive/hive.dart';

part 'user.g.dart';

@HiveType(typeId: 0)
class User {
  @HiveField(0)
  String username;

  @HiveField(1)
  int age;

  User({required this.username, required this.age});
}
Step 4: Create a Hive Adapter

Run the following command in the terminal to generate the adapter file:

flutter packages pub run build_runner build
Step 5: Register the Adapter and Open the Box

Register the adapter and open the Hive box:

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'user.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  Hive.registerAdapter(UserAdapter());
  await Hive.openBox<User>('users');
  runApp(MyApp());
}
Step 6: Store and Retrieve Data

You can now store and retrieve data using the Hive box:

import 'package:hive/hive.dart';
import 'user.dart';

Future<void> storeUser(User user) async {
  final box = Hive.box<User>('users');
  await box.put(user.username, user); // Key is the username
}

Future<User?> getUser(String username) async {
  final box = Hive.box<User>('users');
  return box.get(username);
}

Future<void> deleteUser(String username) async {
 final box = Hive.box<User>('users');
  await box.delete(username);
}
Example Usage in a Flutter App

Here’s how you can use Hive in a Flutter app to store user information:

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'user.dart';

class HiveExampleScreen extends StatefulWidget {
  @override
  _HiveExampleScreenState createState() => _HiveExampleScreenState();
}

class _HiveExampleScreenState extends State<HiveExampleScreen> {
  final _usernameController = TextEditingController();
  final _ageController = TextEditingController();

  Future<void> _saveUser() async {
    final username = _usernameController.text;
    final age = int.tryParse(_ageController.text) ?? 0;

    if (username.isNotEmpty && age > 0) {
      final user = User(username: username, age: age);
      await storeUser(user);
      _usernameController.clear();
      _ageController.clear();
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hive Example'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _usernameController,
              decoration: InputDecoration(labelText: 'Username'),
            ),
            TextField(
              controller: _ageController,
              decoration: InputDecoration(labelText: 'Age'),
              keyboardType: TextInputType.number,
            ),
            ElevatedButton(
              onPressed: _saveUser,
              child: Text('Save User'),
            ),
            Expanded(
              child: ValueListenableBuilder<Box<User>>(
                valueListenable: Hive.box<User>('users').listenable(),
                builder: (context, box, _) {
                  final users = box.values.toList();
                  return ListView.builder(
                    itemCount: users.length,
                    itemBuilder: (context, index) {
                      final user = users[index];
                      return ListTile(
                        title: Text(user.username),
                        subtitle: Text('Age: ${user.age}'),
                      );
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    Hive.close();
    super.dispose();
  }
}

Benefits of Using Hive

  • Performance: Fast read and write operations due to its lightweight nature.
  • Simplicity: Easy to set up and use, with a straightforward API.
  • Type-Safe: Supports strong typing, reducing runtime errors.
  • Encryption: Provides encryption for sensitive data.

Conclusion

Flutter offers various data storage options to suit different needs. Shared Preferences is excellent for simple, lightweight data, while Hive provides a robust solution for complex data structures. Understanding the strengths and limitations of each option ensures you can make informed decisions and build efficient, data-driven Flutter applications.