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

  1. Getting Started
  2. Types
    1. Result
    2. Maybe
    3. RequestStatus
    4. FormField
  3. AppError
  4. Util
    1. FormUtils
    2. 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>

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)

and an onFailure callback

Type onFailure(AppError data)

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"
  }
);

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,
  );

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));
FutureOr<Result<K>> mapAsyncSuccess<K>(
    FutureOr<Result<K>> Function(ResultType) combiner,
);

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));
Maybe<ResultType> get maybeData;

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>

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

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

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

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);

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);

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);

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);

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,
  })

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>

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 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",
          );
  }

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",
          );
  }

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",
          );
  }

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 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>

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,
       ]);
 }

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 easy.
Example:

  request.body = formExampleInstance.toJson(),

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>

Method to help validate a FormField<Type> providing its value represented by its Maybe<Type>, and a List<Validator<Type>>, returning a Result<String> with possible error message.

  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
        },
      );