This is the third 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 start working with fpdart
to implement the requests to read and write in storage:
- How to handle dependencies in functional programming (dependency injection)
- How to handle errors in functional programming (
Either
) - How to use the
ReaderTaskEither
type infpdart
There is more ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Recap: EventEntity
and StorageService
In the previous article we defined the requirements for our application as follows:
The app should allow to store events (with
title
anddate
). It then should allow to visualize all the created events.
We then implemented two important building blocks for the app: EventEntity
and StorageService
.
EventEntity
is an immutable class that stores an event
:
import 'package:flutter/material.dart';
@immutable
final class EventEntity {
final int id;
final String title;
final DateTime createdAt;
const EventEntity(this.id, this.title, this.createdAt);
}
StorageService
is an abstract class that defines the API methods to read and write data in storage:
abstract class StorageService {
Future<List<EventEntity>> get getAll;
Future<EventEntity> put(String title);
}
We are now going to use both EventEntity
and StorageService
to define the actual storage implementation using fpdart
.
Functional programming function definition: fpdart
overview
Before working on the actual code using fpdart
it's important to clearly define the requirements.
Specifically, we want to explicitly define 3 types:
- Dependencies
- Errors
- Success
Dependencies
A dependency is any extra service or context required to perform the request.
In object oriented programming, a dependency is usually defined as a parameter in a class, which is provided when initializing a concrete instance (dependency injection):
class GetAllRequest {
final Dependency1 dep1;
final Dependency2 dep1;
const GetAllRequest(this.dep1, this.dep2);
int someMethod() { ... }
}
Dependency Injection will make your app better Easier testing ๐งช Explicit dependencies ๐งฑ Better abstractions ๐ช Do you know how Dependency Injection works in Flutter? Let's learn ๐๐งต
Using this class-based approach, you are required to provide each dependency before using
GetAllRequest
/// Create and provide `dep1` and `dep2` before defining `getAllRequest`
final getAllRequest = GetAllRequest(dep1, dep2);
getAllRequest.someMethod();
In fpdart
(and functional programming in general) it works the opposite way: first define the function, and then provide the dependencies.
Note ๐ก: This is the difference between working with functions instead of classes, you first define the function and then (only at the very end โ๏ธ) provide the parameters
/// This function has [Dependency1] and [Dependency2] as "dependencies"
int someMethod(Dependency1 dep1, Dependency2 dep2) { ... }
someMethod(dep1, dep2);
Dependency injection for testing โ It works, but it's not nice to define, use, and compose ๐ There is a solution, it comes from `fpdart`, and it's called `Reader` ๐๐งต
How to solve this? Make it explicit โ๏ธ Provide "print" as a dependency (parameter) to the function
Important โ๏ธ: All dependencies must be explicit when working with functional programming and
fpdart
. Making dependencies explicit makes the code easier to test and read.This means that ideally you are not allowed to access global functions ๐
Testing is painful ๐ฎโ๐จ Do you know why? Implicit dependencies ๐ For example, how do you test the code below ๐๐งต (TLDR: You can't ๐ซ)
In our case we want to use StorageService
to make a getAll
request. Therefore, StorageService
is a dependency, required to perform the request.
Note ๐ก: A dependency in
fpdart
is always defined as anabstract
class.In our example,
fpdart
only knows about thegetAll
method, without any specific implementation details.
Errors
In functional programming we prefer to avoid throwing Exception
s and using try
/catch
. Instead, each error is explicitly defined in the return type of a function.
In fpdart
we use the Either
type to encode both error and success values when defining the return type of every function:
Either<SomeError, SuccessValue> someMethod(Dependency1 dep1, Dependency2 dep2) {}
There is another way to handle errors ๐ฅ Which doesn't use try/catch/throw at all ๐ โโ๏ธ It's called *Either* This is a big deal, let's learn how this works ๐๐งต x.com/SandroMaglioneโฆ
Error handling is crucial for any app โ It can make or break your app (literally ๐๐ผโโ๏ธ) #dart offers many tools to save you from troubles ๐ฏ Here they are ๐๐งต
In this example, SomeError
encodes every possible error that may occur when calling the function (recoverable errors ๐).
Recoverable VS Unrecoverable errors ๐ค Here is a good analogy ๐๐ "Nurse, if there's a patient in room 5, can you ask him to wait?" โ๏ธ Recoverable error: "Doctor, there is no patient in room 5." โ๏ธUnrecoverable error: "Doctor, there is no room 5!"
What if your #flutter app fails? Should you throw? Error? Exception? #dart gives you both Error and Exception ๐ฏ But how do they work? Which one should you choose? ๐ค Here is the answer ๐๐งต
Since Dart 3 defining type-safe errors has become much easier using sealed
classes:
/// `RequestError` is the type using in [Either].
///
/// We then use pattern matching to handle all possible errors (`sealed` class ๐ค)
sealed class RequestError {
const RequestError();
}
class RequestError1 extends RequestError {
const RequestError1();
}
class RequestError2 extends RequestError {
const RequestError2();
}
Success value
The last type to define is the success value. This value is returned when the request is successful.
The success value is the same type that you use as return type in normal dart code (without
Either
).
The success type is defined in the "right" side of the Either
type:
Either<SomeError, SuccessValue> someMethod(Dependency1 dep1, Dependency2 dep2) {}
Either can *only* be Right or Left ๐๐ ๐ Right: It contains a successful response type ๐ Left: It contains the error if the function fails
There is more ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
ReaderTaskEither
: Dependencies + Errors + Success
We are now going to put all these types together using fpdart
๐ฅ
Since fpdart
v1.0 you can use the full power of the ReaderTaskEither
type.
ReaderTaskEither
encodes a function that:
- Requires some dependencies (
Reader
) - May fail with some error (
Either
) - Returns an
async
success value (Task
)
All these parameters are explicitly defined in the return type:
ReaderTaskEither<Dependency, Error, Success> someMethod = ...
Errors? Async? `fpdart` also provides `ReaderTask` (async) and `ReaderTaskEither` (errors) ๐ฏ Functional programming to the max ๐
Multiple dependencies using Records
Since Dart 3 we can use Records to organize multiple dependencies in one type:
typedef Dependencies = ({ Dependency1 dep1, Dependency2 dep2 });
ReaderTaskEither<Dependencies, Error, Success> someMethod = ...
Dart 3 is nearly here, brining Patterns and Records with it ๐ฏ This will radically change how you write @dart_lang (and @FlutterDev) apps Learn all about them right now ๐งต๐
Multiple errors using sealed
classes
As we saw in the previous section, we can use sealed
and pattern matching to define every possible error:
/// Use pattern matching to handle all possible errors (`sealed` class ๐ค)
sealed class RequestError {
const RequestError();
}
class RequestError1 extends RequestError {
const RequestError1();
}
class RequestError2 extends RequestError {
const RequestError2();
}
Putting all together using ReaderTaskEither
Defining dependencies, errors, and success value is a pre-requisite to implement a type-safe method using fpdart
:
ReaderTaskEither<Dependencies, RequestError, Success> getAllEvents = ReaderTaskEither(/* TODO */);
The next step is the actual implementation inside ReaderTaskEither
๐ค
This is it for part 3!
As you saw we did not yet write any actual code ๐๐ผโโ๏ธ. Instead, we spent more time making every aspect of the app explicit (dependencies, errors, and return value).
This setup will make the actual implementation easier and safer: we know exactly all the methods at our disposal, and we know exactly all the possible errors. fpdart
will then guide using the type-system to avoid making implementation errors (using ReaderTaskEither
) ๐ช
If you want to stay up to date with the latest releases, you can subscribe to my newsletter here below ๐
Thanks for reading.