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.
- Read part 1: Project Objectives And Configuration
- Read part 2: Data model and Storage interface
- Read part 3: Dependencies and Errors in Functional Programming
- Read part 4: How to use fpdart and riverpod 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 thegetAllEvent
function - Learn how to use the Do notation with the
.Do
constructor infpdart
- Use
TaskEither
and.tryCatch
to execute aFuture
and catch errors - Match the result value to a valid instance of
GetAllEventState
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: 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
'sAsyncValue
to handle unexpected errors - We use
ReaderTask
fromfpdart
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>
:
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 successfulGetAllEventError
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
:
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 anyfpdart
'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:
- Call
getAll
fromStorageService
to getList<EventEntity>
while handling possible errors in the request - Mapping
List<EventEntity>
to a valid instance ofGetAllEventState
(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 infpdart
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
andTask
you can read Future & Task: asynchronous Functional Programming
Calling
run()
is not necessary whenfpdart
provides a built-in function to convert from one type to another.In this case, a built-in function to convert from
Task
toReaderTask
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 executegetAll
from the provided instance ofStorageService
- Handle possible errors using the
.tryCatch
constructor ofTaskEither
- Handle possible errors using the
- Call
match
to map error and success values toGetAllEventState
- Call
run
to execute the resultingTask
and return aFuture
insideReaderTask
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.