
Tutorial de como crear una aplicación con Xamarin Forms y MvvmCross
Tutorial de como crear una aplicación con Xamarin Forms y MvvmCross
En uno de los encuentros que hemos realizado en Xamarin Madrid, algunos miembros han pedido una charla de introducción de Xamarin Forms con MvvmCross. Hemos creado un taller explicando cómo realizar los primeros pasos con Xamarin Forms y MvvmCross 5.6.
Como siempre, el código está disponible en github:
Paso 1 - Crear un proyecto Xamarin Forms
Abra visual studio y selecciona la opción crear nueva solución => Multiplataforma => Aplicación de Forms en blanco.
Paso 2 - Configurar MvvmCross
- Agregue .Core al nombre del proyecto PCL.
- Elimine la página que fue creada por los formularios de Xamarin.
- Agregue el paquete MvvmCross Forms StarterPack a los proyectos específicos de la plataforma y .Core
- En Android: elimine MainActivity.cs.
- En los proyectos específicos de la plataforma, elimine la referencia al proyecto principal y haga clic en Aceptar. Luego agrega la referencia nuevamente. (Esto tiene que hacerse porque los proyectos específicos de la plataforma no reconocerán el proyecto central renombrado)
- Cambie la acción de compilación de los siguientes archivos de 'Página' a 'Recurso integrado'(Embedded Resource):
* Core|Pages|MainPage.xaml * Core|App.xaml
Seguiendo los pasos anteriores, MvvmCross ya estaría configurado. Al compilar se debe ver algo similar a la siguiente captura
Paso 3 - Archivo de arranque de la aplicación. CoreApp
public override void Initialize()
{
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
//No es necesario registrar las instancias que termina como Service, se registra automaticamente
//Mvx.LazyConstructAndRegisterSingleton();
//Configuramos la página de arranque, en nuestro caso será MainViewModel
RegisterNavigationServiceAppStart();
}
No es necesario aplicar ningún cambio. Si tenemos un servicio que el nombre no termine por Service, tenemos que registrar de la siguiente forma:
Mvx.LazyConstructAndRegisterSingleton();
Paso 4 - Crear un archivo de configuración
Creamos una carpeta con el nombre de Constants y dentro de ella, un CLASS estatica ConfigConstants.cs
using System;
namespace BaseForms.Constants
{
public static class ConfigConstants
{
public const string keyApi = ""; // Código de la api themoviedb
public const string baseUrl = "https://api.themoviedb.org/3/movie/upcoming?api_key="+keyApi+"&language=es-ES&page=1";
public const string imgSmall = "http://image.tmdb.org/t/p/w185";
public const string imgBig = "http://image.tmdb.org/t/p/w780";
}
}
Paso 5 - Crear las interfaces de los servicios
En el proyecto de la PCL, vamos a crear una carpeta Interfaces y dentro de la misma 2 carpetas más Iconnectors y IWebServices
Dentro de la carpeta Iconnectors, crearemos la interfaz para acceder a HttpClient
IWebClientService.cs
using System;
using System.Net.Http;
namespace BaseForms.Interfaces.IConnectors
{
public interface IWebClientService
{
HttpClient Client();
}
}
* Es ejemplo es muy básico, haga la interfaz según tu necesidad
La implementación de este archivo sería
using System;
using System.Net.Http;
using BaseForms.Interfaces.IConnectors;
using ModernHttpClient;
namespace BaseForms.Service.Connectors
{
public class WebClientService : IDisposable, IWebClientService
{
private HttpClient client;
public NativeCookieHandler CookieContainer { get; set; }
public WebClientService()
{
this.client = this.GenerateHttpClient();
}
private HttpClient GenerateHttpClient()
{
this.CookieContainer = new NativeCookieHandler();
NativeMessageHandler httpClientHandler = new NativeMessageHandler(
false,
true,
this.CookieContainer
);
return new HttpClient(httpClientHandler);
}
public HttpClient Client()
{
return this.client;
}
public void Dispose()
{
this.client.Dispose();
}
}
}
Para que funcione este código vamos a tener que instalar ModernHttpClient.
IMovieWebService
Agregaremos todos los servicios relacionados con las peliculas aqui
using System;
using System.Threading.Tasks;
using BaseForms.Models.Movie;
namespace BaseForms.Interfaces.IWebServices
{
public interface IMovieWebService
{
Task Movie();
}
}
using System;
using System.Globalization;
using System.Threading.Tasks;
using BaseForms.Constants;
using BaseForms.Interfaces.IConnectors;
using BaseForms.Interfaces.IWebServices;
using BaseForms.Models.Movie;
using Newtonsoft.Json;
namespace BaseForms.Service.WebServices
{
public class MovieWebService : IMovieWebService
{
IWebClientService conection;
public MovieWebService(IWebClientService conection)
{
this.conection = conection;
}
public async Task Movie()
{
var url = new Uri(string.Format(CultureInfo.InvariantCulture, ConfigConstants.baseUrl));
var result = await conection.Client().GetAsync(url);
if (result.IsSuccessStatusCode)
{
string content = await result.Content.ReadAsStringAsync();
if (content != null)
{
var response = JsonConvert.DeserializeObject(content);
response.IsCorrect = true;
return response;
}
}
return new MovieResponse() { Mensage = "Servicio no disponible" };
}
}
}
Para que funcione y compile, tenemos que instalar newtonsoft y crear los modelos.
Paso 7 - Crear los modelos
Para manejar los datos devuelvo por el servicio, vamos a crear 3 Modelos.
- BaseResponse.cs, para ayudar a tratar los errores
- MovieResponse.cs, respuesta del servicio
- ResultMovie.cs , es un objeto de MovieResponse, que va a devuelver el detalle por cada pelicula.
BaseResponse.cs
De forma predetermiada, inicializamos que la petición no ha sido bien realizada. Usaremos la propiedad IsCorrect para conocer si el resultado de la peticón ha sido código 200 o no.
namespace BaseForms.Models.Base
{
public class BaseResponse
{
public string Mensage { get; set; }
public bool IsCorrect { get; set; }
public BaseResponse()
{
Mensage = "No se ha podido realizar la operación";
IsCorrect = false;
}
}
}
MovieResponse.cs
Respuesta del servicio, trataremos solo algunos campos usando la propiedad JsonProperty para cambiar el nombre de la propiedad.
using System;
using System.Collections.Generic;
using BaseForms.Models.Base;
using Newtonsoft.Json;
namespace BaseForms.Models.Movie { public class MovieResponse : BaseResponse {
[JsonProperty("results")]
public List Movies { get; set; }
[JsonProperty("page")]
public int Page { get; set; }
[JsonProperty("total_results")]
public int TotalResults { get; set; }
//public Dates dates { get; set; }
[JsonProperty("total_pages")]
public int TotalPages { get; set; }
}
}
ResultMovie.cs
Modelo que contiene la información a pintar en la pantalla.
using System;
using BaseForms.Constants;
using BaseForms.Converters.Json;
using Newtonsoft.Json;
namespace BaseForms.Models.Movie { public class ResultMovie { [JsonProperty("vote_count")] public int VoteCount { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonConverter(typeof(BoolConverter))]
[JsonProperty("video")]
public bool Video { get; set; }
[JsonProperty("vote_average")]
public double VoteAverage { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("original_title")]
public string OriginalTitle { get; set; }
[JsonProperty("overview")]
public string Overview { get; set; }
[JsonProperty("backdrop_path")]
public string Backdrop { get; set; }
[JsonProperty("original_language")]
public string OriginalLanguage { get; set; }
public string ImgSmall => string.Format("{0}{1}", ConfigConstants.imgSmall, Backdrop);
public string ImgBig => string.Format("{0}{1}", ConfigConstants.imgBig, Backdrop);
/*
* Propiedades sin tratar
public double popularity { get; set; }
public string poster_path { get; set; }
public List genre_ids { get; set; }
public string backdrop_path { get; set; }
public bool adult { get; set; }
public string release_date { get; set; }
*/
}
}
Cabe destacar que se puede crear converters para tratar la información retornada por el servicios y que tambien se puede unir y crear nuevas propiedades igual a ImgSmall y ImgBig.
Paso 8 - Crear el converter JSON/newsoft
using System;
using Newtonsoft.Json;
namespace BaseForms.Converters.Json
{
public class BoolConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((bool)value) ? 1 : 0);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return reader.Value.ToString() == "1";
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(bool);
}
}
}
Paso 9 - Crear el ViewModel Base
Todo lo que es generico en todos los viewmodels, lo meteremos aqui.En nuestro caso sería:
- La navegación: navigationService
- El web services de las peliculas: webMovieService
- IsBusy , responsable por indicar si está esperando la respuesta del servicio o no.
- Title , responsable por el título de la página
using System;
using BaseForms.Interfaces.IWebServices;
using MvvmCross.Core.Navigation;
using MvvmCross.Core.ViewModels;
namespace BaseForms.ViewModels
{
public class BaseViewModel : MvxViewModel
{
protected readonly IMvxNavigationService navigationService;
protected readonly IMovieWebService webMovieService;
protected bool isBusy;
protected string title;
public BaseViewModel(IMvxNavigationService navigationService,IMovieWebService webMovieService)
{
this.navigationService = navigationService;
this.webMovieService = webMovieService;
}
public bool IsBusy
{
get
{
return this.isBusy;
}
set
{
this.isBusy = value;
this.RaisePropertyChanged(() => this.IsBusy);
}
}
public string Title
{
get
{
return this.title;
}
set
{
this.title = value;
this.RaisePropertyChanged(() => this.Title);
}
}
public override void Prepare(object parameter)
{
}
}
}
Paso 10 - Crear el ViewModel MainViewModel
Este viewmodel va a contener:
Herenda de BaseViewModel
Movies , listado de peliculas
ResultMovie, pelicula seleccionada
ViewAppeared, relacionado con el ciclo de vida de la aplicación. Se entra en está función cuando la vista se carga.
OpenModal , navega a otra página con los valores de la pelicula seleccionada
using System.Collections.Generic;
using System.Threading.Tasks;
using BaseForms.Interfaces.IWebServices;
using BaseForms.Models.Movie;
using BaseForms.ViewModels;
using MvvmCross.Core.Navigation;
using MvvmCross.Core.ViewModels;
namespace BaseForms.Core.ViewModels
{
public class MainViewModel : BaseViewModel
{
public MainViewModel(IMvxNavigationService navigationService, IMovieWebService webMovieService) : base(navigationService, webMovieService)
{
}
public override Task Initialize()
{
return base.Initialize();
}
public async override void ViewAppeared()
{
this.IsBusy = true;
var response = await this.webMovieService.Movie();
this.IsBusy = false;
if (response.IsCorrect)
{
Movies = response.Movies;
}
else
{
//mostrar el mensaje de error
}
}
private List movies;
public List Movies
{
get
{
return this.movies;
}
set
{
this.movies = value;
this.RaisePropertyChanged();
}
}
private ResultMovie selectedMovie;
public ResultMovie SelectedMovie
{
get
{
return this.selectedMovie;
}
set
{
this.selectedMovie = value;
this.RaisePropertyChanged();
this.OpenModal(value);
}
}
private void OpenModal(ResultMovie value)
{
object parameter = value;
navigationService.Navigate(parameter);
}
}
}
Paso 11 - Vista MainView
Preparando la vista, elementos a tener en cuenta:
MvxContentPage, fijate que estamos usando MvxContentPage en lugar de ContentPage
Que estamos relacionando el viewmodel con la vista. d:MvxContentPage x:TypeArguments="viewModels:MainViewModel
Que estamos cargando el template xmlns:templates="clr-namespace:BaseForms.Views.Template;assembly=BaseForms"
Que los valores que se pinta, viene de la propiedad Movies
SelectedMovie se usa para guardar el elemento seleccionado.
Código aqui Vista MainView
Paso 12 - Crear template
Creamos una carpeta Views en el proyecto BaseForms.Core y luego otro carpeta más con el nombre de template dentro de la carpeta Views.
El próximo paso es crear nuestros templates distinto, para esto debemos crear una página contentView
Template 1 con imagen
Código aqui Template 1
Template 2 sin imagen
Código aqui Template 2
Los templates son buenas formas de reutilizar código, para modificar el aspecto de la lista lo único que debemos indicar ahora es el nombre del template
Paso 13 - Crear el ViewModel AboutViewModel
Este viewmodel contiene:
CloseCommand => Un Command para cerrar la modal
La propiedad SelectedMovie, para pintar los datos en la pantalla
Prepare , este metodo, recupera el valor pasando por el MainViewModel.
using System;
using BaseForms.Interfaces.IWebServices;
using BaseForms.Models.Movie;
using BaseForms.ViewModels;
using MvvmCross.Core.Navigation;
using MvvmCross.Core.ViewModels;
namespace BaseForms.Core.ViewModels
{
public class AboutViewModel : BaseViewModel
{
private IMvxCommand closeCommand;
public IMvxCommand CloseCommand => closeCommand = closeCommand ?? new MvxCommand(DoCloseHandler);
private ResultMovie selectedMovie;
public AboutViewModel(IMvxNavigationService navigationService, IMovieWebService webMovieService) : base(navigationService, webMovieService)
{
}
public ResultMovie SelectedMovie
{
get
{
return this.selectedMovie;
}
set
{
this.selectedMovie = value;
this.RaisePropertyChanged();
}
}
private void DoCloseHandler()
{
Close(this);
}
public override void Prepare(object parameter)
{
this.SelectedMovie = (ResultMovie)Convert.ChangeType(parameter, typeof(ResultMovie));
this.Title = this.SelectedMovie.Title;
}
}
}
Paso 14 - Crear la AboutView
Código aqui Vista AboutView
Para que se visualize como modal, debemos agregar el tipo de presentación en la vista. (MvxModalPresentation)
using System;
using System.Collections.Generic;
using BaseForms.Core.ViewModels;
using MvvmCross.Forms.Views;
using MvvmCross.Forms.Views.Attributes;
using Xamarin.Forms;
namespace BaseForms.Core.Pages
{
[MvxModalPresentation(WrapInNavigationPage = true, Title = "Modal")]
public partial class AboutPage : MvxContentPage
{
public AboutPage()
{
InitializeComponent();
}
}
}
El resultado final de este turtorial, se puede ver en la carpeta "FIN" de este repositorio.