Pure Functional app in Flutter - Pokemon app using fpdart and Functional Programming

Sandro Maglione

Sandro Maglione

Functional programming

Pure Functional app in Flutter, is it possible? Of course! Today we are going to explore a Pure Functional app in Flutter using fpdart. I am going to explain step by step how to use the power of Functional Programming and fpdart to develop a Flutter application.

You will learn many Functional Programming types from fpdart used to make your application more composable. The app will use riverpod for state management, http to perform remote requests, freezed, and flutter_hooks. And of course fpdart for Functional Programming!

Do you want to learn more about fpdart and Functional Programming? Check out the previous two articles in which I explain the basics of fpdart and Functional Programming:

  1. Fpdart, Functional Programming in Dart and Flutter
  2. How to use fpdart Functional Programming in your Dart and Flutter app

A Pokemon app to explain Functional Programming!

That's right! The example you are going to see today is a Flutter app that lets you search and view your favorite Pokemon!

The app has a single screen. The screen shows the image of a pokemon fetched from the pokeAPI. It also has a TextField and a button that allows you to search and view a Pokemon given his id in the Pokedex.

Screen of the Pure Functional app using fpdart and Flutter.

A screenshot of the final application. You can view your favorite Pokemon and search for a new one!

The application is simple, but it has all the main components that you will find in a more complex app:

  • State management using riverpod to perform requests to the API and listen for the response
  • JSON serialization and deserialization to convert the response from the API to a dart object
  • freezed (Sum types) to display a different UI based on the status of the API request
  • flutter_hooks to reduce boilerplate code and improve readability
  • http to perform remote requests

Create the app and import the packages

We start from the beginning. Open a directory that you want and create a new Flutter app using the create command:

flutter create pokeapi_functional

You will now have a new pokeapi_functional folder that contains the app project.

We then need to import the required packages used in the app. Open the pubspec.yaml file inside the pokeapi_functional folder and copy-paste the code below:

pubspec.yaml
name: pokeapi_functional
description: Functional Programming using fpdart. Fetch and display pokemon from pokeApi.
publish_to: "none"
 
version: 1.0.0+1
 
environment:
  sdk: ">=2.12.0 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
 
  http: ^0.13.3
  hooks_riverpod: ^1.0.0-dev.4
  flutter_hooks: ^0.18.0
  freezed: ^0.14.2
  fpdart: ^0.0.7
 
dev_dependencies:
  flutter_test:
    sdk: flutter
  freezed_annotation: ^0.14.2
  build_runner: ^2.0.5
 
flutter:
  uses-material-design: true

We import all the packages mentioned in the previous section, as well as build_runner and freezed_annotation for code generation.

We are now ready to start working on the app!

Pokemon and Sprite models with JSON serialization

We start by defining the model classes. These are simple dart classes used to convert the JSON API response from pokeAPI to a dart object used inside the app.

We are going to request the Pokemon information from this endpoint in the pokeAPI.

{
  "id": 12,
  "name": "butterfree",
  "base_experience": 178,
  "height": 11,
  "is_default": true,
  "order": 16,
  "weight": 320,
  "sprites": { /* ... */ },
  /* ... */

For the purpose of this example, we are going to store the id, name, weight, height, and sprites of the Pokemon.

We create a new models folder inside lib that will contain our model classes. Inside the folder, we create a pokemon.dart file with the following code:

pokemon.dart
import 'package:pokeapi_functional/models/sprite.dart';
 
class Pokemon {
  final int id;
  final String name;
  final int height;
  final int weight;
  final Sprite sprites;
 
  const Pokemon({
    required this.id,
    required this.name,
    required this.height,
    required this.weight,
    required this.sprites,
  });
}

We also create a sprite.dart file with the following code:

sprite.dart
class Sprite {
  final String front_default;
 
  const Sprite({
    required this.front_default,
  });
}

The Sprite class contains the link to the image of the pokemon, while the Pokemon class contains all the information about the pokemon. As you can see, the Pokemon class contains a sprite field of type Sprite.

JSON deserialization

We need a method to convert the JSON response from the API (a plain String) to a Pokemon. We define a fromJson method that tries to map the response to a valid Pokemon object (we do the same also for the Sprite class):

pokemon.dart
import 'package:pokeapi_functional/models/sprite.dart';
 
/// Pokemon information, with method to deserialize json
class Pokemon {
  final int id;
  final String name;
  final int height;
  final int weight;
  final Sprite sprites;
 
  const Pokemon({
    required this.id,
    required this.name,
    required this.height,
    required this.weight,
    required this.sprites,
  });
 
  static Pokemon fromJson(Map<String, dynamic> json) {
    return Pokemon(
      id: json['id'] as int,
      name: json['name'] as String,
      weight: json['weight'] as int,
      height: json['height'] as int,
      sprites: Sprite.fromJson(json['sprites'] as Map<String, dynamic>),
    );
  }
}
sprite.dart
/// Pokemon sprite image, with method to deserialize json
class Sprite {
  final String front_default;
 
  const Sprite({
    required this.front_default,
  });
 
  static Sprite fromJson(Map<String, dynamic> json) {
    return Sprite(
      front_default: json['front_default'] as String,
    );
  }
}

Pure Functional API request

The next step is defining the methods to validate the user input and send the request to the API to fetch the Pokemon. This is where Functional Programming comes into play!

The principle underlining our code is composability. We want to have small pure functions, each performing one simple operation. Then we are going to compose these functions one by one to form the complete request.

Create a new folder for the API request

We create a new api folder inside lib, and inside it a fetch_pokemon.dart file.

We will not create any class, since we want our code to be completely functional. Instead, we are going to define a series of private functions (by adding the _ suffix to them) that cannot be accessed from any other file. These functions will be composed together to form the final function used to perform the API request. This will be the only function that we export from the file (it will be a public function).

Parse the user input in a Functional way

The user inserts the pokemon id using a TextField. Since the TextField value is a String while the pokemon id is an int, we need to parse the user input to the correct format before making the request. We use the parse method of dart.

The parse method can fail and throw a FormatException if the given String is not a valid int. We do not use try/catch or throw at all in Functional Programming. These constructs are not composable! We are going to use fpdart instead.

parse is a synchronous method that can fail:

  • For synchronous operations, we use the IO type of fpdart, which allows us to easily compose multiple methods
  • For operations that can fail, we use the Either type of fpdart

Therefore, we need a way to combine IO and Either together. fpdart provides a type called IOEither exactly for this usecase!

IOEither: a synchronous method that can fail

The first step when working with Functional Programming is defining the signature of the functions, which means writing the input and output types.

The parsing function returns an IOEither. The IOEither type requires two generic types (same as Either), the first one is the type of data to return when the function fails (also called Left), while the second is the return type when the function is successful (also called Right).

In our example, the error type will be a simple String, while the valid return type is an int. The signature of the function therefore is as follows:

/// Parse [String] to [int] in a functional way using [IOEither].
IOEither<String, int> _parseStringToInt(String str);

Try/Catch using IOEither in Functional Programming

We want to catch possible errors when calling the parse function. We use the IOEither.tryCatch constructor.

/// Parse [String] to [int] in a functional way using [IOEither].
IOEither<String, int> _parseStringToInt(String str) => IOEither.tryCatch(
      () => int.parse(str),
      (_, __) => 'Cannot convert input to valid pokemon id (it must be a number)!',
    );

We define the first function parameter to execute as if it can never fail (int.parse). In case of errors, the constructor will call the second function that returns the error message.

Validate the pokemon id, the pokemon must exist!

Not all int are valid pokemon! There are 898 pokemon known today. Therefore, the pokemon id must be between 1 and 898.

We define another function to validate the int value that we parsed in the previous step. The signature is similar. The function is synchronous (IO) and can fail (Either), IOEither!

/// Validate the pokemon id inserted by the user:
/// 1. Parse [String] from the user to [int]
/// 2. Check pokemon id in valid range
IOEither<String, int> _validateUserPokemonId(String pokemonId);

flatMap: chain functions together

We need to chain the previous function so that we can parse the input String to int.

In an imperative world, we would need to write a long series of if-else. Using Functional Programming is easier. We chain an IOEither with another using the flatMap method:

/// Validate the pokemon id inserted by the user:
/// 1. Parse [String] from the user to [int]
/// 2. Check pokemon id in valid range
///
/// Chain (1) and (2) using `flatMap`.
IOEither<String, int> _validateUserPokemonId(String pokemonId) =>
    _parseStringToInt(pokemonId).flatMap(
      (intPokemonId) => /* ... */
    );

flatMap will execute _parseStringToInt. If _parseStringToInt is successful, flatMap will call the given function, passing the intPokemonId. If _parseStringToInt fails instead, no function will be called and the String error we previously defined will be returned.

IOEither from an int using IOEither.fromPredicate

Inside the flatMap method, we have access to the valid Pokemon id as int (intPokemonId). Since our function returns an IOEither, we need to build an IOEither from an int passing a validation function. We use IOEither.fromPredicate!

/// Validate the pokemon id inserted by the user:
/// 1. Parse [String] from the user to [int]
/// 2. Check pokemon id in valid range
///
/// Chain (1) and (2) using `flatMap`.
IOEither<String, int> _validateUserPokemonId(String pokemonId) =>
    _parseStringToInt(pokemonId).flatMap(
      (intPokemonId) => IOEither.fromPredicate(
        intPokemonId,
        (id) =>
            id >= Constants.minimumPokemonId &&
            id <= Constants.maximumPokemonId,
        (id) =>
            'Invalid pokemon id $id: the id must be between ${Constants.minimumPokemonId} and ${Constants.maximumPokemonId + 1}!',
      ),
    );

IOEither.fromPredicate takes the value to validate (the Pokemon id), a validation function (id between 1 and 898), and an error String in case the given int value is not valid. Simple and powerful!

HTTP request to get the Pokemon: TaskEither

Finally, the last step is implementing the function that performs the API request and returns a Pokemon.

If we use the same logic as before, we can say that:

  • The function performs an asynchronous operation
  • The operation can fail

For asynchronous operations, we use the Task type. For operations that can fail, we use the Either type. Therefore, the return type of the function will be TaskEither!

/// Make HTTP request to fetch pokemon information from the pokeAPI
/// using [TaskEither] to perform an async request in a composable way.
TaskEither<String, Pokemon> fetchPokemon(int pokemonId);

Remote request using the http package

We are going to use the tryCatch constructor of TaskEither to catch possible errors. Using the http package, we perform a simple request and then we convert the JSON String to a Pokemon:

/// Make HTTP request to fetch pokemon information from the pokeAPI
/// using [TaskEither] to perform an async request in a composable way.
TaskEither<String, Pokemon> fetchPokemon(int pokemonId) => TaskEither.tryCatch(
      () async {
        final url = Uri.parse(Constants.requestAPIUrl(pokemonId));
        final response = await http.get(url);
        return Pokemon.fromJson(
          jsonDecode(response.body) as Map<String, dynamic>,
        );
      },
      (error, __) => 'Unknown error: $error',
    );

Composing IOEither and TaskEither: flatMapTask

The final function that takes the user input String and returns a Pokemon is extremely simple!

/// Try to parse the user input from [String] to [int] using [IOEither].
/// We use [IOEither] since the `parse` method is **synchronous** (no need of [Future]).
///
/// Then check that the pokemon id is in the valid range.
///
/// If the validation is successful, then fetch the pokemon information from the [int] id.
///
/// All the functions are simply chained together following the principle of composability.
TaskEither<String, Pokemon> fetchPokemonFromUserInput(String pokemonId) =>
    _validateUserPokemonId(pokemonId).flatMapTask(fetchPokemon);

That's the power of Functional Programming! All our functions are composable. We can use flatMapTask to chain the _validateUserPokemonId function, that returns IOEither, with the fetchPokemon function that returns TaskEither.

RequestStatus: using freezed to handle all the app states

When we make a remote request, our app can be in at least 4 states:

  1. Initial state: When the app just launched and it is waiting for any input or function call
  2. Loading state: State when the app is waiting for the API request to complete
  3. Error state: State when the API request failed with an error
  4. Succes state: State when the Pokemon is successfully fetched

We want to force our app to define what to display for each of these states. Therefore, we are going to use freezed to define these 4 states as follows:

request_status.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:pokeapi_functional/models/pokemon.dart';
 
part 'request_status.freezed.dart';
 
/// Different request status when making API request.
///
/// Each status maps to a different UI.
@freezed
class RequestStatus with _$RequestStatus {
  const factory RequestStatus.initial() = InitialRequestStatus;
  const factory RequestStatus.loading() = LoadingRequestStatus;
  const factory RequestStatus.error(String string) = ErrorRequestStatus;
  const factory RequestStatus.success(Pokemon pokemon) = SuccessRequestStatus;
}

Remember to run the build command to generate the class:

flutter packages pub run build_runner build

State management using riverpod

We are going to use riverpod to manage the state of the app. As we said previously, the app can be in 4 states defined by the RequestStatus freezed class:

pokemon_state.dart
/// Manage the [Pokemon] state using [Either] ([TaskEither]) to handle possible errors.
///
/// Each [RequestStatus] changes the UI displayed to the user.
class PokemonState extends StateNotifier<RequestStatus> {
  PokemonState() : super(const RequestStatus.initial());
}

Our provider will have two methods:

  1. The first method takes an int and retrieves the initial random Pokemon to display at the start of the app
  2. The second method takes the user's input String and tries to fetch the Pokemon from the API

We use both the fetchPokemon and fetchPokemonFromUserInput functions we defined in our api folder.

pokemon_state.dart
/// Manage the [Pokemon] state using [Either] ([TaskEither]) to handle possible errors.
///
/// Each [RequestStatus] changes the UI displayed to the user.
class PokemonState extends StateNotifier<RequestStatus> {
  PokemonState() : super(const RequestStatus.initial());
 
  /// Initial request, fetch random pokemon passing the pokemon id.
  Future<Unit> fetchRandom() async => _pokemonRequest(
        () => fetchPokemon(
          randomInt(
            Constants.minimumPokemonId,
            Constants.maximumPokemonId + 1,
          ).run(),
        ),
      );
 
  /// User request, try to convert user input to [int] and then
  /// request the pokemon if successful.
  Future<Unit> fetch(String pokemonId) async => _pokemonRequest(
        () => fetchPokemonFromUserInput(pokemonId),
      );
}

Run a TaskEither and match the result

Both functions above use a method called _pokemonRequest. This method performs the API request by calling the run method of TaskEither. It then uses the match method to return an error state in case of errors, or a success state in case the request is valid:

/// Generic private method to perform request and update the state.
Future<Unit> _pokemonRequest(
  TaskEither<String, Pokemon> Function() request,
) async {
  state = RequestStatus.loading();
  final pokemon = request();
  state = (await pokemon.run()).match(
    (error) => RequestStatus.error(error),
    (pokemon) => RequestStatus.success(pokemon),
  );
  return unit;
}

Finally, we define the actual StateNotifierProvider used inside the display widget:

pokemon_state.dart
import 'package:fpdart/fpdart.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pokeapi_functional/api/fetch_pokemon.dart';
import 'package:pokeapi_functional/constants/constants.dart';
import 'package:pokeapi_functional/models/pokemon.dart';
import 'package:pokeapi_functional/unions/request_status.dart';
 
/// Manage the [Pokemon] state using [Either] ([TaskEither]) to handle possible errors.
///
/// Each [RequestStatus] changes the UI displayed to the user.
class PokemonState extends StateNotifier<RequestStatus> {
  PokemonState() : super(const RequestStatus.initial());
 
  /// Initial request, fetch random pokemon passing the pokemon id.
  Future<Unit> fetchRandom() async => _pokemonRequest(
        () => fetchPokemon(
          randomInt(
            Constants.minimumPokemonId,
            Constants.maximumPokemonId + 1,
          ).run(),
        ),
      );
 
  /// User request, try to convert user input to [int] and then
  /// request the pokemon if successful.
  Future<Unit> fetch(String pokemonId) async => _pokemonRequest(
        () => fetchPokemonFromUserInput(pokemonId),
      );
 
  /// Generic private method to perform request and update the state.
  Future<Unit> _pokemonRequest(
    TaskEither<String, Pokemon> Function() request,
  ) async {
    state = RequestStatus.loading();
    final pokemon = request();
    state = (await pokemon.run()).match(
      (error) => RequestStatus.error(error),
      (pokemon) => RequestStatus.success(pokemon),
    );
    return unit;
  }
}
 
/// Create and expose provider.
final pokemonProvider = StateNotifierProvider<PokemonState, RequestStatus>(
  (_) => PokemonState(),
);

Display the Pokemon in the app

The last step is implementing the screen that will display the UI of the app. I will not go into the details of this part since it is not strictly related to Functional Programming. This is the final result:

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pokeapi_functional/controllers/pokemon_provider.dart';
 
void main() {
  /// [ProviderScope] required for riverpod state management
  runApp(ProviderScope(child: MyApp()));
}
 
class MyApp extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    /// [TextEditingController] using hooks
    final controller = useTextEditingController();
    final requestStatus = ref.watch(pokemonProvider);
    useEffect(() {
      /// Fetch the initial pokemon information (random pokemon).
      Future.delayed(Duration.zero, () {
        ref.read(pokemonProvider.notifier).fetchRandom();
      });
    }, []);
 
    return MaterialApp(
      title: 'Fpdart PokeAPI',
      home: Scaffold(
        body: Column(
          children: [
            /// [TextField] and [ElevatedButton] to input pokemon id to fetch
            TextField(
              controller: controller,
              decoration: InputDecoration(
                hintText: 'Insert pokemon id number',
              ),
            ),
            ElevatedButton(
              onPressed: () => ref
                  .read(
                    pokemonProvider.notifier,
                  )
                  .fetch(
                    controller.text,
                  ),
              child: Text('Get my pokemon!'),
            ),
 
            /// Map each [RequestStatus] to a different UI
            requestStatus.when(
              initial: () => Center(
                child: Column(
                  children: [
                    Text('Loading intial pokemon'),
                    CircularProgressIndicator(),
                  ],
                ),
              ),
              loading: () => Center(
                child: CircularProgressIndicator(),
              ),
 
              /// When either is [Left], display error message 💥
              error: (error) => Text(error),
 
              /// When either is [Right], display pokemon 🤩
              success: (pokemon) => Card(
                child: Column(
                  children: [
                    Image.network(
                      pokemon.sprites.front_default,
                      width: 200,
                      height: 200,
                    ),
                    Padding(
                      padding: const EdgeInsets.only(
                        bottom: 24,
                      ),
                      child: Text(
                        pokemon.name,
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 24,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
  • We use HookConsumerWidget from flutter_hooks. This widget allows us to use the useTextEditingController, ref.watch, and useEffect hooks.
  • We use the when method provided by the freezed RequestStatus class to force our UI to define what to display for each of the 4 states in the app.

The app is complete! You can find the complete source code in the fpdart repository:

That's all for today. If you liked the article and would like to stay updated about Functional Programming and fpdart, just follow me on Twitter at @SandroMaglione and subscribe to my newsletter here below. Thanks for reading.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.