Este es un proyecto desarrollado para plataformas móviles y en este blog muestro de cómo consumir una api(Application Programming Interface) y poder utilizar la información. Implementando Repository Pattern y Model View ViewModel como buenas prácticas y tener un código limpio.
Aplicacion:
Proyecto desarrollado con .Net C# MAUI 8.0
Especificamos el PageContent con el que inicializaría la aplicación al ejecutarse:
using AppDragonBallZ.View;
namespace AppDragonBallZ
{
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new Characters());
}
}
}
View(Vista): Creamos un nuevo archivo ContentPage con el nombre 'Characters' que será la vista para mostrar los personajes.
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AppDragonBallZ.View.Characters"
NavigationPage.HasNavigationBar="False"
Title="Characters">
BackgroundColor="White">
>
Margin="5,5">
Source="dragonballsuper.png"/>
Margin="10">
Brush="Black"
Opacity="0.9"
Offset="5,5"
Radius="10" />
Orientation="Horizontal"
HorizontalScrollBarVisibility="Never">
HorizontalOptions="Center"
Spacing="5"
Margin="10,10">
Text="{Binding Pagination.Meta.TotalItems, StringFormat='Total personajes: {0}'}"
FontAttributes="Bold"
BackgroundColor="#ba0f42"
CornerRadius="20"
HeightRequest="35"
Padding="15,0,15,0" />
Text="{Binding Pagination.Meta.TotalPages, StringFormat='Total página: {0}'}"
FontAttributes="Bold"
BackgroundColor="#30915f"
CornerRadius="20"
HeightRequest="35"
Padding="15,0,15,0" />
Text="{Binding Pagination.Meta.CurrentPage, StringFormat='Página actual: {0}'}"
FontAttributes="Bold"
BackgroundColor="#2c74ab"
CornerRadius="20"
HeightRequest="35"
Padding="15,0,15,0" />
Text="{Binding Pagination.Meta.ItemsPerPage, StringFormat='Cantidad personajes por página: {0}'}"
FontAttributes="Bold"
BackgroundColor="#2c74ab"
CornerRadius="20"
HeightRequest="35"
Padding="15,0,15,0" />
ItemsSource="{Binding Pagination.Characters}"
x:Name="characters">
Orientation="Vertical"
Span="2" />
Padding="20,0,20,0"
RowDefinitions="*,*"
Margin="0,10">
Grid.Row="0">
HeightRequest="280"
Stroke="#87878a"
StrokeShape="RoundRectangle 20,20,20,20"
StrokeThickness="0">
StartPoint="0,0"
EndPoint="1,1">
Color="#5da1a3"
Offset="0.0" />
Color="#ffffff"
Offset="0.5" />
Color="#ffffff"
Offset="1.0" />
Brush="Black"
Opacity="0.5"
Offset="5,5"
Radius="10" />
Aspect="AspectFit"
HeightRequest="300"
Source="{Binding Image}" />
Command="{Binding Path=BindingContext.SeleccionarPersonajeCommand, Source={x:Reference characters}}"
CommandParameter="{Binding Id}" />
Grid.Row="1"
Margin="0,0,0,0">
HorizontalOptions="Center"
TextColor="Black"
Text="{Binding Name}"
FontAttributes="Bold"
FontSize="15"
VerticalOptions="End" />
HeightRequest="1"
BackgroundColor="#d1d1d1"
Margin="0,25,0,0" />
HorizontalOptions="Center"
Margin="0,10"
BindableLayout.ItemsSource="{Binding Pagination}"
x:Name="paginationCharacters">
Text="Anterior"
IsEnabled="{Binding BtnAnterior}"
FontAttributes="Bold"
TextColor="White"
BackgroundColor="#262626"
CornerRadius="20"
HeightRequest="35"
Margin="0,0,5,0"
Padding="15,0,15,0">
Command="{Binding Path=BindingContext.AnteriorPaginaCommand, Source={x:Reference paginationCharacters}}"
CommandParameter="{Binding Pagination}" />
Text="Siguiente"
IsEnabled="{Binding BtnSiguiente}"
FontAttributes="Bold"
TextColor="White"
BackgroundColor="#262626"
CornerRadius="20"
HeightRequest="35"
Margin="5,0,0,0"
Padding="15,0,15,0">
Command="{Binding Path=BindingContext.SiguientePaginaCommand, Source={x:Reference paginationCharacters}}"
CommandParameter="{Binding Pagination}" />
Métodos de Interfaz: ICharactersRepository. En esta interfáz creamos 3 métodos y de los cuáles solo utilizaremos 2. Una interfaz define un contrato o conjunto de reglas que las clases que lo implementen deben cumplirlos.
using AppDragonBallZ.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppDragonBallZ.RepositoryPattern.IRepositoryPattern
{
public interface ICharactersRepository
{
Task<Respuesta> Characters(string uriApi);
Task<Respuesta> GetCharacterById(long characterId);
Task<Respuesta> GetCharacterByName(string name);
}
}
Repository Pattern: CharactersRepository, implementa los métodos de la Interfaz: ICharactersRepository. Este contiene únicamente la lógica para consumir la api y poder deserealizarlo y retornar los datos para luego ser utilizado de acuerdo a la información que necesitaríamos visualizar en vista de la aplicación.
using AppDragonBallZ.Model;
using AppDragonBallZ.Model.DBZmodel;
using AppDragonBallZ.RepositoryPattern.IRepositoryPattern;
using AppDragonBallZ.Generico;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppDragonBallZ.RepositoryPattern
{
public class CharactersRepository : ICharactersRepository
{
public async Task<Respuesta> Characters(string uriApi)
{
Respuesta respuesta = new();
try
{
string urlApi = uriApi;
var httpClient = new HttpClient();
Pagination pagination = new();
var response = await httpClient.GetAsync(urlApi);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Pagination>(content);
pagination = result;
}
respuesta.Data = pagination;
respuesta.Resultado = true;
respuesta.Mensaje = "Consulta exitosa";
}
catch (Exception ex)
{
respuesta.Resultado = false;
respuesta.Mensaje = "Error en método: Characters - > " + ex.Message.ToString();
}
return respuesta;
}
public async Task<Respuesta> GetCharacterById(long characterId)
{
Respuesta respuesta = new();
try
{
string urlApi = $"{Utilidades.UrlApiDBZ}/{characterId}";
var httpClient = new HttpClient();
Character character = new();
var response = await httpClient.GetAsync(urlApi);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Character>(content);
character = result;
}
respuesta.Data = character;
respuesta.Resultado = true;
respuesta.Mensaje = "Consulta exitosa";
}
catch (Exception ex)
{
respuesta.Resultado = false;
respuesta.Mensaje = "Error en método: GetCharacterById - > " + ex.Message.ToString();
}
return respuesta;
}
public Task<Respuesta> GetCharacterByName(string name)
{
throw new NotImplementedException();
}
}
}
ViewModel(Modelo de Vista): CharactersViewModel.
Es una clase que encapsula la lógica de la interfaz de usuario, separándola de la vista (la interfaz de usuario XAML) y del modelo (los datos y la lógica de negocio). Esto facilita el desarrollo de aplicaciones más organizadas, testables y mantenibles.
using AppDragonBallZ.RepositoryPattern;
using AppDragonBallZ.Generico;
using AppDragonBallZ.Model;
using AppDragonBallZ.Model.DBZmodel;
using AppDragonBallZ.RepositoryPattern.IRepositoryPattern;
using AppDragonBallZ.View;
using System.Windows.Input;
namespace AppDragonBallZ.ViewModel
{
public class CharactersViewModel : BaseViewModel
{
#region VARIABLES
private readonly ICharactersRepository charactersRepository = new CharactersRepository();
Pagination _pagination = new();
public bool IsRefreshing { get; set; }
int _page =1;
int _limit=10;
bool _btnAnterior;
bool _btnSiguiente;
#endregion
#region OBJETOS
public Pagination Pagination
{
get { return _pagination; }
set { SetValue(ref _pagination, value); }
}
public bool BtnAnterior
{
get { return _btnAnterior; }
set { SetValue(ref _btnAnterior, value); }
}
public bool BtnSiguiente
{
get { return _btnSiguiente; }
set { SetValue(ref _btnSiguiente, value); }
}
public int Page
{
get { return _page; }
set { SetValue(ref _page, value); }
}
public int Limit
{
get { return _limit; }
set { SetValue(ref _limit, value); }
}
#endregion
#region CONSTRUCTOR
public CharactersViewModel(INavigation navigation)
{
Navigation = navigation;
BtnSiguiente = true;
ObtenerPersonajes();
}
#endregion
#region PROCESOS
public async void ObtenerPersonajes()
{
try
{
Respuesta respuesta = await charactersRepository.Characters($"{Utilidades.UrlApiDBZ}?page={Page}&limit={Limit}");
if (respuesta.Resultado!=true)
{
await DisplayAlert("Error", respuesta.Mensaje, "Ok");
}
Pagination = (Pagination)respuesta.Data;
}
catch (Exception ex)
{
await DisplayAlert("Error en método: ObtenerPersonajes -> ", ex.Message.ToString(), "Ok");
}
}
public async void SiguientePagina(Pagination pagination)
{
try
{
Respuesta respuesta = await charactersRepository.Characters(pagination.Links.Next.ToString());
if (respuesta.Resultado != true)
{
await DisplayAlert("Error", respuesta.Mensaje, "Ok");
}
Pagination = (Pagination)respuesta.Data;
ValidarEstadoBoton("Siguiente");
}
catch (Exception ex)
{
await DisplayAlert("Error en método: SiguientePagina -> ", ex.Message.ToString(), "Ok");
}
}
public async void AnteriorPagina(Pagination pagination)
{
try
{
Respuesta respuesta = await charactersRepository.Characters(pagination.Links.Previous.ToString());
if (respuesta.Resultado != true)
{
await DisplayAlert("Error", respuesta.Mensaje, "Ok");
}
Pagination = (Pagination)respuesta.Data;
ValidarEstadoBoton("Anterior");
}
catch (Exception ex)
{
await DisplayAlert("Error en método: AnteriorPagina -> ", ex.Message.ToString(), "Ok");
}
}
public async void ValidarEstadoBoton(string boton)
{
try
{
if (boton.Equals("Siguiente"))
{
BtnSiguiente = Pagination.Links.Next == null ? false : true;
BtnAnterior = Pagination.Links.Previous == null ? false : true;
}
else if (boton.Equals("Anterior"))
{
BtnAnterior = Pagination.Links.Previous == null ? false : true;
BtnSiguiente = Pagination.Links.Next == null ? false : true;
}
}
catch (Exception ex)
{
await DisplayAlert("Error en método: ValidarEstadoBoton -> ", ex.Message.ToString(), "Ok");
}
}
public async void SeleccionarPersonaje(long characterId)
{
try
{
await Navigation.PushAsync(new CharacterDetail(characterId));
}
catch (Exception ex)
{
await DisplayAlert("Error en método: SeleccionarPersonaje -> ", ex.Message.ToString(), "Ok");
}
}
#endregion
#region COMANDOS
public ICommand SiguientePaginaCommand => new Command<Pagination>(SiguientePagina);
public ICommand AnteriorPaginaCommand => new Command<Pagination>(AnteriorPagina);
public ICommand SeleccionarPersonajeCommand => new Command<long>(SeleccionarPersonaje);
#endregion
}
}
Model(Modelo): En estas clases están todas las propiedades de acuerdo a la información de la API obtenida; estos nos servirá para setear los datos despues de ser deserealizados.
BindingContext:, creamos una instancia hacia CharactersViewModel permitiendo así que los elementos de la Interfaz de usuario se vinculen a sus propiedades, métodos, comandos etc.
using AppDragonBallZ.ViewModel;
namespace AppDragonBallZ.View;
public partial class Characters : ContentPage
{
public Characters()
{
InitializeComponent();
BindingContext = new CharactersViewModel(Navigation);
}
}
Beneficion de utilizar este patrón:
- Separación de responsabilidades: La vista no necesita conocer la lógica del modelo de vista.
- Actualización automática: Cuando una propiedad cambia en el modelo de vista, la interfaz de usuario se actualiza automáticamente.
- Facilita pruebas: Permite probar la lógica de negocio sin depender de la interfaz gráfica.
Nota:
Descargar el nugget: Newtonsoft.Json. Esta biblioteca .NET permite serializar (convertir objetos .NET a JSON o viceversa).
Código completo del proyecto:
[https://github.com/12AbelCabreraMiranda/AppDragonBallZ]
Referencias:
[https://learn.microsoft.com/es-es/dotnet/architecture/maui/mvvm]
[https://web.dragonball-api.com/documentation]