Building Flexible and Efficient Xamarin Apps with GraphQL
There are a lot of ways to approach building APIs for your apps, and no shortage of arguments that ensue whenever discussing them. One of the more standard approaches is to go with a RESTful design (cue the obligatory argument on what this actually means...), which has its advantages but can also lead to needing to make several requests to get the data you want, and can sometimes have a negative impact on mobile performance. Similarly, you're always going to pull back all of the data returned from those RESTful endpoints, regardless of whether your app needs it or not.
In the last few years Facebook introduced GraphQL which provides an alternative approach to API design. Graph APIs are not new, of course, but what GraphQL does is allow you to define a type system for your graph, and a discoverable query language with which to access it. You can also expose mutations through your API, so it goes beyond just being able to query your data. I'm not going to go too in depth into GraphQL itself here, but I recommend skimming the docs on their site.
There are several .NET libraries out there for helping you define your server-side schema as well as helpers for constructing queries, but in this post I'm just going to construct the requests by hand to show how simple it really is. To demo this, let's build a Xamarin.Forms app that queries GitHub's GraphQL API to search for repositories matching a given search query.
Query Engine
In order to send GitHub's GraphQL API a query, we simply need to make a POST request to their endpoint that supplies our query. First, let's define a client for the API:
public class GraphResult<T>
{
public T Data { get; set; }
}
public class GraphClient
{
private readonly HttpClient _client;
public GraphClient()
{
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("Authorization", "Bearer your-bearer-token-goes-here");
_client.DefaultRequestHeaders.Add("User-Agent", "Xamarin-GraphQL-Demo");
}
public async Task<T> Query<T>(string query)
{
var graphQuery = new { query };
var content = new StringContent(JsonConvert.SerializeObject(graphQuery), Encoding.UTF8, "application/json");
var response = await _client.PostAsync("https://api.github.com/graphql", content);
var json = await response.Content.ReadAsStringAsync();
var graphResult = JsonConvert.DeserializeObject<GraphResult<T>>(json);
return graphResult.Data;
}
}
This is pretty boilerplate HttpClient
and Json.NET code here. All we're doing is creating a JSON payload for the request, adding the appropriate headers, and deserializing the response. In a real application you'd also want to handle errors, which come back as an errors
property alongside data
, but we'll ignore that for now for the sake of a simple demo.
Next we need to define the models that will come back from GitHub's API:
public class Repository
{
public string Name { get; set; }
public string Url { get; set; }
}
public class RepositoryEdge
{
public Repository Node { get; set; }
}
public class RepositoryConnection
{
public IList<RepositoryEdge> Edges { get; set; }
}
public class RepositoryQueryResult
{
public RepositoryConnection Xamarin { get; set; }
}
This might look like a lot, but really it's just modeled to match what GitHub returns from their search results. This may vary depending on the API you're consuming (or designing), but this is how GitHub modeled things. There's more data available in the responses, of course, but this is the bare minimum we'll need to display our list of repository results.
Create The App
Now let's set up a basic UI for the app. First, let's define a ContentPage
to handle displaying the search results:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="GraphDemo.SearchResults">
<ContentPage.Content>
<ListView ItemsSource="{Binding Edges}" Margin="15,30,15,0">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Node.Name}" Detail="{Binding Node.Url}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>
This is just boilerplate Xamarin.Forms code for displaying a list of results, where we include the repository's name and URL as the data for each list item.
Next, define the main page for the application as a TabbedPage
(you'll see why shortly):
<?xml version="1.0" encoding="utf-8"?>
<TabbedPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:GraphDemo"
x:Class="GraphDemo.GraphDemoPage">
<local:SearchResults Title="Xamarin" BindingContext="{Binding Xamarin}" />
</TabbedPage>
That's the entire UI we need, so now we can start hooking up the data.
Connect The Data
Again for the sake of keeping things simple, we'll wire things up in the code-behind for our TabbedPage
, but in a real application I'd recommend moving things into some sort of view model layer. Let's start by writing the GraphQL query:
private const string RepositoryQuery =
@"query {
xamarin:search(type:REPOSITORY, query:""xamarin"", first:25) {
edges {
node {
...on Repository {
name
url
}
}
}
}
}";
As you can see, GraphQL allows you to specify precisely which values you would like to bring back over the wire, and allows you to dig as deep into the graph as you'd like. This can be extremely useful for mobile apps in allowing you to limit how many requests you need to make to the server, and making sure those requests are no larger than they need to be. In this case we only pull back each repository's name and URL.
Now let's execute this query and bind to it when the page appears:
private readonly GraphClient _client = new GraphClient();
protected override void OnAppearing()
{
base.OnAppearing();
_client.Query<RepositoryQueryResult>(RepositoryQuery).ContinueWith(result =>
Device.BeginInvokeOnMainThread(() =>
BindingContext = result.Result));
}
That's all that's needed, so let's fire up the app!
Without doing much work we've got a list of 25 repositories matching the search term of "xamarin".
Combining Queries
In addition to being able to specify precisely which fields we want to bring back, we can also run multiple queries at once as part of a single request. To demonstrate that, let's add a second search for "graphql" and display that in a second tab.
First, modify the query:
private const string RepositoryQuery =
@"
fragment repositoryFields on Repository{
name
url
}
query {
xamarin:search(type:REPOSITORY, query:""xamarin"", first:25) {
edges {
node {
...on Repository {
...repositoryFields
}
}
}
}
graphql:search(type:REPOSITORY, query:""graphql"", first:25) {
edges {
node {
...on Repository {
...repositoryFields
}
}
}
}
}";
This is almost the same as the previous queries, with a couple additions. First, we created a fragment
for the repository fields we want so that we could share that across multiple queries. We could have alternatively specified the fields individually in each query, but fragments can be nice for defining reusable pieces for your queries. Second, there is now a second search named graphql
that requests that search term in addition to the existing xamarin
one.
Next we need to add a field to the RepositoryQueryResult
object:
public RepositoryConnection GraphQL { get; set; }
With that in place, add a second tab to the TabbedPage
:
<local:SearchResults Title="GraphQL" BindingContext="{Binding GraphQL}" />
That's everything we need to add this second search. When the app runs it will still execute a single HTTP request to GitHub, but this time it will return the results for both searches in the payload for that request:
Summary
This example only begins to scratch the surface of what you can do with GraphQL, but hopefully it helps get you thinking about the types of opportunities this approach opens up. Without making any changes to your API, you can tweak things on the client-side to make precisely the requests it needs to get just the data it needs. There's a lot more to GraphQL than this, so I highly recommend checking out Facebook's documentation for a good introduction to the subject, and I'll try to follow this up with some more examples in the future as well.