Business logic with fpdart and the Do notation | Fpdart and Riverpod Functional Programming in Flutter

Sandro Maglione

Sandro Maglione

Mobile development

This is the fifth part of a new series in which we are going to learn how to build a safe, maintainable, and testable app in Flutter using fpdart and riverpod.

We will focus less on the implementation details, and more on good practices and abstractions that will helps us to build a flexible yet resilient app in Flutter.

As always, you can find the final Open Source project on Github:

In this article we are going to:

  • Use ReaderTask to implement the getAllEvent function
  • Learn how to use the Do notation with the .Do constructor in fpdart
  • Use TaskEither and .tryCatch to execute a Future and catch errors
  • Match the result value to a valid instance of GetAllEventState

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.

Recap: riverpod providers and fpdart

In the previous article we created all the necessary providers using riverpod.

We added a storageServiceProvider used to provide a concrete instance of StorageService, which is a required dependency to read and write in storage:

@riverpod
StorageService storageService(StorageServiceRef ref) {
  /// Return concrete instance of [StorageService]
  throw UnimplementedError();
}

We then discussed how to handle errors using riverpod and fpdart together:

  • We use riverpod's AsyncValue to handle unexpected errors
  • We use ReaderTask from fpdart and pattern matching to match on success values and expected errors

Finally, we implemented the eventListProvider to connect riverpod and fpdart:

@riverpod
Future<GetAllEventState> eventList(EventListRef ref) async {
  final service = ref.watch(storageServiceProvider);
  return getAllEvent.run(service);
}

eventListProvider is then used in the UI to watch for changes and pattern match on the current state:

class HomePage extends HookConsumerWidget {
  const HomePage({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final eventList = ref.watch(eventListProvider);
    return ...;
  }
}

The last step is implementing the logic to read from storage with fpdart in the getAllEvent function, which is exactly what we will do today 👇


How to implement getAllEvent using StorageService

As we have seen above, inside the eventListProvider we call the getAllEvent function:

@riverpod
Future<GetAllEventState> eventList(EventListRef ref) async {
  final service = ref.watch(storageServiceProvider);
  return getAllEvent.run(service);
}

As we discussed in the previous article, getAllEvent returns a ReaderTask that requires StorageService as dependency, and returns GetAllEventState:

ReaderTask<StorageService, GetAllEventState> getAllEvent = ReaderTask(/* TODO */);

ReaderTask gives us access to an instance of StorageService. We will use this to call the getAll function and get a List<EventEntity>:

storage_service.dart
abstract class StorageService {
  Future<List<EventEntity>> get getAll;
  Future<EventEntity> put(String title);
}

We will then need to map the data to the correct GetAllEventState:

  • SuccessGetAllEventState when the request is successful
  • GetAllEventError when the request fails

fpdart's Do notation

We use the Do notation to implement the logic inside getAllEvent.

The Do notation allows to write functional code that looks like normal imperative dart code.

Instead of chaining methods calls, we can write linear step-by-step code.

Every type inside fpdart has a Do constructor that allows to initialize a Do notation function. We use this to implement getAllEvent:

get_all_event.dart
final getAllEvent = ReaderTask<StorageService, GetAllEventState>.Do(
  (_) async {
    /// ...
  },
);

The Do constructor gives us access to a function (called _ by convention).

The _ function allows to extract and use the result value from any fpdart's type while still handling errors in a functional style

We are going to use _ to extract the data from storage and map it to GetAllEventState 👇

For ReaderTask the _ function has the following type signature:

/// Given a [ReaderTask] with [StorageService], return a [Future] containing type [A]
Future<A> Function<A>(ReaderTask<StorageService, A>) _

Get the events from StorageService

When working with fpdart and functional programming it is helpful to define the program as a series of steps before starting the implementation.

In our case the steps are the following:

  1. Call getAll from StorageService to get List<EventEntity> while handling possible errors in the request
  2. Mapping List<EventEntity> to a valid instance of GetAllEventState (success value or error)

Each of these steps is reflected in the actual implementation.

Call getAll from StorageService

The ReaderTask constructor requires a function that gives us access to an instance of StorageService:

ReaderTask(
  (storageService) async => // ...
);

The value that we return from this function represents the second generic parameter from ReaderTask:

ReaderTask<StorageService, int>(
  ///       👆👇            👆👇
  (storageService) async => 10
);

We want to call getAll from StorageService. getAll returns a Future that may fail (error when loading from storage).

In fpdart when we deal with Future (async) and errors we use the TaskEither type.

You can read the article How to use TaskEither in fpdart for a detailed overview of TaskEither

Specifically we use the tryCatch constructor to catch and handle any possible error thrown by getAll:

ReaderTask(
  (storageService) async => TaskEither.tryCatch(
    () => storageService.getAll,
    QueryGetAllEventError.new,
  ),
);

QueryGetAllEventError.new is a Constructor tear-off, introduced in dart 2.15, which allows to call a constructor as if it was a normal function.

The code is equivalent to the following:

ReaderTask(
  (storageService) async => TaskEither.tryCatch(
    () => storageService.getAll,
    (object, stackTrace) => QueryGetAllEventError(object, stackTrace),
  ),
);

In case of errors we return QueryGetAllEventError, which extends GetAllEventError:

sealed class GetAllEventError extends GetAllEventState {
  const GetAllEventError();
}
 
class QueryGetAllEventError extends GetAllEventError {
  final Object object;
  final StackTrace stackTrace;
  const QueryGetAllEventError(this.object, this.stackTrace);
}

This code will return a ReaderTask with the following type parameters:

///                                   👇 Error               👇 Success
ReaderTask<StorageService, TaskEither<QueryGetAllEventError, List<EventEntity>>>

Use the Do notation to extract the success value

We want to access the TaskEither from ReaderTask inside the Do notation without calling run and passing an instance of StorageService.

This is what the _ function allows us to do:

final getAllEvent = ReaderTask<StorageService, GetAllEventState>.Do(
  (_) async {
    TaskEither<QueryGetAllEventError, List<EventEntity>> executeQuery = await _(
      ReaderTask(
        (storageService) async => TaskEither.tryCatch(
          () => storageService.getAll,
          QueryGetAllEventError.new,
        ),
      ),
    );
 
    /// ...
  },
);

Using _ inside the Do notation allows to extract return values without calling run.

run should only be called at the very end! ☝️

In our case, we call it inside riverpod's provider, which is the very last step before using the result value:

@riverpod
Future<GetAllEventState> eventList(EventListRef ref) async {
  final service = ref.watch(storageServiceProvider);
  return getAllEvent.run(service);
}

There are some exceptions, one of which we are going to see below 👇

Mapping List<EventEntity> to a valid instance of GetAllEventState

The second and last step is extracting the List<EventEntity> value and mapping it to a GetAllEventState.

The _ function inside the Do notation requires a ReaderTask. Therefore, we are going to create a ReaderTask without using the storageService parameter:

return _(
  ReaderTask(
    (_) => /// ...
  ),
);

Inside the ReaderTask we need to extract the success value (List<EventEntity>) from executeQuery (TaskEither) and convert it to GetAllEventState.

We use match from TaskEither to convert both the error and success values to GetAllEventState:

return _(
  ReaderTask(
    (_) => executeQuery
        .match(
          identity,
          SuccessGetAllEventState.new,
        )
        .run(),
  ),
);

identity is a function in fpdart that returns the given input value:

T identity<T>(T a) => a;

Calling match returns a Task<GetAllEventState>. We are then required to call run() to execute the Task and extract GetAllEventState.

That is because the ReaderTask constructor requires to return a Future, which we get by calling run() on Task.

If you are interested in learning more about Future and Task you can read Future & Task: asynchronous Functional Programming

Calling run() is not necessary when fpdart provides a built-in function to convert from one type to another.

In this case, a built-in function to convert from Task to ReaderTask is still missing. This is how it would look like:

return _(
  executeQuery
    .match(
      identity,
      SuccessGetAllEventState.new,
    )
    .toReaderTask<StorageService>(),
);

Or using a from* constructor:

return _(
  ReaderTask.fromTask(
    (_) => executeQuery
        .match(
          identity,
          SuccessGetAllEventState.new,
        ),
  ),
);

We now have the correct instance of GetAllEventState that we return from the Do notation (return _(...)).

Put everything together: getAllEvent

This is it!

This below is the final complete implementation of the getAllEvent method with fpdart and ReaderTask:

final getAllEvent = ReaderTask<StorageService, GetAllEventState>.Do(
  (_) async {
    final executeQuery = await _(
      ReaderTask(
        (storageService) async => TaskEither.tryCatch(
          () => storageService.getAll,
          QueryGetAllEventError.new,
        ),
      ),
    );
 
    return _(
      ReaderTask(
        (_) => executeQuery
            .match(
              identity,
              SuccessGetAllEventState.new,
            )
            .run(),
      ),
    );
  },
);
  • Use the .Do constructor to initialize a do notation function
  • Use TaskEither to execute getAll from the provided instance of StorageService
    • Handle possible errors using the .tryCatch constructor of TaskEither
  • Call match to map error and success values to GetAllEventState
  • Call run to execute the resulting Task and return a Future inside ReaderTask

For reference, below you can see the same code without using the Do notation:

/// Chain of method calls instead of a series of step ⛓️
final getAllEventChain = ReaderTask(
  (StorageService storageService) => TaskEither.tryCatch(
    () => storageService.getAll,
    QueryGetAllEventError.new,
  )
      .match(
        identity,
        SuccessGetAllEventState.new,
      )
      .run(),
);

This is it for part 5!

We learned how to implement a complete function using some advanced fpdart types like ReaderTask and TaskEither. We also learned how the Do notation works and how to use it effectively in your code.

We then put all the pieces together to complete the implementation of the getAllEvent function.

The last step required to run the app is providing a valid implementation for StorageService:

@riverpod
StorageService storageService(StorageServiceRef ref) {
  /// Return concrete instance of [StorageService]
  throw UnimplementedError();
}

This is not related to fpdart or riverpod. You are free to use any solution you want (examples are shared_preferences, isar, flutter_secure_storage)

What instead we will do in the next article is learning how to test the app and seeing how fpdart makes testing as easy as it gets

If you want to stay up to date with the latest releases, you can subscribe to my newsletter here below 👇

Thanks for reading.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.