convenience_types 0.8.1
convenience_types: ^0.8.1 copied to clipboard
A package to ensamble convenience types commonly used through flutter projects developed by Capyba.
Convenience Types #
A package of convenience types commonly used in Flutter projects developed by Capyba.
Motivation #
Across our projects we have adopted types that keep code safer, less error-prone and, in the long run, more productive. In order to share those types between the projects we work, and possibly to inspire others to use those types too, we have created this package.
Table of contents #
Getting started #
To install and have the package good to go in a Flutter project, run:
flutter pub add convenience_types
If you're on a Dart project, run:
dart pub add convenience_types
Types #
Result #
A type-safe way to model operations that can succeed or fail. Result<ResultType> is a union type with two variants:
- Success<ResultType> — carries a value of type
ResultType - Failure — carries an
AppErrordescribing what went wrong
Using Result instead of throwing or nullable return values makes both outcomes explicit and encourages handling them via handle, pattern matching, or the mapSuccess / mapFailure methods.
Handle both outcomes:
Result<String> result = await fetchUserName();
final message = result.handle(
onSuccess: (name) => 'Hello, $name',
onFailure: (error) => 'Error: ${error.message}',
);
Pattern matching:
switch (result) {
Success(:final data) => print(data),
Failure(:final error) => showError(error),
}
Chaining: mapSuccess and mapAsyncSuccess transform the value when Success and pass through Failure. mapFailure and mapAsyncFailure transform the error when Failure; when Success, they return this Result unchanged.
Conversion: The maybeData getter converts to Maybe: Success → Just(data), Failure → Nothing.
Note: asSuccess and asFailure are casting helpers; they throw if the variant is wrong. Prefer handle or pattern matching instead.
Maybe #
A type-safe, declarative way to model optional values. Maybe<T> is a union type with two variants:
- Nothing — no value (type-safe alternative to null)
- Just<T> — a value of type
T
Using Maybe instead of nullable types (T?) makes the presence or absence of a value explicit and encourages handling both cases via pattern matching or methods like mapJust, mapNothing, and getOrElse.
Pattern matching:
Maybe<String> name = Just("test");
final display = switch (name) {
Nothing() => "",
Just(:final value) => value,
};
From nullable input:
Maybe.from(null); // Nothing()
Maybe.from("hi"); // Just("hi")
Chaining: mapJust / mapAsyncJust transform the value when Just and preserve Nothing. mapNothing / mapAsyncNothing run a callback when Nothing and return this Maybe unchanged when Just. Use getOrElse(fallback) to get the value or a fallback when Nothing (or when the inner value is null).
Combining two Maybes: On a record (Maybe<K>, Maybe<J>), call maybeCombine (or maybeAsyncCombine) with optional callbacks for firstJust, secondJust, bothJust, and bothNothing; omitted callbacks yield Nothing.
final combined = (maybeName, maybeCount).maybeCombine<String>(
bothJust: (name, count) => Just('$name: $count'),
firstJust: (name) => Just(name),
secondJust: (count) => Just(count.toString()),
bothNothing: () => Just('unknown'),
);
Unit #
A type that represents the absence of a meaningful value. Unit has exactly one value, Unit(), and carries no data. Use it when you need a type-safe way to express "no value" or "success with nothing to return", for example:
- Result<Unit> for operations that succeed but return nothing (e.g. delete, logout)
- Callbacks or generic code that require a concrete type instead of
void
Future<Result<Unit>> deleteItem(String id) async {
await api.delete(id);
return Result.success(Unit());
}
The package also provides an identity function identity<T>(T value) => value for use in generic code or as a no-op transformation (e.g. list.map(identity)).
RequestStatus #
When one is dealing with ui responses to different request states, in the course of it,
usually there are four states of interest: Idle, Loading, Succeeded or Failed.
So the convenience generic union type
RequestStatus<ResultType>
serves the purpose of modeling those states. Idle and Loading, carry no inner state, but
Succeeded<ResultType>().data = ResultType data;
contains a field data of type ResultType. And the
Failed().error = AppError error;
contains a field error of type AppError. Where AppError is the convenience type
that models errors in the app.
To deal with the request states one should use one of the unions methods.
The .map forces you to deal with all the four states explicitly, passing callbacks for
each state with non-destructured states.
Example:
Widget build(context) {
final someRequestStatus = someStateManagement.desiredRequestStatus;
return someRequestStatus.map(
idle: (idle) => "widget for idle state",
loading: (loading) => "widget for loading state",
succeeded: (succeeded) => "widget for succeeded state using possibly data within succeeded.data",
failed: (failed) => "widget for failed state using possibly AppError within failed.error",
);
}
The .when forces you to deal with all the four states explicitly, passing callbacks for
each state with destructured states.
Example:
Widget build(context) {
final someRequestStatus = someStateManagement.desiredRequestStatus;
return someRequestStatus.when(
idle: () => "widget for idle state",
loading: () => "widget for loading state",
succeeded: (data) => "widget for succeeded state using possibly data within data",
failed: (error) => "widget for failed state using possibly AppError within error",
);
}
You can also use .maybeMap and .maybeWhen, passing only the cases you care about and an orElse callback for the rest.
Example:
Widget build(context) {
final someRequestStatus = someStateManagement.desiredRequestStatus;
return someRequestStatus.maybeWhen(
orElse: () => "default widget to be displayed when the current state is not specified in other callbacks",
loading: () => "widget for loading state",
succeeded: (data) => "widget for succeeded state using possibly data within succeeded.data",
);
}
So, RequestStatus provides a safe and declarative way to always deal with all possible or desired states of a request.
Maybe<ResultType> get maybeData;
Getter that results in a [Maybe] that is [Just] if the [RequestStatus] is [Succeeded] and [Nothing] otherwise.
FormField #
When providing data to a form and then passing it forward, for instance,
in a request body, one problem that is common here is the need of dealing
with the cases where the field is not filled, and then one might need to
treat every possible resulting Map (json) separately, either passing the not
filled field with no value or not passing it at all.
The generic sealed data class
FormField<Type>
is a convenience type that models, as the name already points,
a field in a Form, and uses the convention of not passing not filled fields to the resulting Map.
Here we are already passing the [name] of the field in its possible Map
(json) position, and the actual [field] data is a Maybe<Type>.
FormFields are usually used in a Form defined class, and with the usage of
our convenience mixin FormUtils, one should have everything it needs to have
form validation, and toJson method. It might introduce some verbose api, to
deal with, but the convenience of dealing with the most critical parts, like
validating and passing the Form information through, makes the usage of our
FormFields worthwhile.
Example (using freezed to create the Form class):
@freezed
class FormExampleWithFreezed with _$FormExampleWithFreezed, FormUtils {
const FormExampleWithFreezed._();
const factory FormExampleWithFreezed({
@Default(FormField(name: 'firstFieldJsonName'))
FormField<String> firstField,
@Default(FormField(name: 'secondFieldJsonName'))
FormField<String> secondField,
}) = _FormExampleWithFreezed;
Result<String> get firstFieldValidation => validateField(
field: firstField.field,
validators: <String? Function(String)>[
// list of validators to first field
],
);
Result<String> get secondFieldValidation => validateField(
field: secondField.field,
validators: <String? Function(String)>[
// list of validators to second field
],
);
Map<String, dynamic> toJson() => fieldsToJson([
firstField,
secondField,
]);
}
Just to point out that the usage of a freezed class is not required to enjoy the advantages
of the FormField type, we present another example(not using
freezed):
class FormExample with FormUtils {
final FormField<String> firstField;
final FormField<String> secondField;
const FormExample({
required this.firstField,
required this.secondField,
});
Result<String> get firstFieldValidation => validateField(
field: firstField.field,
validators: <String? Function(String)>[
// list of validators to first field
],
);
Result<String> get secondFieldValidation => validateField(
field: secondField.field,
validators: <String? Function(String)>[
// list of validators to second field
],
);
Map<String, dynamic> toJson() => fieldsToJson([
firstField,
secondField,
]);
}
Using a Form class as presented, one has a safe way to pass the values of
the field to a request body with ease.
Example:
request.body = formExampleInstance.toJson(),
AppError #
Abstract class to model errors in the application. As a preset of foreseen specific errors there are several implementations of this type. Namely: [HttpError] models errors related to http requests [CacheError] models cache errors [DeviceInfoError] models device's information gathering related errors [FormError] models form related errors [StorageError] models storage operations related errors
In addition to the [AppError], there are a preset of foreseen [Exceptions].
Util #
FormUtils #
Class used as a Dart Mixin on a Form class, providing methods to conveniently
deal with validation and serialization of fields.
Result<String> validateField<Type>
Method to help validate a [FormField
Map<String, dynamic> fieldsToJson(List<FormField> fields)
Method to help in the task of passing the provided List<FormField> to its Map<String, dynamic> representation,
that is useful when it comes to pass the Form data through, for instance, a request body
SeedTestStateMixin #
Mixin to StateNotifier to help seeding test states.
Example:
class MyStateNotifier extends StateNotifier<MyState> with SeedTestStateMixin<MyState> {}
and in a test:
test(
'Test description',
() {
myStateNotifier.setSeedState(
mySeedState
);
/// test body
},
);