Consuming an API and Populating a List with the Results in MAUI MVVM .NET 9 [GamesCatalog] - Part 4

Previous part: Reusable Components and Styles in MAUI MVVM .NET 9 [GamesCatalog] - Part 3

Step 1. Let's create an account and register the application.

Create an account

https://api-docs.igdb.com/#account-creation

This way, we have the keys to generate a temporary token.

Temporary token

https://api-docs.igdb.com/#account-creation

Postman Print Token

The token is valid for 2 months, so for now, we won't need a function to update it constantly.

Step 2. Let's create the logic layers, the models, and the API access layer.

API access layer

Step 3. Let's create a model to receive API responses in a general way.

API responses

Code

public class ApiResp
{
    public bool Success { get; set; }

    public string? Content { get; set; }

    public ErrorTypes? Error { get; set; }

    public bool TryRefreshToken { get; set; }
}

public enum ErrorTypes
{
    TokenExpired = 0,
    Unknown = 1,
    ServerUnavaliable = 2,
    WrongEmailOrPassword = 3,
    Unauthorized = 4,
    BodyContentNull = 5,
}

Step 4. Let's create the class that will store our private API keys. While we don't have a local database, we'll keep a fixed token.

Private API keys

Code

 public static class ApiKeys
  {
      public const string TOKENTEMP = <Token>;

      public const string CLIENTID = <clientId>;
  }

Step 5. Let's create the class that will access game data using our Client-ID and the token we generated in the POST request, which is currently limited to 20 records.

 Client-ID

Code

using Models.Resps;
using System.Net;
using System.Text;

namespace ApiRepo
{
    public static class IGDBGamesAPIRepo
    {
        public async static Task<ApiResp> Get(string search, int startIndex)
        {
            try
            {
                HttpClient httpClient = new();

                httpClient.DefaultRequestHeaders.Add("Client-ID", ApiKeys.CLIENTID);
                httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKeys.TOKENTEMP}");

                var bodyContent = new StringContent(
                    $"fields cover,cover.url,cover.image_id,first_release_date,name,platforms.abbreviation;" +
                    $"search \"{search}\"; limit 20; offset {startIndex};", 
                    Encoding.UTF8, 
                    "application/json"
                );

                HttpResponseMessage httpResponse = await httpClient.PostAsync(
                    "https://api.igdb.com/v4/games", 
                    bodyContent
                );

                return new ApiResp
                {
                    Success = httpResponse.IsSuccessStatusCode,
                    Error = httpResponse.StatusCode == HttpStatusCode.Unauthorized ? ErrorTypes.Unauthorized : null,
                    Content = await httpResponse.Content.ReadAsStringAsync()
                };
            }
            catch
            {
                throw;
            }
        }
    }
}

Step 6. Model related to the API response.

namespace Models.Resps
{
    public record IGDBGame
    {
        public string id { get; set; }

        public Cover cover { get; set; }

        public string name { get; set; }

        public string first_release_date { get; set; }

        public List<Platform> platforms { get; set; }
    }

    public record Cover
    {
        public string id { get; set; }

        public string url { get; set; }

        public string image_id { get; set; }
    }

    public record Platform
    {
        public string id { get; set; }

        public string abbreviation { get; set; }
    }
}

Step 7. Business class that deserializes the API response.

using ApiRepo;
using Newtonsoft.Json;

namespace Services.Games
{
    public static class IGDBGamesApiService
    {
        public async static Task<List<Models.Resps.IGDBGame>> Get(string search, int startIndex)
        {
            Models.Resps.ApiResp resp = await IGDBGamesAPIRepo.Get(search, startIndex);

            if ((resp is not null) && resp.Success && resp.Content is not null)
            {
                return JsonConvert.DeserializeObject<List<Models.Resps.IGDBGame>>(resp.Content)
                    ?? throw new Exception("Error getting games");
            }
            else throw new Exception("Error getting games");
        }
    }
}

Step 8. In the ViewModel, we will remove the mock list and include a variable to control the search.

ViewModel

Step 9. Let's create the function that will pass the result to the listview.

Step 9.1. This function passes the text entered in the field to the search, receives the result list and processes it for display on the screen.

private async Task LoadIGDBGamesList(int startIndex)
{
    List<IGDBGame> resp = await IGDBGamesApiService.Get(SearchText, startIndex);

    DateTime? releaseDate = null;

    foreach (var item in resp)
    {
        if (item is null) continue;

        releaseDate = item.first_release_date is not null
            ? DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(item.first_release_date)).UtcDateTime
            : null;

        ListGames.Add(new UIIGDBGame
        {
            Id = item.id,
            Name = item.name ?? "",
            ReleaseDate = releaseDate?.Date.ToString("MM/yyyy") ?? "",
            CoverUrl = item.cover?.id is not null 
                ? $"https://images.igdb.com/igdb/image/upload/t_cover_big/{item.cover?.image_id}.jpg" 
                : "",
            Platforms = item.platforms?.Count > 0 
                ? string.Join(", ", item.platforms.Select(p => p.abbreviation)) 
                : ""
        });
    }
}

Step 9.2. This function sets a time interval of 2.5 seconds to perform the search.

private async Task SearchGamesList()
{
    if (Searching || SearchText.Length < 3)
        return;

    Searching = true;

    while (Searching)
    {
        await Task.Delay(2500);

        if (ListGames.Count > 0)
            ListGames.Clear();

        try
        {
            await LoadIGDBGamesList(0);
        }
        catch (Exception)
        {
            throw;
        }

        Searching = false;
    }
}

Step 9.3. We will add it to SearchText and use it asynchronously.

  public string SearchText
  {
      get => searchText;
      set
      {
          if (searchText != value)
          {
              SetProperty(ref (searchText), value);
              _ = SearchGamesList();
          }
      }
  }

Step 10. We will modify the frontend to include a scroll in the ListView, defining the main layout as a grid, setting the sizes, and defining the ScrollView parameters.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Border
        Padding="10,0,10,5"
        HorizontalOptions="FillAndExpand"
        Style="{StaticResource BorderPrimary}">
        <components:BorderedEntry
            LabelText="Search"
            MaxLength="100"
            Text="{Binding SearchText}" />
    </Border>

    <ScrollView
        Grid.Row="1"
        Orientation="Vertical"
        VerticalOptions="FillAndExpand">
        <VerticalStackLayout>
            <Border
                Margin="5"
                Padding="5"
                BackgroundColor="Transparent"
                StrokeShape="RoundRectangle 10"
                VerticalOptions="FillAndExpand">
                <ListView
                    CachingStrategy="RecycleElement"
                    HasUnevenRows="True"
                    ItemsSource="{Binding ListGames}"
                    SelectionMode="None">
                    <ListView.ItemTemplate>
                        <DataTemplate x:DataType="model:UIIGDBGame">
                            <ViewCell>
                                <Border
                                    Margin="0,0,0,5"
                                    Padding="10"
                                    BackgroundColor="#101923"
                                    Stroke="#2B659B"
                                    StrokeShape="RoundRectangle 10">
                                    <Grid Padding="10">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="Auto" />
                                            <ColumnDefinition Width="*" />
                                        </Grid.ColumnDefinitions>

                                        <Image
                                            Grid.Column="0"
                                            Aspect="AspectFit"
                                            HeightRequest="100"
                                            Source="{Binding CoverUrl}"
                                            VerticalOptions="Center"
                                            WidthRequest="150" />

                                        <StackLayout Grid.Column="1" Margin="10,0,0,0">
                                            <Label
                                                FontSize="Large"
                                                LineBreakMode="TailTruncation"
                                                Text="{Binding Name}" />
                                            <Label
                                                FontAttributes="Italic"
                                                FontSize="Micro"
                                                Text="{Binding ReleaseDate, StringFormat='Release: {0:F0}'}"
                                                TextColor="#98BDD3" />
                                            <Label
                                                FontSize="Micro"
                                                Text="{Binding Platforms, StringFormat='Platforms: {0:F0}'}"
                                                TextColor="#98BDD3" />
                                        </StackLayout>
                                    </Grid>
                                </Border>
                            </ViewCell>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </Border>
        </VerticalStackLayout>
    </ScrollView>
</Grid>

Step 11. As a result, we have our screen with the functional search.

Functional search

In the next step, we will add dynamic pagination to the grid and handle loading the information.

Code on git: GamesCatalog git

Up Next
    Ebook Download
    View all
    Learn
    View all