Featured image of post Building a Who's that Pokémon game for Android with .NET MAUI and Blazor Hybrid
Picture by Daniel Genezini

Building a Who's that Pokémon game for Android with .NET MAUI and Blazor Hybrid

Using Blazor components to build an Android mobile app

Follow me

Introduction Link to this section

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

What is MAUI? Link to this section

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? Link to this section

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 Link to this section

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:

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

The components Link to this section

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

App components

  • WhosThatPokemon: Renders the other components + logic to select a random pokemon and check if Pokémon selected by the user is correct;
  • WhosThatPokemonData: Renders the Pokémon image and name;
  • PokemonSelector: Component to search and select a Pokémon;
  • WhosThatPokemonResult: Show if the selected Pokémon was the right one.

Creating a service to store the Pokémon information Link to this section

First, I created a service to get the list of Pokémon from a public API (PokéAPI) using 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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:

builder.Services.AddSingleton<PokemonDataService>();

Installing MudBlazor components Link to this section

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.

Instructions on how to install may change for newer versions, so I’ll link the official docs.

Configuring the app layout Link to this section

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:

1
2
3
@page "/"

<WhosThatPokemon></WhosThatPokemon>

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

1
2
3
4
5
6
7
8
9
@inherits LayoutComponentBase

<MudThemeProvider />

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

And add some customizations in the app.css:

1
2
3
4
5
body {
    overflow-y: scroll;
    background-color: red;
}
...

Creating the components Link to this section

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

WhosThatPokemonData Link to this section

WhosThatPokemonData Component

WhosThatPokemonData.razor Link to this section

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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 Link to this section

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.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 Link to this section

No logic, just parameters used to render the component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 Link to this section

PokemonSelector Component

PokemonSelector.razor Link to this section

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@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 Link to this section

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 Link to this section

WhosThatPokemonResult Component

WhosThatPokemonResult.razor Link to this section

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<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 Link to this section

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using Microsoft.AspNetCore.Components;

namespace BlazorHybridApp.Components;

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

WhosThatPokemon Link to this section

WhosThatPokemon Component

WhosThatPokemon.razor Link to this section

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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 Link to this section

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
.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 Link to this section

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 Link to this section

Status bar color Link to this section

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?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:

1
2
3
4
[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 Link to this section

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

💡 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

Full source code Link to this section

GitHub repository

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy