0 purchases
convenience types
Convenience Types #
A package to ensamble convenience types commonly used through flutter projects developed by Capyba.
Motivation #
Along our development cycle of numerous projects we have adopted the usage of some types that helped us to keep things safer, more 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
Types
Result
Maybe
RequestStatus
FormField
AppError
Util
FormUtils
SeedTestStateMixin
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 #
Every asynchronus task can have two possible outcomes as a Result.
It is either a Success or a Failure.
So the
Result<ResultType>
copied to clipboard
generic union type is a convenience type to model
and help safelly deal with any asynchronus task outcomes.
The approach is declarative, so in order to deal with the result, one
should call the handle method which has two required parameters
an onSuccess callback
Type onSuccess(Type data)
copied to clipboard
and an onFailure callback
Type onFailure(AppError data)
copied to clipboard
Where AppError is a convenience type to model errors in the application
Example:
Result<String> asyncTaskResturningStringResult = await someFutureOfResultString();
asyncTaskResturningStringResult.handle(
onSuccess: (String data) {
"here one have access to the succesful value of the async task and might use it as desired"
},
onFailure: (AppError error) {
"here one have access to the failure modeled as AppError representing this async task"
}
);
copied to clipboard
In this way one always needs to deal in a declarative way with both the
success and failure possible outcomes as unfortunatelly any asynchronus
task needs.
anti-patern alert!: The Result generic Union type comes with casts convenience methods asSuccess, asFailure, but although it might be temping to just cast the result into the desired type, it is strongly advised you not to do it, once if you try to cast diferent types (Success as Failure or the other way around) it would throw an exception.
Result<K> mapSuccess<K>(
Result<K> Function(ResultType) combiner,
);
copied to clipboard
A method used to chain access to data held by the [Result]. If this is [Failure] returns [Failure], if this is [Success], returns the result of the combiner method over the data inside [Success]
Example:
Result<String> asyncTaskResturningStringResult = await someFutureOfResultString();
Result<double> parseResult = asyncTaskResturningStringResult.mapSuccess((String data) => methodThatTakesStringDataAndTriesToParseDouble(data));
copied to clipboard
FutureOr<Result<K>> mapAsyncSuccess<K>(
FutureOr<Result<K>> Function(ResultType) combiner,
);
copied to clipboard
A method to chain asynchronous access to data held by the [Result]. If this is [Failure] returns [FutureOr<Failure>], if this is [Success], returns the result of the combiner method over the data inside [Success]
Example:
Result<String> asyncTaskResturningStringResult = await someFutureOfResultString();
Result<double> parseResult = await asyncTaskResturningStringResult.mapAsyncSuccess((String data) => methodThatTakesStringDataAndAsynchronouslyTriesToParseDouble(data));
copied to clipboard
Maybe<ResultType> get maybeData;
copied to clipboard
Getter that results in a [Just] if the [Result] is [Success] and [Nothing] othterwise
Maybe #
Dealing with optional values in ui has always been verbose and unsafe.
So the
Maybe<T>
copied to clipboard
generic union type is a convenience type to model
and help safelly deal with any optional value outcomes.
Where we can have two types that will represent the state of a value that can be null. The [Nothing], representing when it has no value, and the [Just], when it has a value.
The approach is declarative, so in order to deal with the states of [Maybe], one
should use one of the unions methods.
The .map forces you to deal with all the two states explicitly, passing callbacks for
each state with undestructured states.
Example:
Maybe<String> someMaybeValue = Just("test");
final debugValue = someMaybeValue.map(
nothing: (_) => "",
just: (data) => data.value,
);
print(debugValue); // test
copied to clipboard
The .when forces you to deal with all the two states explicitly, passing callbacks for
each state with destructured states
Example:
Maybe<String> someMaybeValue = Nothing();
final debugValue = someMaybeValue.map(
nothing: () => "test",
just: (data) => data,
);
print(debugValue); // test
copied to clipboard
and one also might want to not deal explicitly with all states diferently, so
there are the .maybeMap, and .maybeWhen methods where you need only expclitly to pass a
orElse callback. But I would say that it is not so useful in this case since we only have two states to be treated.
Example:
Maybe<String> someMaybeValue = Just("test");
final debugValue = someMaybeValue.maybeWhen(
just: (data) => data,
orElse() => "",
);
print(debugValue); // test
copied to clipboard
So, Maybe provides a safe and declarative way to always deal with the two possible states of a optional value.
factory Maybe.from(T? input);
copied to clipboard
Factory for helping building a [Maybe] from a nullable input. It produces a [Nothing] if the input is null, and a [Just] otherwise
Type getOrElse<Type>(Type fallback);
copied to clipboard
The [getOrElse] method which receives a parameter to return as a
fallback value, when the value is a [Nothing], or there is no value in the [Just]
Maybe<K> mapJust<K>(Maybe<K> Function(T) combiner);
copied to clipboard
A method to chain access to data held by the [Maybe]. If this is [Nothing] returns [Nothing], if this is [Just], returns the result of the combiner method over the value inside [Just]
FutureOr<Maybe<K>> mapAsyncJust<K>(FutureOr<Maybe<K>> Function(T) combiner);
copied to clipboard
A Method to chain async access to data held by the [Maybe]. If this is [Nothing] returns [Nothing], if this is [Just], returns the result of the combiner method over the value inside [Just]
Maybe<T> maybeCombine<T>({
/// Used to map case where only the first value is [Just]
Maybe<T> Function(K)? firstJust,
/// Used to map case where only the second value is [Just]
Maybe<T> Function(J)? secondJust,
/// Used to map case where both values are [Just]
Maybe<T> Function(K, J)? bothJust,
/// Used to map case where both values are [Nothing]
Maybe<T> Function()? bothNothing,
})
copied to clipboard
Use it to combine two different Maybe's into a new one. Input firstJust to map case where only the first value is [Just], secondJust to map case where only the second value is [Just], bothJust to map case where both first and second value are [Just] and bothNothing to map case where both are [Nothing]
Example:
Maybe<Number> combined = (testString, testInt).maybeCombine<Number>(
bothJust: (val, number) => Just(Number(val, number, '$number$val',)),
firstJust: (val) => Just(Number(val, -1, '-1$val',)),
secondJust: (number) => Just(Number('not a trivia', number, 'NonTrivia',)),
bothNothing: () => Just(Number('not a trivia', -1, 'NonTrivia',)),
);
```
### 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`, `Succeded` or `Failed`.<br>
So the convenience generic union type
```dart
RequestStatus<ResultType>
copied to clipboard
serves the purpose of modeling those states. Idle and Loading, carry no inner state, but
Succeeded<ResultType>().data = ResultType data;
copied to clipboard
contains a field data of type ResultType. And the
Failed().error = AppError error;
copied to clipboard
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 undestructured 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",
);
}
copied to clipboard
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",
);
}
copied to clipboard
and one also might want to not deal explicitly with all states diferently, so
there are the .maybeMap, and .maybeWhen methods where you need only expclitly to pass a
orElse callback.
Example:
Widget build(context) {
final someRequestStatus = someStateManagement.desiredRequestStatus;
return someRequestStatus.maybeWhen(
orElse: () => "default widget to be displayed other wise 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",
);
}
copied to clipboard
So, RequestStatus provides a safe and declarative way to always deal with all possible or desired states of a request.
Maybe<ResultType> get maybeData;
copied to clipboard
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 than one might need to
treat every possible resulting Map (json) separetily, either passing the not
filled field with no value or not passing it at all.
The generic sealed data class
FormField<Type>
copied to clipboard
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 convinice 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,
]);
}
copied to clipboard
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,
]);
}
copied to clipboard
Using a Form class as presented, one has a safe way to pass the values of
the field to a request body with easy.
Example:
request.body = formExampleInstance.toJson(),
copied to clipboard
AppError #
Abstract class to model errors on the application. As a presset of foreseen
specific errors there are some different 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 presset of foreseen [Exceptions]
Util #
FormUtils #
Class used as a dart Mixin to a Form class, providing methods to conviniently
deal with validation and serialization of fields.
Result<String> validateField<Type>
copied to clipboard
Method to help validate a [FormField
Map<String, dynamic> fieldsToJson(List<FormField> fields)
copied to clipboard
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> {}
copied to clipboard
and in a test:
test(
'Test description',
() {
myStateNotifier.setSeedState(
mySeedState
);
/// test body
},
);
copied to clipboard
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.