Building a Who's that Pokémon game for Android with .NET MAUI and Blazor Hybrid
Tags: csharp, dotnet, blazor, mobile
Published: 2023-07-12 11:25:19

## Introduction

Blazor was initially developed to build interactive web applications using C#, but can now be used in many platforms when used together with .NET MAUI.

In this post, I'll show how to use Blazor Hybrid to develop a Who's that Pokémon game for Android.

![Who's that Pokémon game](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/demo.gif)

## What is MAUI?

MAUI (Multi-platform App UI) is a cross-platform framework to create native mobile apps and desktop apps with .NET.

Using MAUI, we can create apps that run on multiple platforms (Windows, MacOS, Android and iOS) with the same codebase (and some specific code for each platform, if necessary).

## What is Blazor Hybrid?

Blazor Hybrid allows us to run Blazor components natively in mobile and desktop apps using a Web View with full access to the devices capabilities. There is no need for a server to render the components and WebAssembly isn't involved.

## Installing MAUI

MAUI doesn't come with Visual Studio or the .NET SDK by default. We need to install its workload manually using the `dotnet workload install` command:

```bash
dotnet workload install maui
```

> ℹ️ The installation process may take some time to complete.

After installing, the MAUI templates will show in Visual Studio. We'll use the **.NET MAUI Blazor Hybrid App**.

![.NET MAUI Blazor Hybrid App Template](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/blazor-hybrid.png)

## The components

The app is composed mainly of the four components marked in the image and described below.

![App components](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/Components.png)

- <span style="color:#00af00">**WhosThatPokemon**</span>: Renders the other components + logic to select a random pokemon and check if Pokémon selected by the user is correct;
- <span style="color:#ffba00">**WhosThatPokemonData**</span>: Renders the Pokémon image and name;
- <span style="color:#0096ff">**PokemonSelector**</span>: Component to search and select a Pokémon;
- <span style="color:#ff0000">**WhosThatPokemonResult**</span>: Show if the selected Pokémon was the right one.

## Creating a service to store the Pokémon information

First, I created a service to get the list of Pokémon from a public API (PokéAPI) using [Refit](https://github.com/reactiveui/refit) and cache it locally to be used by any component. This information is static, so there is no need to go to the API every time.

```csharp
using BlazorHybridApp.Components.Domain;
using BlazorHybridApp.Components.Interfaces.APIEndpoints;
using Microsoft.Extensions.Configuration;
using Refit;

namespace BlazorHybridApp.Components.Services;

public class PokemonDataService
{
    private List<PokemonInfo>? _pokemonList;
    private readonly IPokeApi _pokeApi;

    public PokemonDataService(IConfiguration configuration)
    {
        _pokeApi ??= RestService.For<IPokeApi>(configuration!["PokeApiBaseUrl"]!);
    }

    public async Task<List<PokemonInfo>> GetPokemonListAsync()
    {
        if (_pokemonList == null)
        {
            var species = await _pokeApi.GetAllPokemonSpecies();

            _pokemonList = species.Results
                .Select(pokemonSpecie => new PokemonInfo()
                {
                    Id = GetPokemonIdFromUrl(pokemonSpecie.Url),
                    Name = pokemonSpecie.Name.ToUpperInvariant()
                })
                .ToList();
        }

        return _pokemonList;
    }

    public async Task<PokemonInfo> GetPokemonByIdAsync(int id)
    {
        _pokemonList ??= await GetPokemonListAsync();

        return _pokemonList.Single(a => a.Id == id);
    }

    public async Task<int> GetPokemonCountAsync()
    {
        _pokemonList ??= await GetPokemonListAsync();

        return _pokemonList.Count;
    }

    private static int GetPokemonIdFromUrl(string url)
    {
        return int.Parse(new Uri(url).Segments.LastOrDefault()?.Trim('/') ?? "0");
    }
}
```

And added it as a singleton in the dependency injection container:

```csharp
builder.Services.AddSingleton<PokemonDataService>();
```

## Installing MudBlazor components

The app uses the MudBlazor library, a free open-source component library for Blazor build entirely with C# (not Javascript wrapped components). The list of components and how to use them can be seen [here](https://mudblazor.com/docs/overview).

Instructions on how to install may change for newer versions, so I'll link the [official docs](https://github.com/MudBlazor/MudBlazor/#quick-installation-guide).

## Configuring the app layout

The Blazor Hybrid app template comes with a MAUI app rendering an **Index.razor** component page.

Let's edit it to render the **WhosThatPokemon** component we'll create:

```html
@page "/"

<WhosThatPokemon></WhosThatPokemon>
```

Then, let's modify the **MainLayout.razor** removing the menu:

```html
@inherits LayoutComponentBase

<MudThemeProvider />

<div class="page">
    <main>
        @Body
    </main>
</div>
```

And add some customizations in the **app.css**:

```css
body {
    overflow-y: scroll;
    background-color: red;
}
...
```

## Creating the components

Now, let's create the components in a **Components** folder.

### WhosThatPokemonData

![WhosThatPokemonData Component](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/Components1.png)

#### WhosThatPokemonData.razor

Here we check the `Show` variable to decide if we should show the pokemon image and name.

```html
<div class="container">
    @if (!Show)
    {
        <div>
            <img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/@(PokemonId).png"
                 class="pokemon-image pokemon-image-hidden" />
        </div>
        <div class="pokemon-name">
            <p>?????</p>
        </div>
    }
    else
    {
        <div>
            <img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/@(PokemonId).png"
                 class="pokemon-image" />
        </div>
        <div class="pokemon-name">
            <p>@PokemonName (#@PokemonId)</p>
        </div>
    }
</div>
```

#### WhosThatPokemonData.razor.css

Here we have the "magic" to hide the Pokémon image using `filter: brightness(0)`.

```css
.container {
    font-family: Verdana;
    color: black;
}

.pokemon-image {
    width: 200px;
    height: 200px;
    filter: drop-shadow(2px 4px 6px black);
    margin: auto;
    display: block;
}

.pokemon-image-hidden {
    filter: brightness(0) drop-shadow(2px 4px 6px black)
}

.pokemon-name {
    font-size: large;
}
```

#### WhosThatPokemonData.razor.cs

No logic, just parameters used to render the component.

```csharp
using Microsoft.AspNetCore.Components;

namespace BlazorHybridApp.Components;

public partial class WhosThatPokemonData
{
    [Parameter]
    public int PokemonId { get; init; }
    [Parameter]
    public required string PokemonName { get; init; }
    [Parameter]
    public bool Show { get; init; }
}
```

### PokemonSelector

![PokemonSelector Component](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/Components2.png)

#### PokemonSelector.razor

I used the `MudAutocomplete` component from MudBlazor. The value selected is binded to the `SelectedPokemon` property.

```html
@using BlazorHybridApp.Components.Domain;

<div class="container">
    @if (_pokemon == null)
    {
        <MudSkeleton SkeletonType="SkeletonType.Rectangle" Width="100%" Height="30px" />
    }
    else
    {
        <MudAutocomplete @ref="_selectedPokemon"
                         T="PokemonInfo" @bind-Value="SelectedPokemon"
                         ResetValueOnEmptyText="true"
                         Label="Who's that pokémon?"
                         ToStringFunc="@(e => e?.Name)"
                         SearchFunc="@Search" />
    }
</div>
```

#### PokemonSelector.razor.cs

```csharp
using BlazorHybridApp.Components.Domain;
using BlazorHybridApp.Components.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;

namespace BlazorHybridApp.Components;

public partial class PokemonSelector
{
    [Inject]
    private PokemonDataService PokemonDataService { get; init; } = default!;

    private MudAutocomplete<PokemonInfo> _selectedPokemon = default!;
    private List<PokemonInfo>? _pokemon;

    public PokemonInfo? SelectedPokemon { get; set; }

    public void Clear()
    {
        _selectedPokemon.Clear();
    }

    protected override async Task OnInitializedAsync()
    {
        await GetPokemonListAsync();
    }

    private Task<IEnumerable<PokemonInfo>?> Search(string value)
    {
        // if text is null or empty, show complete list
        if (string.IsNullOrEmpty(value))
            return Task.FromResult(_pokemon?.Take(10));

        return Task.FromResult(_pokemon?
            .Where(pokemon => pokemon.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase))
            .Take(10));
    }

    protected async Task GetPokemonListAsync()
    {
        _pokemon = await PokemonDataService.GetPokemonListAsync();
    }
}
```

### WhosThatPokemonResult

![WhosThatPokemonResult Component](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/Components3.png)

#### WhosThatPokemonResult.razor

Nothing fancy here, this component just shows the result after selecting a Pokémon.

```html
<div class="container">
    @if (Show)
    {
        @if (Correct)
        {
            <div class="game-result-correct">
                That's right!
            </div>
        }
        else
        {
            <div class="game-result-wrong">
                Sorry, it's not
            </div>
        }
    }
</div>
```

#### WhosThatPokemonResult.razor.cs

```csharp
using Microsoft.AspNetCore.Components;

namespace BlazorHybridApp.Components;

public partial class WhosThatPokemonResult
{
    [Parameter]
    public bool Show { get; init; }
    [Parameter]
    public bool Correct { get; init; }
}
```

### WhosThatPokemon

![WhosThatPokemon Component](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/Components4.png)

#### WhosThatPokemon.razor

This component uses all the other components, orchestrating their parameter values and events:

```html
<div class="container">
    @if (_loading)
    {
        <Loader></Loader>
    }
    else
    {
        <div class="title">
            Who's that Pokémon?!
        </div>

        <WhosThatPokemonData PokemonId="_pokemonData.Id" PokemonName="@_pokemonData.Name" Show="_showResult"></WhosThatPokemonData>

        <PokemonSelector @ref="_pokemonSelector"></PokemonSelector>

        <MudButton @onclick="Confirm" class="confirm-button"
                   Variant="Variant.Filled">Confirm</MudButton>

        <WhosThatPokemonResult Correct="_correct" Show="_showResult"></WhosThatPokemonResult>

        <MudButton class="confirm-button" Variant="Variant.Filled" @onclick="ShowNextAsync">Show Next</MudButton>
    }
</div>
```

#### WhosThatPokemon.razor.css

Here we have to use `::deep` to set the css properties for the `MudButton`. This is necessary because Blazor CSS isolation only works for elements directly below its component hierarchy in the DOM.

```css
.container {
    border-radius: 3px;
    background-color: rgba(255, 255, 255, 0.9);
    margin: 15px;
    padding: 10px;
    text-align: center;
    max-width: 280px;
    margin-left: auto;
    margin-right: auto;
    height: calc(100vh - 30px);
}

::deep button.confirm-button {
    margin-top: 10px;
    background-color: red;
    color: white;
    margin: 5px;
}

.title {
    font-size: large;
}
```

#### WhosThatPokemon.razor.cs

In the `OnInitializedAsync` and `ShowNextAsync` method, we select a random Pokémon that is passed to the other components.

In the `Confirm` method, we check if the Pokémon selected is the right one and pass the result to the `WhosThatPokemonResult` component.

```csharp
using BlazorHybridApp.Components.Domain;
using BlazorHybridApp.Components.Services;
using Microsoft.AspNetCore.Components;

namespace BlazorHybridApp.Components;

public partial class WhosThatPokemon
{
    [Inject]
    private PokemonDataService PokemonDataService { get; init; } = default!;

    private PokemonInfo _pokemonData = default!;
    private PokemonSelector _pokemonSelector = default!;
    private bool _correct;
    private bool _showResult;
    private bool _loading;

    protected override async Task OnInitializedAsync()
    {
        _pokemonData = await GetRandomPokemon();
    }

    private async Task<PokemonInfo> GetRandomPokemon()
    {
        try
        {
            _loading = true;

            var pokemonCount = await PokemonDataService.GetPokemonCountAsync();

            Random r = new Random();
            var pokemonId = r.Next(1, pokemonCount);

            return await PokemonDataService.GetPokemonByIdAsync(pokemonId);
        }
        finally
        {
            _loading = false;
        }
    }

    protected void Confirm()
    {
        _correct = _pokemonSelector?.SelectedPokemon?.Id == _pokemonData?.Id;

        _showResult = true;
    }

    protected async Task ShowNextAsync()
    {
        _showResult = false;
        _pokemonSelector.Clear();

        _pokemonData = await GetRandomPokemon();
    }    
}
```

## Other customizations

### Status bar color

To change the status bar color for the app, we have to create a `style` tag and define the `android:statusBarColor` and `android:navigationBarColor` elements in the `Platforms/Android/Resources/values/colors.xml` file:

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<color name="colorPrimary">#FF0000</color>
	<color name="colorPrimaryDark">#DD0000</color>
	<color name="colorAccent">#BB0000</color>

	<style name="MainTheme" parent="@style/Maui.SplashTheme">
		<item name="android:statusBarColor">#FF0000</item>
		<item name="android:navigationBarColor">#FF0000</item>
	</style>
</resources>
```

Then, set the `Theme` property of the `Activity` attribute to the style name (`@style/MainTheme`, in this example) in the `MainActivity.cs` file:

```csharp
[Activity(Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}
```

## Running the app

Since MAUI applications run on multiple platforms, we can run the app on Windows to debug or do quicker tests. Just select `Windows Machine` from the Run menu:

![Running on Windows](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/running.gif)

> 💡 **I recommend using an Android physical device** when running/debugging on Android, as it is many times faster than running on an emulator. Just connect the device to the computer using an USB cable and the ADB drivers installed, and it will show up in the Run menu:
>
> ![Run on Android Device](https://blog.genezini.com/p/building-a-whos-that-pokemon-game-for-android-with-net-maui-and-blazor-hybrid/images/android-device.png)

## Full source code

[GitHub repository](https://github.com/dgenezini/WhosThatPokemonGameApp)

## Related

{% embed https://blog.genezini.com/p/real-time-charts-with-blazor-signalr-and-apexcharts/ %}

## Liked this post?

I post extra content in my personal blog. [Click here](https://blog.genezini.com/) to see.

## Follow me

- [On Feedly](https://feedly.com/i/subscription/feed/https://blog.genezini.com/index.xml)
- [RSS](https://blog.genezini.com/index.xml)
- [Linkedin](https://www.linkedin.com/in/danielgenezini/)
- [Twitter](https://twitter.com/GeneziniDaniel)

## References and Links

- [What's MAUI](https://learn.microsoft.com/en-us/dotnet/maui/what-is-maui)
- [Blazor Hybrid](https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/?view=aspnetcore-7.0)
- [MudBlazor docs](https://mudblazor.com/docs/overview)
- [Change a .NET MAUI app icon](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/images/app-icons?tabs=android)
- [Add a splash screen to a .NET MAUI app project](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/images/splashscreen?tabs=android)