How to fetch data from an API in Flutter using SpaceX API

Flutter Apr 30, 2023

Making a connection between an API and your Flutter application can be done with the help of packages. These packages will make sure that we can send HTTP requests to an API and deserialize the JSON data.

During this tutorial, we will be fetching rocket launch data from the SpaceX API.

1. New project

Let us start by creating a new project flutter create flutter_deserialize_json_response. After our new project is created we want to create the necessary directories. Make sure to create a directory for models and pages inside the lib directory.

flutter_deserialize_json_response
|-- lib/
  |-- models/
  |-- pages/

The next step is to modify the pubspec.yaml file to include all the packages we need.

name: flutter_deserialize_json_response
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.19.6 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.5
  json_annotation: ^4.8.0
  mocktail: ^0.3.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.3.3
  flutter_lints: ^2.0.0
  json_serializable: ^6.6.1
  test: ^1.22.0

flutter:
  uses-material-design: true

When working with .yaml files it is very important to pay attention to the indentation. If the indentation is not correct the .yaml file will not work correctly.

Let us go over the packages:

After we modified our pubspec.yaml let us run flutter pub get to install all the packages.

1.1 Model

Now that we have all the packages installed let us create our Launch model. From the SpaceX API we want to retrieve the launches. Whether the API returns a list of launches or just a single launch, we always want to make sure that it will be deserialized into our Launch model.

We want to create a launch.dart file inside our models directory, with the following content:

import 'package:json_annotation/json_annotation.dart';

part 'launch.g.dart';

@JsonSerializable()
class Launch {
  const Launch({
    required this.flightNumber,
    required this.name,
    this.success,
});

  factory Launch.fromJson(Map<String, dynamic> json) =>
      _$LaunchFromJson(json);

  final int flightNumber;
  final String name;
  final bool? success;
}

As you can see on line 3 we refer to part 'launch.g.dart' this file will be generated and will contain the _$LaunchFromJson function.

Before we generate our launch.g.dart file using build_runner, let us create a build.yaml file inside our main directory. The build_runner package will use the configuration inside our build.yaml file.

Here is the content of our build.yaml file:

targets:
  $default:
    builders:
      json_serializable:
        options:
          field_rename: snake
          create_to_json: false
          checked: true

The reason we created a build.yaml file is to avoid creating a toJson function because the json_serializable package can also be used to serialize a Dart object into JSON.

Also if we take a look at the data we receive from the SpaceX API you can see that they use snake_case for their JSON attributes. We want to convert them into pascalCase to follow the Dart convention. For example, we want to save the flight_number as flightNumber inside our model.

Now that our build.yaml is created we can execute flutter pub run build_runner build to create our launch.g.dart file.

1.2 Client

To fetch data from the API we create a separate client class called LaunchClient. We want to create a launch_client.dart file inside the lib directory with the following content:

import 'dart:convert';

import 'package:flutter_deserialize_json_response/models/launch.dart';
import 'package:http/http.dart' as http;

class LaunchClient {
  LaunchClient({http.Client? httpClient})
      : _httpClient = httpClient ?? http.Client();

  final http.Client _httpClient;

  Future<Launch> getLaunch() async {
    final request = Uri.https('api.spacexdata.com', 'v5/launches/latest');
    final response = await _httpClient.get(request);
    final bodyJson = jsonDecode(response.body) as Map<String, dynamic>;

    return Launch.fromJson(bodyJson);
  }

  Future<List<Launch>> getLaunches() async {
    final request = Uri.https('api.spacexdata.com', 'v5/launches');
    final response = await _httpClient.get(request);
    final bodyJson = jsonDecode(response.body) as List;

    return bodyJson
        .map((json) => Launch.fromJson(json as Map<String, dynamic>))
        .toList();
  }
}

As you can see we have two functions:

  1. getLaunch which deserializes a JSON object into a Launch object;
  2. getLaunches which deserializes a JSON list of objects into a list of Launch objects.

1.3 User interface

After we created our client we can create the page that will show the data. We want to create this file inside the pages directory and name it launch_page.dart. Here is our launch_page.dart file:

import 'package:flutter/material.dart';
import 'package:flutter_deserialize_json_response/launch_client.dart';
import 'package:flutter_deserialize_json_response/models/launch.dart';

class LaunchPage extends StatefulWidget {
  const LaunchPage({super.key});

  @override
  State<LaunchPage> createState() => _LaunchPageState();
}

class _LaunchPageState extends State<LaunchPage> {
  Launch _launch = const Launch(
    name: 'name',
    flightNumber: 0,
    success: true,
  );

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final LaunchClient launchClient = LaunchClient();

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(_launch.name, style: textTheme.headlineSmall),
        Text(_launch.flightNumber.toString(), style: textTheme.headlineSmall),
        Text(_launch.success.toString(), style: textTheme.headlineSmall),
        const SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
              onPressed: () async => await launchClient.getLaunch()
                  .then((launch) => setState(() => _launch = launch)),
              child: const Text('getLaunch'),
            ),
            ElevatedButton(
              onPressed: () async => await launchClient.getLaunches()
                  .then((launches) => setState(() => _launch = launches.first)),
              child: const Text('getLaunches'),
            )
          ],
        )
      ],
    );
  }
}

Now that we have our page let us make sure that it is shown. We can do this by making changes in our main.dart file. Our main.dart file looks like this:

import 'package:flutter/material.dart';
import 'package:flutter_deserialize_json_response/pages/launch_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(body: LaunchPage()),
    );
  }
}

Now that we have everything our directories and Dart files look like this:

flutter_deserialize_json_response
|-- lib/
  |-- models/
    |-- launch.dart
    |-- launch.g.dart
  |-- pages/
    |-- launch_page.dart
  |-- launch_client.dart
  |-- main.dart

If you have the same files, you can start your emulator and run main.dart.

When the application is started you will see that we have 2 buttons and 3 Text widgets that display launch data.

flutter_application_with_two_buttons_get_launches_and_get_launch

When your press either getLaunch or getLaunches you will see that the data changes.

flutter_application_with_two_buttons_get_launches_and_get_launch_with_displayed_data

In the getLaunch function, we deserialize a single JSON object into our Launch model. In the getLaunches function we deserialize a list of JSON objects into a list of Launch models and show the first one.

As you can see we successfully fetched data from the SpaceX API. To make sure that the deserialization works as intended we will add 2 tests.

1.4 Testing

It is always a good idea to add tests, this will make our application more robust and makes sure that we do not break anything when implementing changes in the future.

Inside the test directory we want to add a launch_client_test.dart file with the following content:

import 'package:flutter_deserialize_json_response/launch_client.dart';
import 'package:flutter_deserialize_json_response/models/launch.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockHttpClient extends Mock implements http.Client {}

class MockResponse extends Mock implements http.Response {}

class FakeUri extends Fake implements Uri {}

void main() {
  late http.Client httpClient;
  late LaunchClient launchClient;

  setUpAll(() {
    registerFallbackValue(FakeUri());
  });

  setUp(() {
    httpClient = MockHttpClient();
    launchClient = LaunchClient(httpClient: httpClient);
  });

  group('launch', () {
    test('returns a launch', () async {
      final response = MockResponse();
      when(() => response.statusCode).thenReturn(200);
      when(() => response.body).thenReturn('''
        {
          "flight_number": 1,
          "name": "first launch",
          "success": false
        }
        ''');
      when(() => httpClient.get(any())).thenAnswer((_) async => response);
      final actual = await launchClient.getLaunch();
      expect(
        actual,
        isA<Launch>()
            .having((l) => l.flightNumber, 'flightNumber', 1)
            .having((l) => l.name, 'name', 'first launch')
            .having((l) => l.success, 'success', false),
      );
    });

    test('returns multiple launches', () async {
      final response = MockResponse();
      when(() => response.statusCode).thenReturn(200);
      when(() => response.body).thenReturn('''
        [
          {
            "flight_number": 1,
            "name": "first launch",
            "success": false
          },
          {
            "flight_number": 2,
            "name": "second launch",
            "success": true
          }
        ]
        ''');
      when(() => httpClient.get(any())).thenAnswer((_) async => response);

      final actual = await launchClient.getLaunches();

      expect(
        actual.first,
        isA<Launch>()
            .having((l) => l.flightNumber, 'flightNumber', 1)
            .having((l) => l.name, 'name', 'first launch')
            .having((l) => l.success, 'success', false),
      );
      expect(actual, isA<List<Launch>>());
    });
  });
}

These tests make sure that we get the expected response with the provided JSON data.

You can see that in the first "launch" test "returns a launch" we are testing with a single JSON object:

{
  "flight_number": 1,
  "name": "first launch",
  "success": false
}

In the second "launch" test "returns multiple launches"  we are testing with a JSON list of objects:

[
  {
    "flight_number": 1,
    "name": "first launch",
    "success": false
  },
  {
    "flight_number": 2,
    "name": "second launch",
    "success": true
  }
]

If we run both tests by executing flutter test you will see that both tests are successful.

flutter_test_shows_that_all_tests_have_passed

With the tests we reached the end of the tutorial, I hope you learned something!

2. Conclusion

In this tutorial, you learned how to connect a Flutter application to an API using the http package. You also learned how to deserialize JSON responses using the json_serializable package and test them. If you enjoyed using these packages, make sure you like them on pub.dev and star them on GitHub.

The source code of this tutorial can be found here: flutter_deserialize_json_response.

Tags