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 the first part, Open Meteo API - Functional programming with fpdart, we define the return type using TaskEither
, and the possible errors using an abstract class
.
In this part 2 we are going to learn how to use fpdart
to perform the request:
- Use the
http
package to make an API request - Validate the request response
- Extract the data from the response
- Convert the validated data to a dart class and return it π
Open Meteo API example
This example is part of the flutter_weather app example in the bloc package.
I report here the locationSearch
request, which we are refactoring to functional programming in this tutorial:
/// 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>);
}
This function does the following:
- Make an http request (using the
http
package) - Check for a valid response
- Decode the response from JSON
- Validate the response format
We are now going to see how to achieve the same result using fpdart
.
Make an http request with fpdart
In part 1 we defined the return type as TaskEither
:
/// Code implemented and explained in part 1: return [TaskEither] π
TaskEither<OpenMeteoApiFpdartLocationFailure, Location> locationSearch(String query)
The first step is the actual http request to Open API. This is the code in the original implementation:
final locationRequest = Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
);
final locationResponse = await _httpClient.get(locationRequest);
- Define a
Uri
where to make the request to Open API - Send the
get
request usinghttp
When refactoring this to functional programming we must consider 3 aspects:
- Is it possible for this code to fail (
throw
)? - Does this code contain side effects?
- Is this operation synchronous or asynchronous?
Is it possible for this code to fail (throw
)?
The first question allows us to define the type used to handle the request:
- If the request never fails, then no specific type is needed
/// Type [int]: this never fails
int thisNeverFails(String str) => str.length;
- If the request may fail with a missing value, then we use the
Option
type
/// Type [Option]: element may be missing
Option<int> getFirst(List<int> list) => list.head;
- If the request may fail and we want to report an error, then we use the
Either
type
/// Type [Either]: report an error in case of failure
Either<String, int> divideOrError(int x, int y) {
if (y == 0) {
return left('Cannot divide by 0');
}
return right(x ~/ y);
}
Does this code contain side effects?
Any function that contains side effects requires special care.
In fact, in functional programming we want to handle side effects differently: any function that runs a side effect will be wrapped in a thunk (a function that returns another function).
/// Regular function, no side effect
int noSideEffect(String str) {
return str.length;
}
/// With side effect no special care ππΌββοΈ
int withSideEffectImperative(String str) {
print(str); // Side effect π
return str.length;
}
/// With side effect: Use a thunk β οΈ
int Function() withSideEffectFunctional(String str) => () {
print(str); // Side effect π
return str.length;
};
The difference between withSideEffectImperative
and withSideEffectFunctional
is the following:
/// This prints "abc" and gives us the result π€
int result1 = withSideEffectImperative("abc");
/// This does not do anything ππΌββοΈ
int Function() result2 = withSideEffectFunctional("abc");
int realResult2 = result2(); /// You need to explicitly execute it
This strategy allows us to have more control on the effect: no side effect will run until we explicitly execute it.
Using
fpdart
we do not need to define all these types manually:fpdart
already has the types we need to handle side effect, read below π
Is this operation synchronous or asynchronous?
The last key question is: synchronous or asynchronous?
This allows us to define the type used to manage any side effect:
- Synchronous: we use the
IO
type
/// With side effect: Use a thunk β οΈ
int Function() withSideEffectFunctional(String str) => () {
print(str); // Side effect (sync) π
return str.length;
};
/// With side effect + `fpdart`: [IO] type
IO<int> withSideEffectIO(String str) => IO(() {
print(str);
return str.length;
});
- Asynchronous: we use the
Task
type (as you can see from the example, the main difference is that the function returns aFuture
)
/// With side effect: Use a thunk β οΈ
Future<int> Function() withSideEffectAsync() => () async {
final value = await stdin.length;
return value;
};
/// With side effect + `fpdart`: [Task] type
Task<int> withSideEffectTask() => Task(() async {
final value = await stdin.length; // Side effect (async) π
return value;
});
In our example we have the following:
- The code may fail for multiple reasons, so we want to use
Either
to encode the error type - The code makes an http request, which is a side effect
- An http request requires async/await, therefore the code is asynchronous (
Task
)
Given this, we want to use TaskEither
.
Note: Even if the return type of the final function is
TaskEither
, it may be more appropriate to use anotherfpdart
's type for some requests while converting all toTaskEither
at the end.
We therefore perform the http request using tryCatch
from TaskEither
:
TaskEither<OpenMeteoApiFpdartLocationFailure, http.Response>.tryCatch(
() => _httpClient.get(
Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
),
),
LocationHttpRequestFpdartFailure.new,
)
You can read more about http request and
fpdart
in a previous article: How to make API requests with validation in fpdart
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.
Http response validation using Either
The http request was the only asynchronous operation in this function. Now we want to validate the response and convert it to a Location
.
Note: Even if the rest of the code is synchronous, we still need to use
TaskEither
.
The validation in our example requires 7 steps:
- Check that the response is 200 and retrieve the response
body
- Decode the
body
from JSON - Cast
dynamic
(returned byjsonDecode
) to aMap
- Get the
results
key theMap
(if it exists) - Cast
results
to aList
- Get the first element from the
List
(if it exists) - Convert the first element of the
List
to aLocation
Future<Location> locationSearch(String query) async {
final locationRequest = Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
);
final locationResponse = await _httpClient.get(locationRequest);
/// 1. Check valid response
if (locationResponse.statusCode != 200) {
throw LocationRequestFailure();
}
/// 2 & 3. `jsonDecode` and cast to [Map]
final locationJson = jsonDecode(locationResponse.body) as Map;
/// 4. Get `results` from [Map]
if (!locationJson.containsKey('results')) throw LocationNotFoundFailure();
/// 5. Cast `results` to [List]
final results = locationJson['results'] as List;
/// Check if the [List] has at least one element
if (results.isEmpty) throw LocationNotFoundFailure();
/// 6 & 7. Get first element and convert it to [Location]
return Location.fromJson(results.first as Map<String, dynamic>);
}
Validation using chainEither
from TaskEither
Since all these validations are synchronous, we need to chain a series of Either
to the current TaskEither
. We are going to use the chainEither
method for this.
chainEither
gives us the current value fromTaskEither
and requires us to returns anEither
.
Either
will contain an error if the validation is unsuccessful (Left
), or the valid value otherwise (Right
)
The first step is getting the body
from the result:
.chainEither(
(response) => Either<E, http.Response>.fromPredicate(
response,
(r) => r.statusCode == 200,
LocationRequestFpdartFailure.new,
)
.map((r) => r.body);
)
fromPredicate
: Used to check that thestatusCode
is 200. If it is not, thenEither
will return aLeft
containingLocationRequestFpdartFailure
map
: Once we verified the status code, we extract thebody
from the response
The second step is using jsonDecode
. jsonDecode
may fail (throw
), so we use the tryCatch
method from Either
:
.chainEither(
(body) => Either.tryCatch(
() => jsonDecode(body),
(_, __) => LocationInvalidJsonDecodeFpdartFailure(body),
),
)
The third step is casting to Map
, using the as
keyword.
Also casting may fail (throw
) in dart. For this reason, fpdart
provides a safeCast
method that allows to cast a value using Either
:
.chainEither(
(json) => Either<OpenMeteoApiFpdartLocationFailure,
Map<dynamic, dynamic>>.safeCast(
json,
LocationInvalidMapFpdartFailure.new,
),
)
Next step is getting results
from the Map
.
We use the lookup
extension method provided by fpdart
. lookup
returns an Option
, which informs us that the value may be missing.
We then convert Option
to Either
using toEither
:
.chainEither(
(body) => body
.lookup('results')
.toEither(LocationKeyNotFoundFpdartFailure.new),
)
We then use safeCast
again to convert the value to List
:
.chainEither(
(currentWeather) => Either<OpenMeteoApiFpdartLocationFailure,
List<dynamic>>.safeCast(
currentWeather,
LocationInvalidListFpdartFailure.new,
),
)
We extract the first value from the List
using head
. Just like before, head
is an extension method that returns Option
, which we then covert to Either
:
.chainEither(
(results) =>
results.head.toEither(LocationDataNotFoundFpdartFailure.new),
)
Finally, the last step is converting the value to Location
.
We use the fromJson
method we defined for the Location
class. Since this method uses as
under the hood, also this may fail.
Therefore, we use again tryCatch
to perform the validation:
.chainEither(
(weather) => Either.tryCatch(
() => Location.fromJson(weather as Map<String, dynamic>),
LocationFormattingFpdartFailure.new,
),
);
Putting all together: Open Meteo API using fpdart
After all these steps we are finally done! π
The final result is the following:
/// Finds a [Location] `/v1/search/?name=(query)`.
TaskEither<OpenMeteoApiFpdartLocationFailure, Location> locationSearch(String query) =>
TaskEither<OpenMeteoApiFpdartLocationFailure, http.Response>.tryCatch(
() => _httpClient.get(
Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
),
),
LocationHttpRequestFpdartFailure.new,
)
.chainEither(
(response) => Either<E, http.Response>.fromPredicate(
response,
(r) => r.statusCode == 200,
LocationRequestFpdartFailure.new,
)
.map((r) => r.body);
)
.chainEither(
(body) => Either.tryCatch(
() => jsonDecode(body),
(_, __) => LocationInvalidJsonDecodeFpdartFailure(body),
),
)
.chainEither(
(json) => Either<OpenMeteoApiFpdartLocationFailure,
Map<dynamic, dynamic>>.safeCast(
json,
LocationInvalidMapFpdartFailure.new,
),
)
.chainEither(
(body) => body
.lookup('results')
.toEither(LocationKeyNotFoundFpdartFailure.new),
)
.chainEither(
(currentWeather) => Either<OpenMeteoApiFpdartLocationFailure,
List<dynamic>>.safeCast(
currentWeather,
LocationInvalidListFpdartFailure.new,
),
)
.chainEither(
(results) =>
results.head.toEither(LocationDataNotFoundFpdartFailure.new),
)
.chainEither(
(weather) => Either.tryCatch(
() => Location.fromJson(weather as Map<String, dynamic>),
LocationFormattingFpdartFailure.new,
),
);
As you can see, there are a lot of steps involved even for a simple API request.
Using fpdart
and functional programming we are able to make this request 100% safe. When calling this function, we are sure that this will never fail and we will receive all the possible errors encoded by the OpenMeteoApiFpdartLocationFailure
class.
We can then extract the valid result Location
, or map the OpenMeteoApiFpdartLocationFailure
to a user-friendly error message, telling exactly what has gone wrong.
That's it for our 2 part series about fpdart
and Open Meteo API π
We covered a lot of ground, even for an apparently simple function to make and validate an http request.
Check out the full example on the fpdart
repository:
You can subscribe to my newsletter here below to stay always up to date with the latests news, tips, articles, and tutorials about fpart
π
Thanks for reading.