Introdução
O Blazor foi inicialmente desenvolvido para criar aplicações web interativas usando C#, mas agora pode ser usado em muitas plataformas quando combinado com o .NET MAUI.
Neste post, mostrarei como usar o Blazor Hybrid para desenvolver um jogo Quem é esse Pokémon para Android.

O que é MAUI?
O MAUI (Multi-platform App UI) é um framework multiplataforma para criar apps nativos para mobile e desktop com .NET.
Usando o MAUI, podemos criar apps que rodam em múltiplas plataformas (Windows, MacOS, Android e iOS) com a mesma base de código (e algum código específico para cada plataforma, se necessário).
O que é Blazor Hybrid?
O Blazor Hybrid permite executar componentes Blazor nativamente em apps para mobile e desktop usando uma Web View com acesso total às capacidades do dispositivo. Não é necessário um servidor para renderizar os componentes e o WebAssembly não é envolvido.
Instalando o MAUI
O MAUI não vem com o Visual Studio ou o .NET SDK por padrão. Precisamos instalar sua workload manualmente usando o comando dotnet workload install:
dotnet workload install maui
ℹ️
O processo de instalação pode levar algum tempo para completar.
Após a instalação, os templates do MAUI aparecerão no Visual Studio. Usaremos o .NET MAUI Blazor Hybrid App.

Os componentes
O app é composto principalmente pelos quatro componentes marcados na imagem e descritos abaixo.

- WhosThatPokemon: Renderiza os outros componentes + lógica para selecionar um Pokémon aleatório e verificar se o Pokémon selecionado pelo usuário está correto;
- WhosThatPokemonData: Renderiza a imagem e o nome do Pokémon;
- PokemonSelector: Componente para buscar e selecionar um Pokémon;
- WhosThatPokemonResult: Mostra se o Pokémon selecionado era o correto.
Primeiro, criei um serviço para obter a lista de Pokémon de uma API pública (PokéAPI) usando o Refit e armazená-la em cache localmente para ser usada por qualquer componente. Essas informações são estáticas, então não há necessidade de acessar a API toda vez.
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");
}
}
|
E adicionei-o como singleton no container de injeção de dependência:
builder.Services.AddSingleton<PokemonDataService>();
Instalando componentes MudBlazor
O app usa a biblioteca MudBlazor, uma biblioteca de componentes open-source gratuita para Blazor construída inteiramente com C# (não são componentes wrappers de JavaScript). A lista de componentes e como usá-los pode ser vista aqui.
As instruções de instalação podem mudar para versões mais recentes, então linkarei a documentação oficial.
Configurando o layout do app
O template de app Blazor Hybrid vem com um app MAUI renderizando uma página de componente Index.razor.
Vamos editá-lo para renderizar o componente WhosThatPokemon que criaremos:
1
2
3
| @page "/"
<WhosThatPokemon></WhosThatPokemon>
|
Então, vamos modificar o MainLayout.razor removendo o menu:
1
2
3
4
5
6
7
8
9
| @inherits LayoutComponentBase
<MudThemeProvider />
<div class="page">
<main>
@Body
</main>
</div>
|
E adicionar algumas personalizações no app.css:
1
2
3
4
5
| body {
overflow-y: scroll;
background-color: red;
}
...
|
Criando os componentes
Agora, vamos criar os componentes em uma pasta Components.
WhosThatPokemonData

WhosThatPokemonData.razor
Aqui verificamos a variável Show para decidir se devemos mostrar a imagem e o nome do Pokémon.
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
Aqui temos a “mágica” para esconder a imagem do Pokémon usando 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
Sem lógica, apenas parâmetros usados para renderizar o componente.
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

PokemonSelector.razor
Usei o componente MudAutocomplete do MudBlazor. O valor selecionado é vinculado à propriedade SelectedPokemon.
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
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)
{
// se o texto for nulo ou vazio, mostra a lista completa
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.razor
Nada de especial aqui, este componente apenas mostra o resultado após selecionar um 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">
Correto!
</div>
}
else
{
<div class="game-result-wrong">
Desculpe, não é
</div>
}
}
</div>
|
WhosThatPokemonResult.razor.cs
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

WhosThatPokemon.razor
Este componente usa todos os outros componentes, orquestrando seus valores de parâmetros e eventos:
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">
Quem é esse 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">Confirmar</MudButton>
<WhosThatPokemonResult Correct="_correct" Show="_showResult"></WhosThatPokemonResult>
<MudButton class="confirm-button" Variant="Variant.Filled" @onclick="ShowNextAsync">Próximo</MudButton>
}
</div>
|
WhosThatPokemon.razor.css
Aqui temos que usar ::deep para definir as propriedades CSS para o MudButton. Isso é necessário porque o isolamento de CSS do Blazor só funciona para elementos diretamente abaixo da hierarquia de componentes no 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
Nos métodos OnInitializedAsync e ShowNextAsync, selecionamos um Pokémon aleatório que é passado para os outros componentes.
No método Confirm, verificamos se o Pokémon selecionado é o correto e passamos o resultado para o componente WhosThatPokemonResult.
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();
}
}
|
Outras personalizações
Cor da barra de status
Para alterar a cor da barra de status do app, precisamos criar uma tag style e definir os elementos android:statusBarColor e android:navigationBarColor no arquivo Platforms/Android/Resources/values/colors.xml:
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>
|
Então, defina a propriedade Theme do atributo Activity para o nome do estilo (@style/MainTheme, neste exemplo) no arquivo MainActivity.cs:
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
{
}
|
Executando o app
Como as aplicações MAUI rodam em múltiplas plataformas, podemos executar o app no Windows para depurar ou fazer testes mais rápidos. Basta selecionar Windows Machine no menu Executar:

💡
Recomendo usar um dispositivo Android físico ao executar/depurar no Android, pois é muito mais rápido que rodar em um emulador. Basta conectar o dispositivo ao computador usando um cabo USB e os drivers ADB instalados, e ele aparecerá no menu Executar:

Código fonte completo
Repositório GitHub
Referências e Links