essential_dart

Reusable building blocks, patterns, and services for Dart to improve efficiency and code quality.

Features

  • Memoizer: Cache results of expensive computations with lazy/eager execution and reset functionality.
  • Task: Type-safe state management for asynchronous operations with intuitive state transitions.
  • TaskGroup: Manage collections of tasks with aggregate state tracking and batch operations.

Getting started

Add this to your package's pubspec.yaml file:

dependencies:
  essential_dart: ^1.3.0

Usage

Memoizer

Cache expensive computations:

import 'package:essential_dart/essential_dart.dart';

final memoizer = Memoizer<int>(
  computation: () => expensiveCalculation(),
);

final result = await memoizer.result; // Computed once, cached
final again = await memoizer.result;  // Returns cached value

Task

Manage asynchronous operation states with clean, type-safe transitions:

import 'package:essential_dart/essential_dart.dart';

// Using the SimpleTask type alias (recommended for most cases)
var task = SimpleTask<String>.pending(label: 'fetch-users');

// Or use explicit types for custom Label/Tags
var customTask = Task<String, String?, Set<String>>.pending(
  label: 'fetch-users',
  tags: {'api', 'critical'},
);

// Transition to running
task = task.toRunning();

// Transition to success with data
task = task.toSuccess('User data loaded');

// Or handle failure
task = task.toFailure(Exception('Network error'));

// Retry after failure
task = task.toRetrying();

// Refresh with existing data
task = task.toRefreshing();

Custom Label and Tags Types

You can use custom types for labels and tags for better type safety:

enum TaskLabel { fetching, processing, completed }
enum TaskTag { critical, background, userInitiated }

var task = Task<int, TaskLabel, Set<TaskTag>>.pending(
  label: TaskLabel.fetching,
  tags: {TaskTag.critical, TaskTag.userInitiated},
);

Task States

Tasks can be in one of six states:

  • Pending: Initial state, waiting to start
  • Running: Operation in progress
  • Refreshing: Reloading with previous data available
  • Retrying: Retrying after a failure
  • Success: Operation completed successfully
  • Failure: Operation failed with an error

State Checking

if (task.state == TaskState.success) {
  print('Data: ${task.success.data}');
}

// Or use convenience getters
if (task.isSuccess) {
  print('Success!');
}

// Access data type-safely (throws if state doesn't match)
if (task.isSuccess) {
  print(task.success.data); 
}

Chaining Transitions

final result = SimpleTask<int>.pending()
    .toRunning()
    .toSuccess(42)
    .toRefreshing();

Data Transformation

// Transform data while preserving state
final task = Task<int>.success(data: 42);
final doubled = task.mapData((data) => data * 2);

// Transform error
final failure = Task<int>.failure(error: Exception('Error'));
final wrapped = failure.mapError((e) => CustomError(e));

Capturing Execution

Execute synchronous or asynchronous callbacks and automatically capture the result as a Task:

// Synchronous execution
final task = Task.runSync(() => int.parse('42'));

// Asynchronous execution
final asyncTask = await Task.run(() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Result';
});

Watching Execution

Create a stream that emits Task states (running, success, failure) as the callback executes:

Task.watch(() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Loaded';
}).listen((task) {
  if (task.isRunning) print('Running...');
  if (task.isSuccess) print('Data: ${task.effectiveData}');
  if (task.isFailure) print('Error: ${(task as TaskFailure).error}');
});

Caching

Cache task results to improve performance and reduce redundant computations:

// Memoize - cache indefinitely
final task = Task<int>.pending(
  cachingStrategy: CachingStrategy.memoize,
);

final result1 = await task.execute(() async => expensiveComputation());
final result2 = await task.execute(() async => expensiveComputation());
// result2 uses cached value, computation runs only once

// Temporal - cache for a duration
final apiTask = Task<User>.pending(
  cachingStrategy: CachingStrategy.temporal,
  cacheDuration: Duration(minutes: 5),
);

final user = await apiTask.execute(() async => fetchUser());
// Subsequent calls within 5 minutes use cached result

// Refresh cached data
final freshData = await task.refresh(() async => fetchFreshData());

// Invalidate cache manually
task.invalidateCache();

TaskGroup

Manage multiple tasks as a single unit:

// Create a group of tasks
var group = TaskGroup<int>.uniform({
  'users': Task.pending(),
  'posts': Task.pending(),
});

// Run all tasks
group = await group.runAll((key, task) async {
  return await fetchCount(key);
});

// Check aggregate state
if (group.isCompleted) {
  print('All done!');
} else if (group.isPartial) {
  print('Some failed or pending');
}

// Access individual tasks type-safely
final userCount = group.getTask<TaskSuccess<int>>('users');
if (userCount != null) {
  print('Users: ${userCount.data}');
}

// Retry only failed tasks
group = await group.retryFailed((key, task) => fetchCount(key));

// Customize execution strategy (parallel or sequential)
// Default is TaskGroupExecutionStrategy.parallel
group = await group.runAll(
  (key, task) => fetchCount(key),
  strategy: TaskGroupExecutionStrategy.sequential,
);

Retry

Handle temporary failures with configurable retry strategies:

import 'package:essential_dart/essential_dart.dart';

// Static methods for one-off operations
final result = await Retry.run(() => fetchData());

// Exponential backoff for network requests
await Retry.withExponentialBackoff(
  () => api.fetchUser(userId),
  initialDuration: Duration(milliseconds: 500),
  multiplier: 2.0,
  maxDelay: Duration(seconds: 5),
  maxAttempts: 5,
);

// Linear backoff with progress logging
await Retry.withLinearBackoff(
  () => uploadFile(file),
  initialDuration: Duration(seconds: 1),
  increment: Duration(seconds: 1),
  maxAttempts: 4,
  onRetry: (error, attempt) {
    print('Upload failed: $error. Retrying (attempt $attempt)...');
    return true; // Return false to abort retry
  },
);

// Reusable instances for multiple operations
final networkRetry = Retry(
  maxAttempts: 5,
  strategy: ExponentialBackoffStrategy(
    initialDuration: Duration(milliseconds: 500),
    multiplier: 2.0,
    maxDelay: Duration(seconds: 5),
  ),
);

// Use the same retry configuration across multiple operations
await networkRetry(() => fetchComments());

Interval

Work with generic ranges of comparable values (int, double, DateTime, etc.):

// Integer intervals
final range = Interval(1, 10);
print(range.contains(5)); // true
print(range.length);      // 9 (for num/int)

// DateTime intervals
final period = Interval(
  DateTime(2023, 1, 1),
  DateTime(2023, 1, 31),
);
print(period.duration.inDays); // 30

// Set operations
final a = Interval(10, 20);
final b = Interval(15, 25);

if (a.overlaps(b)) {
  final intersection = a.intersection(b); // [15, 20]
  final span = a.span(b);                 // [10, 25]
}

Stream Transformers

Powerful stream transformers for common patterns:

StringSplitter

Split string streams by separator (supports single or multi-character separators):

import 'package:essential_dart/essential_dart.dart';

// Split by newline (default)
Stream.fromIterable(['line1\nline2', '\nline3'])
  .transform(StringSplitter())
  .listen(print); // Prints: line1, line2, line3

// Split by custom separator
Stream.fromIterable(['a,b', ',c'])
  .transform(StringSplitter(','))
  .listen(print); // Prints: a, b, c

// Multi-character separator
Stream.fromIterable(['a--b', '--c'])
  .transform(StringSplitter('--'))
  .listen(print); // Prints: a, b, c

Debounce

Filter out rapid-fire events, emitting only after a quiet period:

// Search as user types
searchController.stream
  .transform(Debounce(Duration(milliseconds: 300)))
  .listen(performSearch);

Throttle

Limit event rate by ignoring events within a time window:

// Prevent double-clicks
buttonClickStream
  .transform(Throttle(Duration(milliseconds: 500)))
  .listen(handleClick);

BufferCount

Collect items into fixed-size batches:

// Process data in batches of 10
dataStream
  .transform(BufferCount(10))
  .listen((batch) => processBatch(batch));

BufferTime

Collect items over time intervals:

// Aggregate events every second
eventStream
  .transform(BufferTime(Duration(seconds: 1)))
  .listen((events) => processEvents(events));

License

This project is licensed under the MIT License.

Libraries

essential_dart
Reusable building blocks, patterns, and utilities for Dart.