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
ReaderTaskto implement thegetAllEventfunction - Learn how to use the Do notation with the
.Doconstructor infpdart - Use
TaskEitherand.tryCatchto execute aFutureand catch errors - Match the result value to a valid instance of
GetAllEventState
There is more 🤩
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
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'sAsyncValueto handle unexpected errors - We use
ReaderTaskfromfpdartand 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:
SuccessGetAllEventStatewhen the request is successfulGetAllEventErrorwhen 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
ReaderTaskthe_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
getAllfromStorageServiceto 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.newis 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.
runshould 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(),
),
);
identityis a function infpdartthat 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
FutureandTaskyou can read Future & Task: asynchronous Functional Programming
Calling
run()is not necessary whenfpdartprovides a built-in function to convert from one type to another.In this case, a built-in function to convert from
TasktoReaderTaskis 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
.Doconstructor to initialize a do notation function - Use
TaskEitherto executegetAllfrom the provided instance ofStorageService- Handle possible errors using the
.tryCatchconstructor ofTaskEither
- Handle possible errors using the
- Call
matchto map error and success values toGetAllEventState - Call
runto execute the resultingTaskand return aFutureinsideReaderTask
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.
