Crystalline
Note: This library is currently in early development. Documentation is not yet ready, and the API is subject to changes as I continue to refine the core concepts.
The Core Idea
Crystalline is a state management solution built on a simple premise: Everything that exists is data, and everything that is done is a manipulation of that data.
While many state management libraries focus on complex flows or boilerplate-heavy patterns, Crystalline treats state as a living piece of data that inherently reflects its own lifecycle. Whether data is being read, updated, created, or deleted, those transitions shouldn't just be "side effects"—they should be first-class properties of the state itself.
In Crystalline, state is not just a value; it's a Data object that knows its current operation, its failures, and its history. This makes the relationship between data manipulation and the UI observer transparent and effortless.
Quick Example
Here is how you can manage an asynchronous operation like fetching a user profile.
1. Define and Manipulate Data
Instead of manually managing loading booleans and error strings, the Data object tracks the state of the operation for you.
// Define a piece of state for a UserProfile
final userProfile = Data<UserProfile>();
// Perform an async operation
Future<void> fetchUserProfile() async {
// Set operation to 'read' to indicate loading
userProfile.operation = Operation.read;
try {
final profile = await api.getUserProfile();
// Setting the value automatically updates observers
userProfile.value = profile;
} catch (e) {
userProfile.failure = Failure(e.toString());
} finally {
// Reset operation to 'none' when finished
userProfile.operation = Operation.none;
}
}
2. Observe with DataBuilder
DataBuilder gives you full control over how to render the state based on its current properties.
DataBuilder(
data: userProfile,
builder: (context, data) {
if (data.isReading) return CircularProgressIndicator();
if (data.hasFailure) return Text('Error: ${data.failure}');
if (data.hasValue) return Text('Welcome, ${data.value.name}');
return Text('No profile loaded');
},
)
3. Simplify with WhenDataBuilder
For a more declarative approach, you can use WhenDataBuilder to handle different states of your data explicitly.
WhenDataBuilder(
data: userProfile,
onRead: (context, data) => CircularProgressIndicator(),
onFailure: (context, data) => Text('Error: ${data.failure}'),
onValue: (context, data) => Text('Welcome, ${data.value.name}'),
onNoValue: (context, data) => Text('No profile loaded'),
)
Features & Roadmap
- Everything is Data: Crystalline provides specialized data classes like
Data,ListData,CollectionData, andOperationDatato handle different state shapes. - Built-in Builders: Reactive widgets like
DataBuilder,StoreBuilder, andWhenDataBuildermake it easy to consume state changes without manual listeners. - Store System: A structured
Storeclass to organize multiple states, with upcoming support for Code Generation to automatically produce custom data classes and builders. - Semantic Operations: State naturally tracks whether it is
isReading,isUpdating, or has afailure, allowing you to build robust UIs that respond to every stage of a data's lifecycle.
Crystalline exists to make state management feel like what it actually is: simple data manipulation.
Libraries
- annotations
- crystalline
- Crystalline