RxStorageUtils
A powerful Flutter utility that seamlessly binds GetX reactive state with persistent storage, ensuring your UI state and device storage remain perfectly synchronized.
Table of Contents
- Features
- Installation
- Getting Started
- API Reference
- Advanced Examples
- Best Practices
- Troubleshooting
- Contributing
- License
Features
- 🔄 Reactive State Binding - Automatically sync GetX reactive variables with persistent storage
- 📦 Type-Safe Storage - Strongly typed data persistence with custom converters
- 📋 List Support - Special handling for reactive lists
- 🔒 Update Protection - Prevents infinite update loops with intelligent locking
- 🐞 Debug Mode - Detailed logging and performance tracking for troubleshooting
- ⚡ Performance Optimization - Minimizes storage writes by tracking actual changes
Installation
Add to your pubspec.yaml:
dependencies:
rx_storage_utils: ^1.0.0
get: ^4.6.5
get_storage: ^2.1.1
Getting Started
Initialization
Initialize the storage system in your app's main() method:
import 'package:rx_storage_utils/rx_storage_utils.dart';
void main() async {
// Initialize storage before runApp
await RxStorageUtils.initStorage();
// Enable debug mode during development
RxStorageUtils.setDebugMode(true, trackTiming: true);
runApp(MyApp());
}
Basic Usage
Binding a Simple Reactive Value
// Create a reactive variable
final RxString username = ''.obs;
// Bind it to persistent storage
await RxStorageUtils.bindReactiveValue<String>(
key: 'username',
rxValue: username,
onUpdate: (data) => print('Username updated: $data'),
onInitialLoadFromDb: (data) => print('Username loaded: $data'),
toRawData: (data) => data, // String can be stored directly
fromRawData: (data) => data.toString(),
defaultValue: 'Guest', // Provides a default value if key doesn't exist
autoSync: true, // automatically sync changes to storage
);
// Use the reactive value normally in your UI
// Any changes will be automatically persisted
username.value = 'JohnDoe';
Binding Complex Objects
class User {
final String name;
final int age;
User({required this.name, required this.age});
// Convert to JSON
Map<String, dynamic> toJson() => {
'name': name,
'age': age,
};
// Create from JSON
factory User.fromJson(Map<String, dynamic> json) => User(
name: json['name'] ?? '',
age: json['age'] ?? 0,
);
}
// Create a reactive user
final Rx<User> currentUser = User(name: '', age: 0).obs;
// Bind to storage with converters
await RxStorageUtils.bindReactiveValue<User>(
key: 'current_user',
rxValue: currentUser,
onUpdate: (data) => print('User updated'),
onInitialLoadFromDb: (data) => print('User loaded'),
toRawData: (data) => data.toJson(), // Convert to storable format
fromRawData: (data) => User.fromJson(data), // Convert back to User
);
Binding Lists
// Create a reactive list of strings
final RxList<String> todoItems = <String>[].obs;
// Bind the list to storage
await RxStorageUtils.bindReactiveListValue<String>(
key: 'todo_items',
rxList: todoItems,
onUpdate: (data) => print('Todo list updated'),
onInitialLoadFromDb: (data) => print('Todo list loaded with ${data?.length} items'),
itemToRawData: (item) => item, // String items can be stored directly
itemFromRawData: (data) => data.toString(),
);
// Use the list normally - changes are automatically persisted
todoItems.add('Buy groceries');
todoItems.add('Walk the dog');
API Reference
Initialization
// Initialize storage
static Future<void> initStorage() async
// Enable or disable debug mode
static void setDebugMode(bool enabled, {bool trackTiming = false})
Binding Reactive Values
static Future<void> bindReactiveValue<T>({
required String key,
required Rx<T> rxValue,
required Function(T? data) onUpdate,
required Function(T? data) onInitialLoadFromDb,
required dynamic Function(T data) toRawData,
required T Function(dynamic data) fromRawData,
T? defaultValue, // Optional default value when key doesn't exist
bool autoSync = true,
})
Binding Reactive Lists
static Future<void> bindReactiveListValue<T>({
required String key,
required RxList<T> rxList,
required Function(List<T>? data) onUpdate,
required Function(List<T>? data) onInitialLoadFromDb,
required dynamic Function(T item) itemToRawData,
required T Function(dynamic data) itemFromRawData,
List<T>? defaultValue, // Optional default list when key doesn't exist
bool autoSync = true,
})
Direct Storage Access
// Get a value without reactive binding
static T? getValue<T>({
required String key,
required T Function(dynamic data) fromRawData,
T? defaultValue,
})
// Set a value without reactive binding
static Future<bool> setValue<T>({
required String key,
required T value,
required dynamic Function(T data) toRawData,
})
// Check if a key exists
static bool hasKey(String key)
// Clear a specific key
static Future<void> clearKey(String key)
// Clear all storage
static Future<void> clearAll()
Advanced Examples
Custom Objects with List Binding
class Task {
final String id;
final String title;
final bool completed;
Task({required this.id, required this.title, this.completed = false});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'completed': completed,
};
factory Task.fromJson(Map<String, dynamic> json) => Task(
id: json['id'] ?? '',
title: json['title'] ?? '',
completed: json['completed'] ?? false,
);
}
// Reactive list of Task objects
final RxList<Task> tasks = <Task>[].obs;
// Bind to storage
await RxStorageUtils.bindReactiveListValue<Task>(
key: 'tasks',
rxList: tasks,
onUpdate: (data) => updateUI(),
onInitialLoadFromDb: (data) => initializeUI(),
itemToRawData: (task) => task.toJson(),
itemFromRawData: (data) => Task.fromJson(data),
);
Best Practices
- Initialize Early: Call
initStorage()before your app rendering starts - Use Strong Types: Always use properly typed converters (toRawData/fromRawData)
- Handle Null Values: Always check for null in onInitialLoadFromDb, especially on first run
- Use Default Values: Provide defaultValue parameter for a better first-run experience
- Error Handling: Add try/catch blocks in your converters for resilience
- Debug First: Enable debug mode during development with
setDebugMode(true) - Key Naming: Use consistent, descriptive key names with potential for namespacing
- Minimal Updates: Only modify the values that actually change to minimize storage writes
Troubleshooting
- Null Values: If onInitialLoadFromDb receives null, the key likely doesn't exist yet. Use defaultValue or handle null appropriately.
- If you experience update loops, check your
onUpdatehandlers for code that might modify the same value - For slow performance, consider using
trackTiming: trueto identify bottlenecks - Clear problematic keys using
clearKey()if data becomes corrupted
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details