In this series we are going to learn how to convert the Open Meteo API example (from the bloc package) from imperative code to functional programming using fpdart
.
We will see step-by-step how to refactor the code to functional programming, as well as the benefits that this brings.
Each post of the series will cover in details one specific aspect of the refactoring.
In this first post:
- Introduction to the Open Meteo API code example
- Refactoring of the return type of the function from
Future
toTaskEither
- What is the difference between
Future
andTask
- How
TaskEither
works (Task
async +Either
error handling) - How to encode errors using
Either
andabstract class
- What is the difference between
Open Meteo API example
We are going to refactor the Open Meteo API client using fpdart
and functional programming.
This example is part of the flutter_weather app example in the bloc package.
The implementation is relatively simple. Nonetheless, it contains some of the most usual usecases of a real production app, like:
- Making an http request (using the
http
package) - Checking for a valid response
- Decoding the response from JSON
- Validating the response format
We are going to focus specifically on the locationSearch
request, that you can see reported below π:
/// Finds a [Location] `/v1/search/?name=(query)`.
Future<Location> locationSearch(String query) async {
final locationRequest = Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
);
final locationResponse = await _httpClient.get(locationRequest);
if (locationResponse.statusCode != 200) {
throw LocationRequestFailure();
}
final locationJson = jsonDecode(locationResponse.body) as Map;
if (!locationJson.containsKey('results')) throw LocationNotFoundFailure();
final results = locationJson['results'] as List;
if (results.isEmpty) throw LocationNotFoundFailure();
return Location.fromJson(results.first as Map<String, dynamic>);
}
The goal is to see step by step what you must consider to convert this implementation to functional programming using fpdart
.
Furthermore, we are going to highlight the reason behind each refactoring, showing the potential benefits along the way.
Future
is not your friend
Let's start from the very top: the return type, Future<Location>
. Here is a general rule that you must follow when working with fpdart
and functional programming:
Do not return
Future
from a function, useTask
,TaskOption
, orTaskEither
instead
You can read my previous post about sync and async functions, and why Future
is not functional-friendly.
The main reason is that Future
makes the function impure: the result of calling the function will change at every request. Furthermore, Future
is not composable and it makes error handling complex and verbose.
I strongly suggest you to read this post about Future & Task: asynchronous Functional Programming, which goes into more details on the difference between
Future
andTask
fpdart
has 3 alternatives, each suited for different situations based on the requirements:
Task
: Used to perform async requests that you are 100% sure that will never fail (100% βοΈ)TaskOption
: Used to perform async requests that may fail and/or return a missing valueTaskEither
: Used to perform async requests that may fail for multiple reasons that we want to encode in the return type
What makes Task
functional programming friendly?
You may be thinking: "Wait, but making an http request will always return a different result on every call of the function, how can Task
make the function pure?".
Simple: Task
doesn't make the request, yet. In fact, Task
is implemented as follows π:
class Task<A> {
final Future<A> Function() _run;
/// Build a [Task] from a function returning a [Future].
const Task(this._run);
/// Run the task and return a [Future].
Future<A> run() => _run();
Task
is a thunk: a function that, when called, will return a Future
.
Until you call the run()
method, not request is executed.
Task
is simply a description of your request, it doesn't do anything untilrun()
is called ππΌββοΈ
That's what makes the function pure and composable: you can now chain methods to extract and validate the response, without actually making any request πͺ.
Again, you can learn more about
Task
in Future & Task: asynchronous Functional Programming
Which Task
should I choose?
Task
, TaskOption
, or TaskEither
?
As a rule of thumb, TaskEither
is generally what you are looking for.
TaskEither
allows to encode the error directly in the response type. It takes 2 generic parameters (TaskEither<L, R>
):
L
(Left): represents the error type in case the request failsR
(Right): represent the response type in case of a successful request
We are going to use TaskEither
in this example. What types should we give it? π€
Error handling with the Either
type
In functional programming, the response type of the function should give us all the information needed to handle all possible events inside the app.
TaskEither
(which is a fusion of Task
+ Either
) allows to do just that:
- Since it's a
Task
, we know we are making an async request - Since it's an
Either
, we know the request can fail
Furthermore, Either<L, R>
will give us even more information:
- If the function fails, all the possible failures are described by the
L
(Left) type - If the function succeeds, the response type is
R
(Right)
Just by reading the return type, we know everything that we need to handle any possible response in the app (regardless of how the function is implemented internally)
How to encode any possible error using Either
The function can fail for multiple reasons, but we have only 1 error type allowed.
That's the perfect usecase for an abstract class
.
We define an abstract class
which represents our error:
/// Abstract class which represents a failure in the `locationSearch` request.
abstract class OpenMeteoApiFpdartLocationFailure {}
Now all the possible errors in the response will implement OpenMeteoApiFpdartLocationFailure
, for example:
/// [OpenMeteoApiFpdartLocationFailure] when location response
/// cannot be decoded from json.
class LocationInvalidJsonDecodeFpdartFailure
implements OpenMeteoApiFpdartLocationFailure {
const LocationInvalidJsonDecodeFpdartFailure(this.body);
final String body;
}
Finally, we can define the response type of the locationSearch
function:
/// Finds a [Location] `/v1/search/?name=(query)`.
TaskEither<OpenMeteoApiFpdartLocationFailure, Location> locationSearch(String query)
Notice how the function is no more
async
, since it does not return aFuture
To recap:
TaskEither
: async request that can failOpenMeteoApiFpdartLocationFailure
: class which encodes all the possible errorsLocation
: result of a successful response
That is all for this first part.
In the second part of this series we are going to learn how to make http requests in functional programming, and how to validate the response.
You can subscribe to the newsletter here below to stay up to date with each new post π