data_channel 5.0.0
data_channel: ^5.0.0 copied to clipboard
data_channel (DC) is a simple dart utility for handling exceptions and data routing.
Introduction #
Data Channel for Dart and Flutter.
data_channel (DC) is a lightweight, type-safe utility for exception handling and null safety in
Dart applications. Rather than cluttering your code with try-catch blocks and null checks,
data_channel provides a clean, composable way to handle errors, data, and optional values.
An Opinionated Alternative to Result and Option Types:
data_channel is an opinionated alternative to traditional Result/Either and Option/Maybe
patterns found in functional programming. Unlike standalone implementations, DC tightly couples
error handling with optional data through its integrated Option type, providing maximum
convenience and compile-time type safety.
Core Concepts:
- DC (Data Channel) - Represents either an error or optional data (similar to Result/Either types)
- Option - Represents presence (Some) or absence (None) of a value (eliminates nullable types)
- Non-Nullable Guarantee -
Somealways contains non-null values; useNonefor absence - Tight Coupling - DC always wraps data in
Option<Data>, enforcing explicit null handling - Type Safety - Explicit error and data types in your function signatures
- Composability - Chain operations without repetitive error/null checking
Prerequisites #
Breaking Changes in 4.0.0:
- Complete API redesign with Option type
- Requires Dart 3.0+ for sealed class support
What version to use
- Use
data_channel5.0.0 and above for Dart SDK >= 3.0.0 - Use
data_channel3.0.0+2 for Dart SDK >= 2.12.0 and < 3.0.0
Installation #
Visit https://pub.dev/packages/data_channel#-installing-tab- for the latest version of
data_channel
API Reference #
Data Channel (DC) API #
Constructors
DC.error(error)- Creates a DC containing an errorDC.auto(nullableData)- Automatically createsSomeif non-null,Noneif nullDC.some(data)- Creates a DC with NON-NULL data wrapped inSomeDC.none()- Creates a DC with no data (None)DC.fromOption(option)- Creates a DC from an existing Option without double-wrapping
Instance Methods
hasError- Returns true if this is a DCErrorhasOptionalData- Returns true if this is DCData (withSomeorNone), false if DCError- Note: To check if actual data is present, use the Option's
isSome/isNonemethods
- Note: To check if actual data is present, use the Option's
fold<U>({onError, onData})- Pattern match on error or data, returning a valuemapError<NewErr>(transform)- Transform the error type while preserving data
Static Methods
DC.forwardErrorOr(dc, newData)- Forward error if present, otherwise create new DCData with provided dataDC.forwardErrorOrNull(dc)- Forward error if present, otherwise create DCData withNoneDC.forwardErrorOrElse(dc, builder)- Forward error if present, otherwise lazily call builder to transform data
Option API #
The Option<T> type represents optional values and is used within DCData to hold the actual data.
Constructors
Some(value)- Creates an Option containing a NON-NULL valueNone()- Creates an empty Option (useNone<T>()for explicit typing)Option.auto(nullable)- Automatically creates Some if non-null,Noneif null (requires explicit type for null:Option<T>.auto(null))
Instance Methods
isSome- Returns true if contains a value (guaranteed non-null)isNone- Returns true if emptytryMaybe()- Returns the value or nullorElse(defaultValue)- Returns the value or a defaultorElseGet(getDefault)- Returns the value or computes a default (lazy)map<U>(transform)- Transform the contained valueflatMap<U>(transform)- Transform and flatten nested Optionsfilter(predicate)- Returns None if predicate failsfold<U>({onSome, onNone})- Pattern match on Some or None
Type Aliases
DCOption<T>- Alias forOption<T>DCSome<T>- Alias forSome<T>DCNone<T>- Alias forNone<T>
Examples #
Non-Nullable Guarantee #
With the extends Object constraint, Some ALWAYS contains non-null values:
void main() {
final userOption = Some(User('1', 'Alice'));
if (userOption.isSome) {
// Safe to unwrap - guaranteed non-null!
final user = userOption.tryMaybe()!;
print('User: ${user.name}'); // No null check needed
}
// None represents absence, not null value
final emptyOption = None<User>();
print(emptyOption.isNone); // true
print(emptyOption.tryMaybe()); // null (returns null, doesn't contain null)
}
⚠️ Warning: Type casting and dynamic can bypass this guarantee. Use DC.auto() or
Option.auto() for nullable values instead of casting.
void main() {
// Unsafe - can crash at runtime
dynamic value = null;
Some(value as User);
// Safe - handles null correctly
DC.auto(nullableUser);
Option.auto(nullableUser);
}
DC.auto - Auto-handle Nullable Values #
Automatically wraps nullable values in the appropriate Option variant. Use this to eliminate manual null checks when working with nullable data:
Future<DC<Exception, User>> getUserById(String id) async {
try {
final User? user = await database.findUser(id); // might be null
// Automatically creates Some(user) or None() based on null check
return DC<Exception, User>.auto(user);
// Instead of manual:
// return user != null ? DC.some(user) : DC.none();
} on Exception catch (e) {
return DC.error(e);
}
}
Creates a DC from an existing Option WITHOUT double-wrapping. Use this when you already have an Option from another operation and want to lift it into a DC:
Future<DC<Exception, Profile>> getProfileFromCache() async {
try {
// Cache returns Option<Profile>
final Option<Profile> cachedProfile = cache.get('profile');
// Lift Option directly into DC WITHOUT creating Option<Option<Profile>>
return DC<Exception, Profile>.fromOption(cachedProfile);
// Instead of manual:
// return cachedProfile.fold(
// onSome: (p) => DC.some(p),
// onNone: () => DC.none(),
// );
} on Exception catch (e) {
return DC.error(e);
}
}
Basic Usage #
Return either error or data from any method:
import 'package:data_channel/data_channel.dart';
Future<DC<Exception, LoginModel>> getSomeLoginData() async {
try {
return DC<Exception, LoginModel>.some(someData);
} on Exception catch (e) {
return DC<Exception, LoginModel>.error(e);
}
}
Check for errors in the calling code:
void doSomething() async {
final value = await getSomeLoginData();
if (value.hasError) {
// handle error case
} else if (value.hasOptionalData) {
// handle success case - but data might be None!
// Use fold or access .data to check Option
}
}
Using DC.fold #
Handle both cases exhaustively with a clean functional approach:
final message = result.fold(
onError: (error) => 'Operation failed: $error',
onData: (dataOption) =>
dataOption.fold(
onSome: (data) => 'Success: $data',
onNone: () => 'No data available',
),
);
// Use in widget building
final widget = result.fold(
onError: (error) => ErrorWidget(error),
onData: (dataOption) =>
dataOption.fold(
onSome: (data) => SuccessView(data),
onNone: () => EmptyStateWidget(),
),
);
DC.forwardErrorOr - Propagate Errors #
Eliminate redundant error checks when transforming data models. DC.forwardErrorOr automatically
propagates errors while allowing you to transform successful data:
Future<DC<Exception, UserModel>> checkSomethingAndReturn() async {
final loginData = await getSomeLoginData();
// If loginData has error, it's forwarded automatically
// If loginData has success, old data is discarded and new UserModel is wrapped
return DC.forwardErrorOr(
loginData,
UserModel(id: 'some-id'),
);
}
DC.forwardErrorOrNull - Propagate Errors, Discard Data #
When you want to acknowledge success but don't need to return data:
Future<DC<Exception, void>> deleteUser(String id) async {
final result = await apiDeleteUser(id);
// Forward error if present, otherwise return success with no data
return DC.forwardErrorOrNull(result);
}
DC.forwardErrorOrElse - Transform with Option Methods #
The most powerful forwarding method - use Option transformations on the data:
Future<DC<Exception, Profile>> createProfile() async {
final userResult = await fetchUser();
// Return Some directly
return DC.forwardErrorOrElse(
userResult,
(_) => Some(Profile.defaultProfile()),
);
}
// Transform the data
Future<DC<Exception, String>> getUserName() async {
final userResult = await fetchUser();
return DC.forwardErrorOrElse(
userResult,
(userData) => userData.map((user) => user.name),
);
}
// Filter with validation
Future<DC<Exception, User>> getVerifiedUser() async {
final userResult = await fetchUser();
return DC.forwardErrorOrElse(
userResult,
(userData) => userData.filter((user) => user.isVerified),
);
}
// Provide fallback
Future<DC<Exception, String>> getDisplayName() async {
final userResult = await fetchUser();
return DC.forwardErrorOrElse(
userResult,
(userData) => Some(userData.map((user) => user.name).orElse('Guest')),
);
}
DC.mapError - Transform Errors #
Transform error types while preserving data:
// Convert technical errors to user-friendly messages
final userFriendly = apiResult.mapError(
(error) => UserFacingException(error.message),
);
// Chain error transformations
final logged = apiResult
.mapError((e) => logError(e))
.mapError((e) => UserFacingException(e.message));
Working with Option Inside DCData #
When you have DCData, you can work with the inner Option:
void main() {
final result = await getUserData();
result.fold(
onError: (e) => print('Error: $e'),
onData: (userOption) {
// userOption is Option<User>
// Check if data exists
if (userOption.isSome) {
final user = userOption.tryMaybe()!; // Safe - guaranteed non-null
print('User: ${user.name}');
}
// Or use Option methods
final name = userOption
.map((user) => user.name)
.orElse('Anonymous');
// Or use fold
userOption.fold(
onSome: (user) => print('Found: ${user.name}'),
onNone: () => print('No user data'),
);
},
);
}
Example: HTTP API with DC #
Return data or error from an API call without leaking try-catch everywhere:
import 'dart:convert';
import 'package:data_channel/data_channel.dart';
import 'package:http/http.dart' as http;
Future<DC<Exception, StarwarsResponse>> getStarwarsCharacters() async {
try {
final response = await http.get(
Uri.parse('https://starwars-api.com/characters'),
);
if (response.statusCode != 200) {
return DC.error(Exception('HTTP ${response.statusCode}'));
}
if (response.body.isEmpty) {
return DC.none(); // Success but no data
}
final data = StarwarsResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
return DC.some(data);
} on Exception catch (e) {
return DC.error(e);
}
}
Example: Consuming the API Result #
void loadCharacters() async {
final result = await getStarwarsCharacters();
final message = result.fold(
onError: (error) => 'Failed to load: $error',
onData: (characters) =>
characters.fold(
onSome: (data) => 'Loaded ${data.count} characters',
onNone: () => 'No characters found',
),
);
print(message);
}
Example: Chaining Operations #
Build complex workflows by chaining DC operations:
void main() {
Future<DC<Exception, ProfileView>> loadUserProfile(String userId) async {
// Step 1: Fetch user
final userResult = await fetchUser(userId);
// Step 2: Validate user is verified, forward error if any
final verifiedUserResult = DC.forwardErrorOrElse(
userResult,
(userData) => userData.filter((user) => user.isVerified),
);
// Step 3: Load profile data, forward error if any
final profileResult = DC.forwardErrorOrElse(
verifiedUserResult,
(userData) => userData.map((user) => user.profileId),
);
// Step 4: Transform to view model
return DC.forwardErrorOrElse(
profileResult,
(profileIdOption) =>
profileIdOption.map(
(profileId) => ProfileView(id: profileId, verified: true),
),
);
}
// Use it
final result = await loadUserProfile('123');
result.fold(
onError: (e) => showError('Could not load profile: $e'),
onData: (profileView) =>
profileView.fold(
onSome: (view) => showProfile(view),
onNone: () => showMessage('User not verified'),
),
);
}
Example: Flutter Widget #
class UserProfileWidget extends StatelessWidget {
final DC<Exception, User> userResult;
const UserProfileWidget({required this.userResult});
@override
Widget build(BuildContext context) {
return userResult.fold(
onError: (error) =>
ErrorCard(
message: 'Failed to load user',
error: error,
),
onData: (userData) =>
userData.fold(
onSome: (user) =>
UserCard(
name: user.name,
email: user.email,
verified: user.isVerified,
),
onNone: () =>
const EmptyStateCard(
message: 'No user data available',
),
),
);
}
}
Example: Option Standalone Usage #
You can also use Option independently for nullable value handling:
import 'package:data_channel/data_channel.dart';
// Create Options
final some = Some(42);
final none = None<int>();
final fromNullable = Option<int>.auto(possiblyNullValue); // Explicit type needed
// Transform values
final doubled = some.map((x) => x * 2); // Some(84)
final noneDoubled = none.map((x) => x * 2); // None()
// Provide defaults
final value1 = some.orElse(0); // 42
final value2 = none.orElse(0); // 0
// Filter
final evenOnly = some.filter((x) => x.isEven); // Some(42)
final oddOnly = some.filter((x) => x.isOdd); // None()
// Chain operations
final result = Option.auto(user)
.filter((u) => u.isActive)
.map((u) => u.email)
.map((e) => e.toLowerCase())
.orElse('no-email@example.com');
// Pattern matching
final message = option.fold(
onSome: (value) => 'Got: $value',
onNone: () => 'Nothing here',
);
Buy me a coffee #
Help keep this package free and open for everyone.
PayPal: paypal.me/ganeshrvel
Contact #
Feel free to reach out at ganeshrvel@outlook.com
About #
- Author: Ganesh Rathinavel
- License: MIT
- Package: pub.dev/packages/data_channel
- Repository: github.com/ganeshrvel/pub-data-channel
- Email: ganeshrvel@outlook.com
License #
data_channel | Data Channel for Dart and Flutter
MIT License
Copyright © 2018-Present Ganesh Rathinavel