Containers topic

Table of Contents

What are Containers?

A container is a logical grouping of objects within a storage system. Containers can be used to organize and manage data, as well as to apply policies and settings to a group of objects. Containers are similar to directories or folders in a file system, but they are not hierarchical. Instead, containers are flat and can contain any number of objects.

They are namespaced, meaning that each container has a unique name within the storage system. Containers can be used to isolate/group/categorize data, making it easier to manage and retrieve.

For example, you might create a container for storing settings, another for storing user data, another for caching feed, favorites, downloads, etc.

Using Containers

To use containers in Hyper Storage, you can create a new container using the container method. This method returns a Container object that you can use to interact with the container.

If the container does not exist, it will be created automatically. If it already exists, the existing container will be returned.

import 'package:hyper_storage/hyper_storage.dart';
import 'package:hyper_storage_shared_preferences/shared_preferences_backend.dart';

void main() async {
    final storage = await HyperStorage.init(backend: SharedPreferencesBackend());
    
    // Create or get a container named 'settings'
    final settingsContainer = await storage.container('settings');
    
    // Use the container to store and retrieve data
    await settingsContainer.setString('theme', 'dark');
    final theme = await settingsContainer.getString('theme');
    print('Current theme: $theme'); // Output: Current theme: dark
}

You can perform all operations on a container that you can perform on the main storage instance, such as set, get, delete, clear, etc. All the supported data types are also supported within containers.

await settingsContainer.setDouble('volume', 0.8);
await settingsContainer.setBool('notifications', true);
await settingsContainer.setDateTime('last_updated', dateTime);

Containers are a powerful feature of Hyper Storage that can help you organize and manage your data more effectively. Think of them like boxes where you can store related items together for easy access and management. You can have multiple boxes (containers) for different purposes, such as one for user settings, another for app data, and so on.

JSON Serializable Containers

Containers also support storing and retrieving JSON serializable objects. You can use these kind of containers to store complex data structures in a structured way. For example, storing a list of Todo items with Todo class.

final todos = await storage.jsonContainer<Todo>(
  'todos',
  fromJson: Todo.fromJson,
  toJson: (todo) => todo.toJson(),
);

await todos.set('task1', Todo(id: 'task1', title: 'Buy groceries', isCompleted: false));
final todo = await todos.get('task1');
await todos.remove('task1');

Providing a custom ID

By default, serializable containers would generate a unique String ID for each object you store. However, you can also provide a custom ID by providing a idGetter function when creating the container.

final todos = await storage.jsonContainer<Todo>(
  'todos',
  fromJson: Todo.fromJson,
  toJson: (todo) => todo.toJson(),
  idGetter: (todo) => todo.id,
);

await todos.add(Todo(id: 't1', title: 'Buy groceries', isCompleted: false));
await todos.get('t1');
await todos.remove('t1');

Custom Serializable Containers

While Hyper Storage provides built-in support for JSON serialization, you can also create containers for custom serialization formats by extending the SerializableStorageContainer class. This allows you to define your own serialization and deserialization logic.

class XmlSerializableContainer<E extends Object> extends SerializableStorageContainer<E> {
  XmlSerializableContainer({
    required super.backend,
    required super.name,
    super.delimiter,
  });

  @override
  E deserialize(String value) {
    // Convert the stored XML string back into your object type
    return parseXml<E>(value);
  }

  @override
  String serialize(E value) {
    // Convert your object into an XML string for storage
    return toXml(value);
  }
}

// Helper functions that convert between your XML representation and the Dart object.
E parseXml<E>(String xml) => throw UnimplementedError();
String toXml(Object value) => throw UnimplementedError();

// Register the custom container with Hyper Storage
final posts = await storage.objectContainer<Post, XmlSerializableContainer<Post>>(
  'posts',
  factory: () => XmlSerializableContainer<Post>(
    backend: storage.backend,
    name: 'posts',
  ),
);

// storage.backend exposes the underlying backend instance that Hyper Storage uses internally.

Container Delimiter

The way containers are implemented is by prefixing the keys with the container name followed by a delimiter. By default, the delimiter is a triple underscore (___). For example, if you have a container named settings and you store a key theme with value dark, the actual key stored in the backend would be settings___theme.

This is done to isolate the keys into a namespace, preventing key collisions between different containers. You can change the delimiter for serializable containers by providing the delimiter argument when creating them.

final todos = await storage.jsonContainer<Todo>(
  'todos',
  fromJson: Todo.fromJson,
  toJson: (todo) => todo.toJson(),
  delimiter: '--',
);

Classes

HyperStorageContainer Containers
A concrete implementation of StorageContainer for storing key-value pairs.