Injection de dépendances en Xamarin Forms

L’inversion de contrôle (IOC) et l’injection de dépendances sont deux patterns bien répandus en développement de logiciels. Ils servent à créer des objets découplés.
Cet article montre comment intégrer ces deux notions dans les applications mobiles avec Xamarin Forms. L’objectif est de créer une application faiblement découplée, facile à maintenir, à tester et à faire évoluer. Un autre avantage significatif de cette architecture est de rendre possible l’exécution d’un code spécifique aux Android API ou iOS API à partir d’un projet de type PCL (Portable Class Library).
Pour l’instanciation et la configuration des objets et de ses dépendances, il est très commun d’utiliser des frameworks de conteneurs IOC. Ces frameworks sont disponibles en packages Nuget. Les plus connus sont : Ninject, Autofac, TinyIoc, StructureMap… La plupart d’entre eux partagent pratiquement les mêmes concepts et la même syntaxe, au moins au niveau de l’enregistrement et de la récupération des dépendances. Comme exemple, nous allons utiliser Unity.

Projet Xamarin Forms en MVVM

On va commencer par créer une application Xamarin Forms qui utilise le type de projet PCL (et non pas Shared Project) pour partager du code. On peut aussi utiliser .NET Standard, qui vient de remplacer PCL.

Création du modèle Produit

On crée ensuite les 3 dossiers pour la structure MVVM : Models, ViewModels and Views. Dans le dossier Models, on crée une classe Product comme ceci :

public class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
    public override string ToString()
    {
        return $"{Name} : {Price} USD";
    }
}

Création de ProductsService

L’interface IProductsService définit la méthode pour récupérer la liste des produits. Elle est implémentée par la classe ProductsService. Typiquement, la liste des produits est récupérée à partir d’un appel à un web service. Pour la simplicité, nous allons utiliser une liste statique.

public interface IProductsService
{
    IEnumerable<Product> Getproducts();
}
public class ProductsService : IProductsService
{
    public IEnumerable<Product> Getproducts()
    {
        return new List<Product>
        {
            new Product { Name = "Surface Laptop", Price = 1500 },
            new Product { Name = "XBox One", Price = 400 },
        };
    }
}

Création du ViewModel

ProductsViewModel est l’intermédiaire entre l’interface utilisateur et le code métier. Il récupère les données à partir du service et les expose à la page XAML à travers la propriété Products.

public class ProductsViewModel
{
    private readonly ProductsService _productsService;
    public IEnumerable<Product> Products { get; set; }
    public ProductsViewModel()
    {
        _productsService = new ProductsService();
        DownloadProducts();
    }
    public void DownloadProducts()
    {
        Products = _productsService.Getproducts();
    }
}

Création de la Vue

La vue ProductsView va être reliée à ProductsViewModel à travers le DataBinding, ce qui lui permet d’accéder à ses propriétés et commandes. Elle prendra la propriété Products pour l’afficher dans une ListView.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IocAndDiXamarinForms.Views.ProductsPage"
xmlns:viewModels="clr-namespace:IocAndDiXamarinForms.ViewModels">

<ContentPage.BindingContext>
    <viewModels:ProductsViewModel />
</ContentPage.BindingContext>

<ContentPage.Content>
    <ListView ItemsSource="{Binding Products}" />
</ContentPage.Content>

</ContentPage>

Note : La ListView affiche les deux propriétés Name et Price pour chaque produit. Ceci est dû au fait que l’on a fait un override pour la méthode ToString() dans la classe Product. Et la ListView appelle, par défaut, la méthode ToString() s’il n’y a pas de binding explicite à Name et Price.

Tip : Le code au-dessus utilise le design pattern MVVM pour isoler le code de l’interface graphique du code métier.

Plus de détails ici

Configuration de IOC/DI

ProductsViewModel crée une nouvelle instance de l’objet ProductsService, ce qui enfreint le principe de la responsabilité unique de la POO. Au lieu de créer l’objet à cet endroit, quelqu’un d’autre prendra la responsabilité de le créer et de le passer à celui qui le demande. ProductsViewModel ne créera donc pas une instance, mais en demandera une et l’obtiendra dans son constructeur.
Si l’on pense aux tests unitaires pour lesquels on a besoin de créer un Mock pour l’objet ProductsService, on voit bien l’utilité de passer par une interface. On pourra passer n’importe quelle implémentation de l’interface IProductsService comme ProductsService ou un certain MockProductsService dédié pour les tests unitaires. On devra donc modifier le code pour utiliser les interfaces au lieu des objets concrets.

public class ProductsViewModel
{
    private readonly IProductsService _productsService;
    public ProductsViewModel(IProductsService productsService)
    {
        _productsService = productsService;
    }
    // code supprimé pour la brieveté
}

Configuration de Unity

On installe le package Nuget de Unity dans le projet PCL avec cette commande :
PM> Install-Package Unity -Version 4.0.1

Tip : On pourrait aussi installer les packages Nuget par clic droit sur la Solution dans Visual Studio et sélectionner l’option ‘Manage Nuget Packages for Solution’.

Enregistrer les dépendances

On crée le conteneur pour enregistrer les dépendances dans le constructeur de la classe App.

var unityContainer = new UnityContainer();
unityContainer.RegisterType<IProductsService, ProductsService>();
unityContainer.RegisterInstance(typeof(ProductsViewModel));//optional

Configurer le ServiceLocator

Puis, on configure l’application pour chercher ses dépendances dans le conteneur. Ceci est faisable grâce au ServiceLocator :

var unityServiceLocator = new UnityServiceLocator(unityContainer);
ServiceLocator.SetLocatorProvider(() => unityServiceLocator);

Résoudre les dépendances

À ce stade-là, on pourrait résoudre les dépendances. L’application va récupérer une instance de ProductsService lorsqu’elle demandera un objet implémentant IProductsService dans le constructeur de ProductsViewModel.
Jusqu’à maintenant, la vue est reliée au view-model. Elle crée une nouvelle instance de ce view-model. Nous voulons changer cela pour récupérer le view-model à partir du conteneur. Pour cela, on supprime la partie du code XAML utilisant l’élément BindingContext, et nous appliquons l’une des deux solutions suivantes :

• À partir du code-behind (C-Sharp), on a un accès direct à ServiceLocator:

public ProductsPage()
{
    InitializeComponent();
    BindingContext = ServiceLocator.Current.GetInstance(typeof(ProductsViewModel));
}

• À partir du code XAML, en utilisant le ViewModelLocator :

XAML ne peut pas appeler la méthode GetInstance(…) directement pour récupérer le view-model à partir de ServiceLocator. Pour cela, on passera par une propriété. Créons une nouvelle classe ViewModelLocator définissant cette propriété :

public class ViewModelLocator
{
    public ProductsViewModel ProductsViewModel
    {
        get { return ServiceLocator.Current.GetInstance<ProductsViewModel>(); }
    }
}

Dans App.xaml, on crée une instance de ViewModelLocator à laquelle les vues XAML pourront accéder. C’est pour cela qu’elle est déclarée dans les ressources globales :

<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:IocAndDiXamarinForms.ViewModels"
x:Class="IocAndDiXamarinForms.App">

<Application.Resources>
    <ResourceDictionary>
        <viewModels:ViewModelLocator x:Key="Locator"/>
    </ResourceDictionary>
</Application.Resources>
</Application>

A partir des vue XAML, on aura accès au view-model récupéré à partir du conteneur :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IocAndDiXamarinForms.Views.ProductsPage"
xmlns:viewModels="clr-namespace:IocAndDiXamarinForms.ViewModels"
BindingContext="{Binding ProductsViewModel, Source={StaticResource Locator}}">

<!--<ContentPage.BindingContext>
    <viewModels:ProductsViewModel />
</ContentPage.BindingContext>-->

<ContentPage.Content>
    <ListView ItemsSource="{Binding Products}" />
</ContentPage.Content>
</ContentPage>

Finalement, nous avons transformé la manière dont l’application obtient ses dépendances. La création des objets se fait dans le conteneur, et non plus dans nos objets métier. Nous avons donc délégué cette responsabilité.

Dans la section suivante, nous verrons deux avantages importants de l’utilisation de IoC/DI : l’accès au code spécifique à la plateforme Android ou iOS, et le développement des tests unitaires.

5. Accès au code spécifique à chaque platform depuis PCL

Nous ne pouvons pas accéder directement au code spécifique Android, iOS ou UWP à partir du PCL. Pour cela, nous utilisons une abstraction de l’implémentation par une interface. Un des cas est l’utilisation de la fonctionnalité ‘text to speech’ : on crée l’implémentation native spécifique à chaque plateforme, puis on passe cette implémentation au projet PCL.
L’interface doit être créée dans le projet PCL :

public interface ITextToSpeech
{
    void Speak(string text);
}

On implémente l’interface dans chaque projet spécifique à une plateforme. Le code suivant est l’implémentation pour iOS :

public class TextToSpeechIosImpl : ITextToSpeech
{
    public void Speak(string text)
    {
        var speechSynthesizer = new AVSpeechSynthesizer();
        var speechUtterance = new AVSpeechUtterance(text)
        {
            Rate = AVSpeechUtterance.MaximumSpeechRate / 4,
            Voice = AVSpeechSynthesisVoice.FromLanguage("en-US"),
            Volume = 0.5f,
            PitchMultiplier = 1.0f
        };
        speechSynthesizer.SpeakUtterance(speechUtterance);
    }
}

On peut enregistrer et résoudre cet objet avec deux approches : DependencyService et l’injection de dépendance.

DependencyService

Xamarin Forms définit une classe DependencyService qui peut enregistrer et résoudre les dépendances. Elle peut être utilisée à partir du projet PCL ou à partir des projets Android et iOS.
Pour enregistrer une dépendance dans le projet iOS :

[assembly: Xamarin.Forms.Dependency(typeof(TextToSpeechIosImpl))]

Pour récupérer l’implémentation à partir du PCL :

var textToSpeach = DependencyService.Get<ITextToSpeech>();

Vous trouverez plus de détails sur les implémentations pour chaque plateforme ici.

Injection de dépendance

Pour enregistrer l’implémentation d’un objet dans le conteneur, on le transmet en tant que paramètre dans le constructeur de la classe App :

// AppDelegate.cs (iOS native project)
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();
    LoadApplication(new App(new TextToSpeechIosImpl()));
    return base.FinishedLaunching(app, options);
}
// App.xaml.cs
public App(ITextToSpeech textToSpeech)
{
    //... code supprimé pour brièveté
    unityContainer.RegisterType<IProductsService, ProductsService>();
    unityContainer.RegisterInstance(typeof(ITextToSpeech), textToSpeech);
}

TextToSpeechIosImpl est maintenant récupérable à partir de ProductsViewModel. On pourrait donc invoquer le méthode Speak() qui exécutera l’implémentation native à partir du projet PCL.

public ProductsViewModel(IProductsService productsService, ITextToSpeech textToSpeech)
{
    //... code supprimé pour brièveté
    textToSpeech.Speak("IoC and DI");
}

Tests unitaires & Mocks

Pour créer un test unitaire pour ProductsViewModel, on a besoin de faire passer une implémentation de ITextToSpeech alors que l’implémentation que l’on a créée est dépendante de la plateforme. Heureusement, on pourrait créer notre propre implémentation pour ‘mocker’ l’objet réel.

public class MockTextToSpeach : ITextToSpeech
{
    public void Speak(string text)
    {
        // implémentation...
    }
}

Tip : Dans cet exemple, on a choisi de créer un Mock en implémentant l’interface. Mais il existe des frameworks pour en créer facilement, comme Moq.

Accéder à Moq.
On peut aussi créer un Mock pour IProductsService, mais comme l’implémentation réelle n’est pas dépendante d’une plateforme, n’appelle pas un web service ou une base de données, on va continuer à en utiliser. Le test unitaire est le suivant :

[TestClass]
public class ProductsUnitTest
{
    [TestMethod]
    public void GetProductsTest()
    {
        // Arrange
        IProductsService productsService = new ProductsService();
        ITextToSpeech mockTextToSpeech = new MockTextToSpeach();
        var vm = new ProductsViewModel(productsService, mockTextToSpeech);
        // Act
        vm.DownloadProducts();
        // Assert
        var expected = productsService.Getproducts();
        var actual = vm.Products;
        Assert.AreEqual(expected, actual);
    }
}

Conclusion

Nous avons vu l’utilité d’appliquer IOC/DI dans les projets Xamarin Forms afin d’assurer la séparation des responsabilités, la maintenabilité et l’extensibilité. Les étapes sont pratiquement les mêmes pour les autres plateformes comme WPF, ASP.NET ou Windows UWP.
De plus, le nombre de plateformes supportant les conteneurs IOC s’élargit et leur syntaxe s’uniformise, ce qui les rend simples à apprendre une seule fois, pour pouvoir ensuite les appliquer partout.

Le code source complet est disponible sur Github

© SOAT
Toute reproduction interdite sans autorisation de l’auteur

Nombre de vue : 255

AJOUTER UN COMMENTAIRE