π Ephemeral Value
Ephemeral Value is a Dart library for representing and managing the transient (ephemeral) state of any value. Effortlessly model loading, success, error, empty, and initial states in your Dart and Flutter apps with a simple, type-safe API.
β¨ Features
- Unified State Representation: Model loading, success, error, empty, and initial states with dedicated classes.
- Type Safety: All states are generic and strongly typed.
- Easy State Transitions: Built-in methods for transitioning between states.
- Equatable Support: All states are value-equal for easy comparison.
- Flutter & Dart Ready: Works seamlessly in both Dart and Flutter projects.
- Extension Methods: Convenient getters and type checking methods.
- Message Support: Optional messages for better state context.
π Getting Started
1. Install
Add to your pubspec.yaml
:
dependencies:
ephemeral_value: ^1.0.0
2. Import
import 'package:ephemeral_value/ephemeral_value.dart';
π§© Usage
Basic State Types
Ephemeral value types represent different states of a value:
NoneValue()
orNoneValue("Jane Doe")
: Initial nullable value (optional).InitialValue(guestUser)
: Initial non-null value.LoadingValue()
: Loading state (optional value).SuccessValue(userObj)
: Loaded successfully (non-null value).ErrorValue(null, errorObj)
: Error state (optional value, error object).EmptyValue()
: Loaded but empty or null.RefreshingValue()
: Background refresh state.StaleValue()
: Outdated but available data.
Example: State Class
class LoginState {
final Ephemeral<User> user;
final Ephemeral<List<Post>> posts;
LoginState({
this.user = const NoneValue(),
this.posts = const NoneValue(),
});
LoginState copyWith({
Ephemeral<User>? user,
Ephemeral<List<Post>>? posts,
}) {
return LoginState(
user: user ?? this.user,
posts: posts ?? this.posts,
);
}
}
Example: Bloc Logic
try {
emit(state.copyWith(user: LoadingValue(null)));
final user = await UserRepository().getUser(event.id);
emit(state.copyWith(user: SuccessValue(user)));
} catch (e) {
emit(state.copyWith(user: ErrorValue(null, e)));
// Or use transition methods:
emit(state.copyWith(user: state.user.toError()));
}
Example: Widget UI
Widget buildUserWidget(Ephemeral<User> user) {
if (user.isLoading) {
return const CircularProgressIndicator();
} else if (user.isSuccess) {
return Text('User: ${user.getSuccess.name}');
} else if (user.isError) {
return Text('Error: ${user.getError}');
} else if (user.isEmpty) {
return const Text('No user found');
} else {
return const Text('Initial state');
}
}
π Complete API Reference
Core Classes
Ephemeral<T>
Abstract base class for all ephemeral value types.
Properties:
T? value
- The underlying value (nullable)
Transition Methods:
toNone([T? value])
- Transition toNoneValue
toInitial([T? value, String? message])
- Transition toInitialValue
toLoading([T? value, String? message])
- Transition toLoadingValue
toSuccess([T? value, String? message])
- Transition toSuccessValue
toError([T? value, Object? error])
- Transition toErrorValue
toEmpty([T? value, String? message])
- Transition toEmptyValue
toRefreshing([T? value, String? message])
- Transition toRefreshingValue
toStale([T? value, DateTime? lastUpdated, String? message])
- Transition toStaleValue
NoneValue<T>
Represents an initial nullable value.
const NoneValue() // Null initial value
const NoneValue("Jane Doe") // With initial value
InitialValue<T>
Represents an initial non-null value.
const InitialValue(user) // With value only
const InitialValue(user, "Initial user") // With value and message
LoadingValue<T>
Represents a loading state.
const LoadingValue() // No value, no message
const LoadingValue(user) // With previous value
const LoadingValue(user, "Loading...") // With value and message
SuccessValue<T>
Represents a successfully loaded value.
const SuccessValue(user) // With value only
const SuccessValue(user, "Loaded successfully") // With value and message
ErrorValue<T>
Represents an error state.
const ErrorValue() // No value, no error
const ErrorValue(null, exception) // With error only
const ErrorValue(user, exception) // With previous value and error
EmptyValue<T>
Represents an empty or null result.
const EmptyValue() // No value, no message
const EmptyValue(null, "No data found") // With message
RefreshingValue<T>
Represents a background refresh state.
const RefreshingValue() // No value, no message
const RefreshingValue(user, "Refreshing...") // With value and message
StaleValue<T>
Represents outdated but available data.
const StaleValue(user) // With value only
const StaleValue(user, DateTime.now()) // With value and timestamp
const StaleValue(user, DateTime.now(), "Data is stale") // Complete
Extension Methods
The library provides convenient extension methods for type checking and value extraction:
Type Checking
ephemeral.isNone // bool
ephemeral.isInitial // bool
ephemeral.isLoading // bool
ephemeral.isSuccess // bool
ephemeral.isError // bool
ephemeral.isEmpty // bool
ephemeral.isRefreshing // bool
ephemeral.isStale // bool
Value Extraction
ephemeral.getNone // T? (throws if not NoneValue)
ephemeral.getInitial // T (throws if not InitialValue)
ephemeral.getLoading // T? (throws if not LoadingValue)
ephemeral.getSuccess // T (throws if not SuccessValue)
ephemeral.getError // Object? (throws if not ErrorValue)
ephemeral.getEmpty // T? (throws if not EmptyValue)
ephemeral.getRefreshing // T? (throws if not RefreshingValue)
ephemeral.getStale // T? (throws if not StaleValue)
Message Extraction
ephemeral.getInitialMessage // String? (throws if not InitialValue)
ephemeral.getLoadingMessage // String? (throws if not LoadingValue)
ephemeral.getSuccessMessage // String? (throws if not SuccessValue)
ephemeral.getEmptyMessage // String? (throws if not EmptyValue)
ephemeral.getRefreshingMessage // String? (throws if not RefreshingValue)
ephemeral.getStaleMessage // String? (throws if not StaleValue)
Special Getters
ephemeral.getStaleLastUpdated // DateTime? (throws if not StaleValue)
π οΈ Advanced Usage Examples
State Management with BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc() : super(UserState()) {
on<LoadUser>(_onLoadUser);
on<RefreshUser>(_onRefreshUser);
}
Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async {
try {
// Start loading
emit(state.copyWith(user: state.user.toLoading()));
final user = await userRepository.getUser(event.id);
if (user == null) {
emit(state.copyWith(user: state.user.toEmpty()));
} else {
emit(state.copyWith(user: state.user.toSuccess(user)));
}
} catch (e) {
emit(state.copyWith(user: state.user.toError(null, e)));
}
}
Future<void> _onRefreshUser(RefreshUser event, Emitter<UserState> emit) async {
try {
// Keep current value while refreshing
emit(state.copyWith(user: state.user.toRefreshing()));
final user = await userRepository.getUser(event.id);
emit(state.copyWith(user: state.user.toSuccess(user)));
} catch (e) {
// Keep current value on error
emit(state.copyWith(user: state.user.toError(null, e)));
}
}
}
Flutter Widget with Complete State Handling
class UserProfileWidget extends StatelessWidget {
final Ephemeral<User> user;
const UserProfileWidget({super.key, required this.user});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'User Profile',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
_buildUserContent(),
],
),
),
);
}
Widget _buildUserContent() {
if (user.isLoading) {
return const Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Loading user...'),
],
);
}
if (user.isSuccess) {
final userData = user.getSuccess;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${userData.name}'),
Text('Email: ${userData.email}'),
if (user.getSuccessMessage != null)
Text(
user.getSuccessMessage!,
style: const TextStyle(color: Colors.green),
),
],
);
}
if (user.isError) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.error, color: Colors.red),
Text('Error: ${user.getError}'),
if (user.value != null)
Text('Previous data may be available'),
],
);
}
if (user.isEmpty) {
return Column(
children: [
const Icon(Icons.person_off, color: Colors.grey),
Text(user.getEmptyMessage ?? 'No user found'),
],
);
}
if (user.isRefreshing) {
return Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Refreshing...'),
if (user.value != null)
Text('Current data: ${user.value!.name}'),
],
),
),
],
);
}
if (user.isStale) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.warning, color: Colors.orange),
const SizedBox(width: 8),
Text('Data may be outdated'),
],
),
Text('Name: ${user.getStale.name}'),
Text('Last updated: ${user.getStaleLastUpdated}'),
if (user.getStaleMessage != null)
Text(user.getStaleMessage!),
],
);
}
// Initial state
return const Text('No user data');
}
}
Complex State Management
class DashboardState {
final Ephemeral<User> currentUser;
final Ephemeral<List<Post>> posts;
final Ephemeral<Analytics> analytics;
DashboardState({
this.currentUser = const NoneValue(),
this.posts = const NoneValue(),
this.analytics = const NoneValue(),
});
DashboardState copyWith({
Ephemeral<User>? currentUser,
Ephemeral<List<Post>>? posts,
Ephemeral<Analytics>? analytics,
}) {
return DashboardState(
currentUser: currentUser ?? this.currentUser,
posts: posts ?? this.posts,
analytics: analytics ?? this.analytics,
);
}
bool get isLoading =>
currentUser.isLoading || posts.isLoading || analytics.isLoading;
bool get hasError =>
currentUser.isError || posts.isError || analytics.isError;
String? get errorMessage {
if (currentUser.isError) return 'User: ${currentUser.getError}';
if (posts.isError) return 'Posts: ${posts.getError}';
if (analytics.isError) return 'Analytics: ${analytics.getError}';
return null;
}
}
Transition Patterns
// Pattern 1: Force new value
Ephemeral<User> user = NoneValue();
user = NoneValue(newUser); // Forces new value
// Pattern 2: Retain current value
user = user.toLoading(); // Keeps current value, changes state
// Pattern 3: Conditional transitions
if (user.isSuccess) {
user = user.toRefreshing(); // Keep success value while refreshing
} else {
user = user.toLoading(); // Start fresh loading
}
// Pattern 4: Error handling with value retention
try {
user = user.toLoading();
final newUser = await fetchUser();
user = user.toSuccess(newUser);
} catch (e) {
user = user.toError(null, e); // Keep current value, add error
}
π€ Why Ephemeral Value?
- Reduce Boilerplate: No more manual state enums or flags.
- Consistent Patterns: Use the same approach for all async or transient values.
- Improved Readability: Clear, self-documenting state transitions.
- Testable: All states are value-equal and easy to test.
- Type Safe: Compile-time guarantees for state transitions.
- Extensible: Easy to add new state types if needed.
π¦ Pub.dev
π License
π‘ Contributing
Contributions, issues, and feature requests are welcome!
See issues.
Ephemeral Value β The easiest way to manage transient state in Dart and Flutter!
Libraries
- ephemeral_value
- Support for doing something awesome.