Explore the SpaceX GraphQL API by building a Xamarin Forms app
Tags: spacex, graphql, xamarinforms, xaml
Published: 2021-03-02 22:47:20

---
title: Explore the SpaceX GraphQL API by building a Xamarin Forms app
published: true
description: If you've ever wondered how to integrate that awesome GraphQL API from SpaceX in your app, this post is for you.
tags: spacex, graphql, xamarinforms, xaml
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgekq76t2056hr8b65ol.png
---
### What's this about
We're going to build a Xamarin.Forms app that connects to the SpaceX GraphQL API and displays some cool rocket launch pictures. You'll see how to generate C# classes for typesafe coding, send queries with the GraphQLHttpClient, build an elegant UI in XAML and use a Xamarin.Forms Behavior.

3
2
1
🚀

### Generate types from the GraphQl schema
Here's the endpoint where you can get the schema from https://api.spacex.land/graphql

If you have your own way of generating the types, you can skip this section but this is how I did it.

In a folder of your choice, initialize a new yarn project

```
yarn init
```
Next, install these dev dependencies
```
yarn add -D graphql @graphql-codegen/c-sharp @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript
```

Then add the following script property to your `package.json`
```JSON
  "scripts": {
    "generate": "graphql-codegen --config codegen.yml"
  }
```
Now we're done with the packages.json and we can move on to configuring the node module that will take care of generating the classes.

Here's how that YAML file needs to look like
```YAML
overwrite: true
schema: "https://api.spacex.land/graphql"
#documents: "src/**/*.graphql"
generates:
  gen/Models.cs:
    plugins:
      - "c-sharp"
    config: 
      namespaceName: SpaceXGraphQL.SpaceX.gen
      scalars:
        timestamptz: DateTime
        uuid: Guid
  ./graphql.schema.json:
    plugins:
      - "introspection"
```
Let me translate: get the schema from https://api.spacex.land/graphql, using the "c-sharp" plugin generate classes with the namespace `SpaceXGraphQL.SpaceX.gen` into the file (relative to self) `gen/Models.cs`.

The `scalars` property is a mapping between custom types from the GraphQL schema and matching C# types. It looks like they use Postgres under the hood. `timestamptz` stores date and time info including the time zone and `uuid` is just a 128 -bit quantity for identifying entities.

{% collapsible GraphiQL (https://api.spacex.land/graphql/) %} 
![uuid](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bzafq46qqwjpqo4bfha8.png)

![timestamp](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4mn7nd6unql5b7ag2v3z.png)
{% endcollapsible %}

Now install the referenced dependencies with
```
yarn install
```

And run the script
```
yarn generate
```

Our classes were generated! Yay! 🎉

![Generated models](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ea5tbqu7yhpwryccql9f.png)

Thats it! We now have types to work with.  

### Write the Xamarin.Forms App
Create a new Xamarin.Forms app and add the following NuGet packages
```XML
<PackageReference Include="GraphQL.Client" Version="3.2.1" />
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="3.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="3.2.2" />
```
Next, add the generated classes to your shared project. I actually added the whole yarn project folder
![generated models](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86sb8c5yrnt6rq3ifcs1.png)

Now let's have fun!

Set up this global style
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SpaceXGraphQL.App">
    <Application.Resources>

        <!-- Styles -->
        <Style TargetType="NavigationPage">
            <Setter Property="BarBackgroundColor" Value="#384355"/>
            <Setter Property="BarTextColor" Value="#FF543D"/>
        </Style>
        <!-- Styles -->
        
    </Application.Resources>
</Application>
```

The app will have two pages. 
The first page shows a list of rocket launches, uses infinite scrolling and shows a pulsing indicator for "upcoming" launches.

To keep things simple I didn't use any third party UI library nor did I integrate Prism or MvvmCross, though I'm a big fan of those.

Start with the view model, I named mine `LaunchesPageViewModel`. It implements `INotifyPropertyChanged` like so
```csharp
 public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
```
It needs a few properties to hold the rocket launches, to indicate that it is fetching data and a command to load the next batch/page of launches
```csharp
        public Command LoadMoreCommand
        {
            get => _loadMoreCommand;
            set
            {
                _loadMoreCommand = value;
                OnPropertyChanged();
            }
        }

        public bool IsLoading
        {
            get => _isLoading;
            set
            {
                _isLoading = value;
                OnPropertyChanged();
            }
        }

        public ObservableCollection<Types.Launch> Launches
        {
            get => _launches;
            set
            {
                _launches = value;
                OnPropertyChanged();
            }
        }
```
Next, in the constructor I'm doing a bit of init work. The `LoadMoreCommand` fetches rocket launches and, in case that it is executed again while the previous fetching isn't done, it just returns. When I'm done with the init, I start fetching.
```csharp
 public LaunchesPageViewModel()
        {
            _client = new GraphQLHttpClient("https://api.spacex.land/graphql", new NewtonsoftJsonSerializer());
            Launches = new ObservableCollection<Types.Launch>();

            LoadMoreCommand = new Command(async () =>
            {
                if (_isFetchingMore)
                {
                    return;
                }

                try
                {
                    _isFetchingMore = true;
                    await GetLaunches();
                }
                finally
                {
                    _isFetchingMore = false;
                }
            });

            IsLoading = true;
            GetLaunches().ContinueWith((_, __) => IsLoading = false, null);
        }
```

And now for the last piece, the actual GraphQL query. We're making use of the "launches" query, which allows use to tell it how many launch events to return and how many to skip, effectively offering a paginated api.
Notice that I'm using the amount of launches that I currently have as offset.
```csharp
        private async Task GetLaunches()
        {
            var launchesRequest = new GraphQLRequest
            {
                Query = @"query getLaunches($limit: Int, $offset: Int)
                                {
                                  launches(limit: $limit, offset: $offset) {
                                    id
                                    is_tentative
                                    upcoming                                    
                                    mission_name
                                    links {
                                      article_link
                                      video_link
                                      flickr_images
                                      mission_patch
                                    }
                                    launch_date_utc
                                    details
                                  }
                                }",
                Variables = new
                {
                    limit = 15,
                    offset = Launches.Count
                }
            };

            var response = await _client.SendQueryAsync<Types.Query>(launchesRequest);
            if (!(response.Errors ?? Array.Empty<GraphQLError>()).Any())
            {
                foreach (var launch in response.Data.launches)
                {
                    Launches.Add(launch);
                }
            }
        }
```

And that's all for the `LaunchesPageViewModel`.
The `LaunchesPage`, is implemented exclusively in XAML, except for a selection handler.

Basically I'm making use of a [CollectionView](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/collectionview/introduction) (for its virtualization) to show the launches. Notice the properties `RemainingItemsThreshold` and `RemainingItemsThresholdReachedCommand`. Whenever theres 5 items or fewer to show, the `CollectionView` calls the `LoadMoreCommand`, which sends a query to the GraphQL API to fetch the next batch of launches.

{% collapsible LaunchesPage.xaml %}
```XML
<!-- Loading -->
        <ActivityIndicator IsVisible="{Binding IsLoading}"
                           IsRunning="True"
                           Color="#FF543D"
                           VerticalOptions="Center" HorizontalOptions="Center"
                           WidthRequest="120" HeightRequest="120" />

        <!-- Launches -->
        <CollectionView ItemsSource="{Binding Launches}"
                        ItemSizingStrategy="MeasureFirstItem"
                        SelectionMode="Single"
                        SelectionChanged="OnLaunchSelected"
                        RemainingItemsThreshold="5"
                        RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
                        Margin="10">
            <CollectionView.ItemsLayout>
                <LinearItemsLayout Orientation="Vertical"
                                   ItemSpacing="5.0" />
            </CollectionView.ItemsLayout>
            
            <!-- Launch Template -->
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Frame CornerRadius="15"
                           Padding="0" Margin="5,0,5,0"
                           HasShadow="False"
                           BackgroundColor="#041727"
                           IsClippedToBounds="True">
                        <Grid RowDefinitions="*,*"
                              ColumnDefinitions="Auto,*"
                              HeightRequest="80">

                            <!-- Mission patch -->
                            <Image HeightRequest="60" WidthRequest="60"
                                   Margin="10"
                                   Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
                                   Aspect="AspectFit"
                                   HorizontalOptions="CenterAndExpand"
                                   VerticalOptions="CenterAndExpand"
                                   Source="{Binding links.mission_patch}" />
                            <Label Grid.Row="0" Grid.Column="1"
                                   VerticalOptions="End"
                                   TextColor="White"
                                   FontAttributes="Bold"
                                   Text="{Binding mission_name}" />
                            <Label Grid.Row="1" Grid.Column="1"
                                   FontSize="Small"
                                   TextColor="#FF543D"
                                   VerticalOptions="Start"
                                   Text="{Binding launch_date_utc}" />
                            
                            <!-- Upcoming indicator -->
                            <BoxView Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
                                     HeightRequest="10" WidthRequest="10"
                                     Margin="20"
                                     CornerRadius="5"
                                     HorizontalOptions="End" VerticalOptions="Center"
                                     Color="#FF543D"
                                     IsVisible="False">
                                <BoxView.Behaviors>
                                    <behaviors:PulseBehavior />
                                </BoxView.Behaviors>
                                <BoxView.Triggers>
                                    <DataTrigger TargetType="BoxView"
                                                 Binding="{Binding Path=upcoming}"
                                                 Value="True">
                                        <DataTrigger.Setters>
                                            <Setter Property="IsVisible" Value="True" />
                                        </DataTrigger.Setters>
                                    </DataTrigger>
                                </BoxView.Triggers>
                            </BoxView>
                        </Grid>
                    </Frame>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
```
{% endcollapsible %}

{% collapsible LaunchesPage.xaml.cs %}
```csharp
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class LaunchesPage : ContentPage
    {
        public LaunchesPage()
        {
            InitializeComponent();
            BindingContext = new LaunchesPageViewModel();
        }

        private async void OnLaunchSelected(object sender, SelectionChangedEventArgs e)
        {
            if (e.CurrentSelection.Any() && e.CurrentSelection.First() is Types.Launch launch)
            {
                await Navigation.PushAsync(new LaunchPage(new LaunchPageViewModel(launch.id)));

                if (sender is CollectionView cw)
                {
                    cw.SelectedItem = null;
                }
            }
        }
    }
```
{% endcollapsible %}

{% collapsible PulseBehavior.cs %}
```csharp
using Xamarin.Forms;
using Xamarin.Forms.Internals;

namespace SpaceXGraphQL.Behaviors
{
    public class PulseBehavior : Behavior<View>
    {
        private const string PulsingAnimation = "Pulsing";
        private bool _running = false;
        private bool _isReversed = false;

        protected override void OnAttachedTo(View bindable)
        {
            base.OnAttachedTo(bindable);

            _running = true;
            bindable.Animate(PulsingAnimation,
                d =>
                {
                    var newScale = _isReversed ? 1.0d - d : d;
                    bindable.Scale = newScale.Clamp(0.3, 1.0);
                },
                length: 1000,
                easing: Easing.CubicInOut,
                repeat: () =>
                {
                    _isReversed = !_isReversed;
                    return _running;
                });
        }

        protected override void OnDetachingFrom(View bindable)
        {
            base.OnDetachingFrom(bindable);

            _running = false;
            bindable.AbortAnimation(PulsingAnimation);
        }
    }
}
```
{% endcollapsible %}

We're done with the first page and the result looks like this
![Launches overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n626ska3wth2nfjzdznj.png)

Now for the second page. It is going to show some launch details like the coolest images of the launch, the launch name and patch, a link to the original article and a way of sharing the images.

We're starting with the view model which I named `LaunchPageViewModel`. It uses the exact same `INotifyPropertyChanged` implementation, so I'm skipping over it here.
This view model needs properties to hold the data of a single launch and commands to open the original article link and to share a launch image.
```csharp
        public Types.Launch Launch
        {
            get => _launch;
            set
            {
                _launch = value;
                OnPropertyChanged();
            }
        }

        public Command ArticleLinkTappedCommand { get; set; }
        public Command ImageTappedCommand { get; set; }
```

In the constructor, the view model initializes its members and the two commands, making use of Xamarin Essentials for opening links and sharing data.
You'll notice that the constructor take as argument a launch id. That's the id of the launch whose data we're querying and showing.

Finally, here's how the GetLaunch() method looks like
```csharp
private async Task GetLaunch()
        {
            Launch = null;
            var launchRequest = new GraphQLRequest
            {
                Query = @"query getLaunch($id: ID!) {
                                      launch(id: $id) {
                                        id
                                        is_tentative
                                        upcoming
                                        mission_name
                                        links {
                                          article_link
                                          video_link
                                          flickr_images
                                          mission_patch
                                        }
                                        launch_date_utc
                                        details
                                      }
                                    }",
                Variables = new
                {
                    id = _launchId,
                }
            };

            var response = await _client.SendQueryAsync<Types.Query>(launchRequest);
            if (!(response.Errors ?? Array.Empty<GraphQLError>()).Any())
            {
                Launch = response.Data.launch;
            }
        }
```

Again, the page is almost exclusively implemented in XAML.
{% collapsible LaunchPage.xaml %}
```XML
<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:converters="clr-namespace:SpaceXGraphQL.Converters;assembly=SpaceXGraphQL"
             x:Class="SpaceXGraphQL.LaunchPage"
             BackgroundColor="#041727"
             x:Name="ParentPage">

    <ContentPage.Resources>
        <converters:GetLastItemConverter x:Key="GetLastItemConverter" />
    </ContentPage.Resources>

    <ScrollView Padding="20, 0">
        <StackLayout>
            <!-- Top image -->
            <Grid VerticalOptions="Start">
                <Frame CornerRadius="20"
                       Padding="0"
                       HasShadow="True"
                       IsClippedToBounds="True">
                    <Image Source="{Binding Launch.links.flickr_images, Converter={StaticResource GetLastItemConverter}}"
                           HorizontalOptions="FillAndExpand"
                           BackgroundColor="#384355"
                           Aspect="AspectFill"
                           HeightRequest="250" />
                </Frame>

                <!-- Patch, name and date -->
                <StackLayout Orientation="Horizontal"
                             HorizontalOptions="Start" VerticalOptions="Start"
                             Margin="20">
                    <Image Source="{Binding Launch.links.mission_patch}"
                           Aspect="AspectFit"
                           HeightRequest="40" WidthRequest="40" />
                    <StackLayout>

                        <Label Text="{Binding Launch.mission_name}"
                               TextColor="White"
                               FontAttributes="Bold" FontSize="Large"
                               HorizontalOptions="Start" VerticalOptions="Center" />
                        <Label Text="{Binding Launch.launch_date_utc}"
                               TextColor="LightGray"
                               FontAttributes="Bold" FontSize="Micro"
                               HorizontalOptions="Start" VerticalOptions="Center" />
                    </StackLayout>
                </StackLayout>
            </Grid>

            <!-- Article link -->
            <Label VerticalOptions="End" HorizontalOptions="End"
                   Margin="0, 5, 20,0">
                <Label.FormattedText>
                    <FormattedString>
                        <Span Text="Article Link" TextColor="RoyalBlue"
                              FontSize="Small" FontAttributes="Bold">
                            <Span.GestureRecognizers>
                                <TapGestureRecognizer Command="{Binding ArticleLinkTappedCommand}" />
                            </Span.GestureRecognizers>
                        </Span>
                    </FormattedString>
                </Label.FormattedText>
            </Label>

            <!-- Description -->
            <Label Text="Description" TextColor="#FF543D" FontSize="Large"
                   FontAttributes="Bold"
                   Margin="0, 20,0,5" />

            <Label Text="{Binding Launch.details}"
                   FontSize="Body"
                   TextColor="White" />

            <!-- Media -->
            <Label Text="Media" TextColor="#FF543D"
                   FontSize="Large" FontAttributes="Bold" Margin="0, 20,0,5" />
            <StackLayout BindableLayout.ItemsSource="{Binding Launch.links.flickr_images}">
                <BindableLayout.EmptyView>
                    <Label Text="Loading ..." TextColor="White" FontSize="Small"
                           HorizontalOptions="Center"/>
                </BindableLayout.EmptyView>

                <!-- Images -->
                <BindableLayout.ItemTemplate>
                    <DataTemplate>
                        <SwipeView>
                            <SwipeView.RightItems>
                                <SwipeItems>
                                    <SwipeItem Text="Share"
                                               BackgroundColor="#FF543D"
                                               Command="{Binding Source={x:Reference ParentPage}, Path=BindingContext.ImageTappedCommand}"
                                               CommandParameter="{Binding .}" />
                                </SwipeItems>
                            </SwipeView.RightItems>
                            <Frame CornerRadius="20"
                                   Padding="0" Margin="0,0,0,5"
                                   HasShadow="True"
                                   IsClippedToBounds="True">
                                <Image Source="{Binding .}"
                                       HorizontalOptions="FillAndExpand"
                                       BackgroundColor="#384355"
                                       Aspect="AspectFill"
                                       HeightRequest="250" />
                            </Frame>
                        </SwipeView>

                    </DataTemplate>
                </BindableLayout.ItemTemplate>
            </StackLayout>
        </StackLayout>
    </ScrollView>
</ContentPage>
```
{% endcollapsible %}

{% collapsible LaunchPage.xaml.cs %}
```csharp
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace SpaceXGraphQL
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class LaunchPage : ContentPage
    {
        
        public LaunchPage(LaunchPageViewModel viewModel)
        {
            InitializeComponent();
            BindingContext = viewModel;
        }
    }
}
```
{% endcollapsible %}

{% collapsible GetLastItemConverter.cs %}
```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Xamarin.Forms;

namespace SpaceXGraphQL.Converters
{
    public class GetLastItemConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is IEnumerable<object> enumerable)
            {
                return new List<object>(enumerable).Last();
            }

            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
```
{% endcollapsible %}

We're done with the second and last page and the result looks like this
![Launch details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4wbz3erhn4c0tua5dzgv.png)

###Conclusion
Except for the hiccup while generating the types, connecting to the GraphQL API was pretty easy. Kudos to them for providing the data for free!
Let me know if you're using another tool for code generation which can handle SpaceX's GraphQL schema.

I have to say that implementing the app in two afternoons was a lot of fun. I was pleasantly surprised by how good the Xamarin.Forms hot reload has become. Last time I checked, which was in the summer of 2020, it was much less stable. This time around I could actually use it to prototype my app.

If you've made it this far, I got a bonus for you! Here's the  link to my GitHub repo with all the code: https://github.com/mariusmuntean/SpaceXGraphQL