Loupe

[Xamarin] Partie 4 - Utilisation des services

Dans le billet précédent , nous avons pu voir l’intégration de nos ViewModels au sein de notre application ainsi que leur utilisation, dans la continuité, nous allons principalement traiter de l’utilisation des services.

Le but ici est donc de créer un service qui va permettre à l’application d’obtenir ou poster des informations auprès d’un WebService distant, dans notre cas, nous allons récupérer des informations liées à un utilisateur de Betaseries et l’afficher sur une vue de l’application.

Afin de pouvoir accéder à ces informations, nous devrons au préalable nous munir d’une clé d’API (fournie par Betaserie après les avoir contacter par mail), et enfin d’un compte valide (Login + Mot de passé hashé en MD5). Après avoir réunis ces prérequis, vous pourrez avoir accès aux API du site, les informations retirées ici seront au format JSON, dans le but de les récupérer et de les traiter, il vous faudra installer deux packages Nuget :

 

Microsoft HTTP Client Libraries

image

 

Json.NET

image

 

Une fois les packages installés, nous créerons deux classes d’entité représentant respectivement notre utilisateur courant ainsi que les différentes séries qu’il suit, nous en aurons besoin dans notre futur service.

 

MyWatchlist.Core/Entities/UserEntity.cs

namespace MyWatchlist.Core.Entities
{
    public class UserEntity
    {
        public int BadgesCount { get; set; }

        public int CommentsCount { get; set; }

        public int EpidodesCount { get; set; }

        public int FriendsCount { get; set; }

        public string Id { get; set; }

        public string Login { get; set; }

        public int SeriesCount { get; set; }

        public int Xp { get; set; }
    }
}

 

MyWatchlist.Core/Entities/SerieEntity.cs

namespace MyWatchlist.Core.Entities
{
    public class SerieEntity
    {
        public int CommentsCount { get; set; }

        public string Description { get; set; }

        public int EpisodesCount { get; set; }

        public int FollowersCount { get; set; }

        public int Id { get; set; }

        public int SeasonsCount { get; set; }

        public string Title { get; set; }
    }
}

Nous allons également ajouté un fichier regroupant les différentes informations relatives à l’identification.

 

MyWatchlist.Core/Common/Globals.cs

namespace MyWatchlist.Core.Common
{
    public static class Globals
    {
        public static string ApiKey = "VOTRE_CLÉ_API";

        public static string Login = "VOTRE_LOGIN";

        public static string Password = "VOTRE_MOTDEPASSE_HASHÉ_EN_MD5";

        public static string Token = string.Empty;
    }
}

Une fois nos classes créées, nous devrons intégrer le service à notre application, il proposera les fonctionnalités suivantes:

  • Authentification
  • Récupération des informations de l’utilisateur
  • Récupération de la liste des séries suivies par l’utilisateur
  • Traitement de l’évènement lorsqu’une série est sélectionnée dans la liste

 

MyWatchlist.Core/Interfaces/IUserService.cs

using MyWatchlist.Core.Entities;
using System.Collections.ObjectModel;
using System.Threading.Tasks;

namespace MyWatchlist.Core.Interfaces
{
    public interface IUserService
    {
        Task<UserEntity> GetInfosAsync();

        Task<ObservableCollection<SerieEntity>> GetSeriesAsync();

        Task LoginAsync(string login, string password);

        void OnSerieEntitySelectedCommand(SerieEntity entity);
    }
}

Le réel intérêt d’utiliser une interface est de pouvoir proposer une implémentation différente pour chaque plateforme ciblée (Android, iOS, Windows Phone), ainsi, du code spécifique pourra être écrit. Nous n’en aurons pas besoin dans notre cas puisque les librairies HttpClient et Json.NET sont disponibles en PCL, les seules implémentations seront donc présentes directement dans notre Portable Class Library.

 

MyWatchlist.Core/Services/UserService.cs

using MyWatchlist.Core.Common;
using MyWatchlist.Core.Entities;
using MyWatchlist.Core.Interfaces;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace MyWatchlist.Core.Services
{
    public class UserService : IUserService
    {
        #region ATTRIBUTES

        public static HttpClient HttpClient = new HttpClient();

        #endregion ATTRIBUTES

        #region METHODS

        // récupération des informations de l'utilisateur
        async Task<UserEntity> IUserService.GetInfosAsync()
        {
            var response = await HttpClient.GetAsync(
                string.Format("http://api.betaseries.com/members/infos?v=2.2&key={0}&token={1}", Globals.ApiKey, Globals.Token)
            );
            var html = await response.Content.ReadAsStringAsync();
            var json = JObject.Parse(html);
            var user = new UserEntity
            {
                BadgesCount = Int32.Parse((string)json["member"]["stats"]["badges"]),
                CommentsCount = Int32.Parse((string)json["member"]["stats"]["comments"]),
                EpidodesCount = Int32.Parse((string)json["member"]["stats"]["episodes"]),
                FriendsCount = Int32.Parse((string)json["member"]["stats"]["friends"]),
                Id = (string)json["member"]["id"],
                Login = (string)json["member"]["login"],
                SeriesCount = Int32.Parse((string)json["member"]["stats"]["shows"]),
                Xp = Int32.Parse((string)json["member"]["xp"])
            };
            return user;
        }

        // récupération de la liste des séries suivies par l'utilisateur
        async Task<ObservableCollection<SerieEntity>> IUserService.GetSeriesAsync()
        {
            var response = await HttpClient.GetAsync(
                string.Format("http://api.betaseries.com/members/infos?v=2.2&key={0}&token={1}", Globals.ApiKey, Globals.Token)
            );
            var html = await response.Content.ReadAsStringAsync();
            var json = JObject.Parse(html);
            var series = json["member"]["shows"].Select(obj => new SerieEntity
            {
                Description = (string)obj["description"],
                EpisodesCount = Int32.Parse((string)obj["episodes"]),
                FollowersCount = Int32.Parse((string)obj["followers"]),
                Id = Int32.Parse((string)obj["id"]),
                SeasonsCount = Int32.Parse((string)obj["seasons"]),
                Title = (string)obj["title"],
            });
            return new ObservableCollection<SerieEntity>(series);
        }

        // connexion au service Betaseries + récupération du Token d'identification
        async Task IUserService.LoginAsync(string login, string password)
        {
            var data = new List<KeyValuePair<string, string>>();
            var content = new FormUrlEncodedContent(data);
            var response = await HttpClient.PostAsync(
                string.Format("https://api.betaseries.com/members/auth?v=2.2&key={0}&login={1}&password={2}", Globals.ApiKey, login, password), content
            );
            var json = JObject.Parse(await response.Content.ReadAsStringAsync());
            Globals.Token = (string)json["token"];
        }

        // à effectuer lorsqu'une série sera sélectionnée
        void IUserService.OnSerieEntitySelectedCommand(SerieEntity entity)
        {
        }

        #endregion METHODS
    }
}

Pour chacune des fonctionnalités implémentées dans cette classe, nous utilisons notre client HTTP pour cibler une URL et récupérer les données de la page au format JSON, ensuite, lorsque cela est nécessaire, nous utilisons nos classes d’entité pour stocker ces données sous forme d’objet.

À présent, il nous faut référencer le service dans notre ViewModel afin d’en utiliser les données, pour cela, nous allons rajouter une entrée dans le constructeur et conserver le service dans une variable locale, il n’y a plus qu’à mettre à jour les données par la méthode UpdateAsync et le tour est joué.

 

MyWatchlist.Core/ViewModels/FirstViewModel.cs

using Cirrious.MvvmCross.ViewModels;
using MyWatchlist.Core.Common;
using MyWatchlist.Core.Entities;
using MyWatchlist.Core.Interfaces;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MyWatchlist.Core.ViewModels
{
    public partial class FirstViewModel : MvxViewModel
    {
        #region ATTRIBUTES

        #region ATTRIBUTES MVVM

        private string _login;

        private ObservableCollection<SerieEntity> _series = new ObservableCollection<SerieEntity>();

        public string Login
        {
            get { return _login; }
            set { _login = value; RaisePropertyChanged(() => Login); }
        }

        public ObservableCollection<SerieEntity> Series
        {
            get { return _series; }
            set { _series = value; }
        }

        #endregion ATTRIBUTES MVVM

        #region ATTRIBUTES SERVICES

        private readonly IUserService _userService;

        #endregion ATTRIBUTES SERVICES

        #endregion ATTRIBUTES

        #region CONSTRUCT

        public FirstViewModel(IUserService userService)
        {
            _userService = userService;
        }

        #endregion CONSTRUCT

        #region METHODS

        // mise à jour des informations du ViewModel
        public async Task UpdateAsync()
        {
            await _userService.LoginAsync(Globals.Login, Globals.Password);
            var user = await _userService.GetInfosAsync();
            Login = user.Login;

            Series.Clear();
            foreach (var serie in (await _userService.GetSeriesAsync()))
            {
                Series.Add(serie);
            }
        }

        #endregion METHODS

        #region COMMANDS

        public ICommand SerieEntitySelectedCommand { get { return new MvxCommand<SerieEntity>(OnSerieEntitySelectedCommand); } }

        private void OnSerieEntitySelectedCommand(SerieEntity entity)
        {
            _userService.OnSerieEntitySelectedCommand(entity);
        }

        #endregion COMMANDS
    }
}

Nous avons maintenant notre Service et notre ViewModel, toutefois, il reste une dernière chose à faire avant d’utiliser notre UserService, l’injection de dépendance. En effet nous indiquons qu’un paramètres IUserService doit être passé au constructeur de FirstViewModel mais à aucun moment nous le passons en paramètres, pour cela, MvvmCross utilise un container IOC permettant d’effectuer ce type d’opérations, sans cette injection de dépendance, notre application ne pourra pas se lancer, l’idée est donc de récupérer chacune de nos classes instanciables contenant le suffixe “*Service” et de les injecter automatiquement en tant que Singleton au sein de nos ViewModels en passant par leur constructeur, pour effectuer cette injection nous utiliserons le code suivant :

 

MyWatchlist.Core/App.cs

using Cirrious.CrossCore.IoC;
using MyWatchlist.Core.ViewModels;

namespace MyWatchlist.Core
{
    public class App : Cirrious.MvvmCross.ViewModels.MvxApplication
    {
        public override void Initialize()
        {
            CreatableTypes()
                .EndingWith("Service")
                .AsInterfaces()
                .RegisterAsLazySingleton(); // l'injection de dépendances se fait ici
            RegisterAppStart<FirstViewModel>();
        }
    }
}

Pour plus de détails, voici un article sur l’injection de dépendance avec MvvmCross.

 

Les modifications à effectuer dans la PCL sont maintenant terminées, il ne nous reste plus qu’à modifier nos fichiers de Layout dans le projet Android, nous aurons besoin de notre fichier principal FirstView.axml dans lequel nous afficherons les informations de l’utilisateur ainsi que ses séries préférées mais également d’un deuxième, SerieEntityItem.axml, qui lui permettra l’affichage de chaque série dans la ListView.

 

MyWatchlist.Droid/Resources/Layout/FirstView.axml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        local:MvxBind="Text Login" />

    <Mvx.MvxListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        local:MvxBind="ItemsSource Series;ItemClick SerieEntitySelectedCommand"
        local:MvxItemTemplate="@layout/serieentityitem" />

</LinearLayout>

Pour associer la sélection d’une série à une action, nous utiliserons le mécanisme de commande proposé par MVVMCross, il suffit pour cela d’effectuer un Binding sur la propriété ItemClick de notre ListView et d’utiliser derrière le type MvxCommand, permettant d’effecter une action tout en récupérant le contexte de données associé à chaque élément.

 

MyWatchlist.Droid/Resources/Layout/SerieEntityItem.axml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="5dp"
    android:paddingBottom="5dp">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:textColor="@android:color/white"
        local:MvxBind="Text Title" />

</RelativeLayout>

Ici nous associons le titre de chacune des séries au champ Text de notre TextView, le contexte de données étant une série (SerieEntity).

Et voilà ! Nous avons maintenant un service capable de communiquer avec un WebService distant.

Enjoy :)

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus