Resolución de dependencias en Xamarin.Forms

Este artículo explica cómo inyectar un método de resolución de dependencias en Xamarin.Forms para que el contenedor de inyección de dependencias de una aplicación tenga control sobre la creación y la duración de los representadores personalizados, los efectos y las implementaciones de DependencyService.

En el contexto de una aplicación Xamarin.Forms que use el patrón Modelo-Vista-Modelo de vista (MVVM), puede usarse un contenedor de inyección de dependencias para registrar y resolver modelos de vista, y para registrar servicios e inyectarlos en modelos de vista. Durante la creación del modelo de vista, el contenedor inyecta las dependencias que sean necesarias. Si esas dependencias no se han creado, el contenedor las crea y resuelve primero. Para más información sobre la inserción de dependencias, incluidos ejemplos de inyección de dependencias en modelos de vista, consulte Inserción de dependencias.

El control sobre la creación y la duración de los tipos en los proyectos de plataforma lo realiza tradicionalmente Xamarin.Forms, que usa el método Activator.CreateInstance para crear instancias de representadores personalizados, efectos e implementaciones de DependencyService. Por desgracia, esto limita el control del desarrollador sobre la creación y la duración de estos tipos, así como la capacidad de inyectarles dependencias. Este comportamiento puede cambiarse inyectando un método de resolución de dependencias en Xamarin.Forms que controle cómo se crearán los tipos, ya sea por el contenedor de inyección de dependencias de la aplicación o por Xamarin.Forms. Sin embargo, tenga en cuenta que no es necesario inyectar un método de resolución de dependencias en Xamarin.Forms. Xamarin.Forms seguirá creando y administrando la duración de los tipos en los proyectos de la plataforma si no se inyecta un método de resolución de dependencias.

Nota:

Aunque este artículo se centra en inyectar un método de resolución de dependencias en Xamarin.Forms que resuelve tipos registrados usando un contenedor de inyección de dependencias, también es posible inyectar un método de resolución de dependencias que use actory Methods para resolver tipos registrados.

Inserción de un método de resolución de dependencias

La clase DependencyResolver proporciona la capacidad de inyectar un método de resolución de dependencias en Xamarin.Forms, usando el método ResolveUsing. Después, cuando Xamarin.Forms necesite una instancia de un tipo concreto, el método de resolución de dependencias tendrá la oportunidad de proporcionar la instancia. Si el método de resolución de dependencias escribe null para un tipo solicitado, Xamarin.Forms vuelve a intentar crear la propia instancia del tipo usando el método Activator.CreateInstance.

El siguiente ejemplo muestra cómo establecer el método de resolución de dependencias con el método ResolveUsing:

using Autofac;
using Xamarin.Forms.Internals;
...

public partial class App : Application
{
    // IContainer and ContainerBuilder are provided by Autofac
    static IContainer container;
    static readonly ContainerBuilder builder = new ContainerBuilder();

    public App()
    {
        ...
        DependencyResolver.ResolveUsing(type => container.IsRegistered(type) ? container.Resolve(type) : null);
        ...
    }
    ...
}

En este ejemplo, el método de resolución de dependencias se establece en una expresión lambda que usa el contenedor de inyección de dependencias Autofac para resolver cualquier tipo que se haya registrado con el contenedor. En caso contrario, se devolverá null, lo que dará lugar a que Xamarin.Forms intente resolver el tipo.

Nota:

El API usada por un contenedor de inyección de dependencias es específica del contenedor. Los ejemplos de código de este artículo usan Autofac como contenedor de inyección de dependencias, que proporciona los tipos IContainer y ContainerBuilder. Podrían usarse igualmente contenedores de inyección de dependencias alternativos, pero usarían API diferentes de las que se presentan aquí.

Tenga en cuenta que no es necesario establecer el método de resolución de dependencias durante el inicio de la aplicación. Se puede establecer en cualquier momento. La única restricción es que Xamarin.Forms necesita conocer el método de resolución de dependencias en el momento en que la aplicación intenta consumir tipos almacenados en el contenedor de inyección de dependencias. Por lo tanto, si hay servicios en el contenedor de inyección de dependencias que la aplicación necesitará durante el inicio, el método de resolución de dependencias tendrá que establecerse al principio del ciclo de vida de la aplicación. Del mismo modo, si el contenedor de inyección de dependencias administra la creación y la duración de una Effect determinada, la Xamarin.Forms necesitará conocer el método de resolución de dependencias antes de intentar crear una vista que use esa Effect.

Advertencia

El registro y la resolución de tipos con un contenedor de inyección de dependencias tiene un coste de rendimiento debido al uso de la reflexión por parte del contenedor para crear cada tipo, especialmente si las dependencias se están reconstruyendo para cada navegación de página en la aplicación. Si hay muchas dependencias, o estas son muy amplias, el costo de la creación puede aumentar significativamente.

Registro de tipos

Los tipos deben registrarse en el contenedor de inyección de dependencias antes de que pueda resolverlos mediante el método de resolución de dependencias. El siguiente ejemplo de código muestra los métodos de registro que la aplicación de ejemplo expone en la clase App, para el contenedor Autofac:

using Autofac;
using Autofac.Core;
...

public partial class App : Application
{
    static IContainer container;
    static readonly ContainerBuilder builder = new ContainerBuilder();
    ...

    public static void RegisterType<T>() where T : class
    {
        builder.RegisterType<T>();
    }

    public static void RegisterType<TInterface, T>() where TInterface : class where T : class, TInterface
    {
        builder.RegisterType<T>().As<TInterface>();
    }

    public static void RegisterTypeWithParameters<T>(Type param1Type, object param1Value, Type param2Type, string param2Name) where T : class
    {
        builder.RegisterType<T>()
               .WithParameters(new List<Parameter>()
        {
            new TypedParameter(param1Type, param1Value),
            new ResolvedParameter(
                (pi, ctx) => pi.ParameterType == param2Type && pi.Name == param2Name,
                (pi, ctx) => ctx.Resolve(param2Type))
        });
    }

    public static void RegisterTypeWithParameters<TInterface, T>(Type param1Type, object param1Value, Type param2Type, string param2Name) where TInterface : class where T : class, TInterface
    {
        builder.RegisterType<T>()
               .WithParameters(new List<Parameter>()
        {
            new TypedParameter(param1Type, param1Value),
            new ResolvedParameter(
                (pi, ctx) => pi.ParameterType == param2Type && pi.Name == param2Name,
                (pi, ctx) => ctx.Resolve(param2Type))
        }).As<TInterface>();
    }

    public static void BuildContainer()
    {
        container = builder.Build();
    }
    ...
}

Cuando una aplicación usa un método de resolución de dependencias para resolver tipos de un contenedor, los registros de tipos se realizan normalmente desde los proyectos de la plataforma. Esto habilita a los proyectos de la plataforma a registrar tipos para representadores personalizados, efectos e implementaciones de DependencyService.

Tras el registro de tipos de un proyecto de plataforma, debe compilarse el objeto IContainer, lo que se consigue llamando al método BuildContainer. Este método invoca el método Build de Autofac en la instancia ContainerBuilder, que compila un nuevo contenedor de inyección de dependencias que contiene las inscripciones que se han hecho.

En las secciones siguientes, una clase Logger que implementa la interfaz ILogger se inyecta en los constructores de clase. La clase Logger implementa una sencilla funcionalidad de registro usando el método Debug.WriteLine, y se usa para demostrar cómo pueden inyectarse servicios en representadores personalizados, efectos e implementaciones de DependencyService.

Registro de representadores personalizados

La aplicación de ejemplo incluye una página que reproduce vídeos web, cuyo origen XAML se muestra en el ejemplo siguiente:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:video="clr-namespace:FormsVideoLibrary"
             ...>
    <video:VideoPlayer Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
</ContentPage>

La vista VideoPlayer se implementa en cada plataforma mediante una clase VideoPlayerRenderer, que proporciona la funcionalidad para reproducir el vídeo. Para más información sobre estas clases de representadores personalizados, consulte Implementación de un reproductor de vídeo.

En iOS y en la Plataforma universal de Windows (UWP), las clases VideoPlayerRenderer tienen el siguiente constructor, que requiere un argumento ILogger:

public VideoPlayerRenderer(ILogger logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

En todas las plataformas, el registro de tipos en el contenedor de inyección de dependencias se realiza mediante el método RegisterTypes, que se invoca antes de que la plataforma cargue la aplicación con el método LoadApplication(new App()). El siguiente ejemplo muestra el método RegisterTypes en la plataforma iOS:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterType<FormsVideoLibrary.iOS.VideoPlayerRenderer>();
    App.BuildContainer();
}

En este ejemplo, el tipo concreto Logger se registra a través de una asignación con su tipo de interfaz, y el tipo VideoPlayerRenderer se registra directamente sin una asignación de interfaz. Cuando el usuario navegue a la página que contiene la vista VideoPlayer, se invocará al método de resolución de dependencias para resolver el tipo VideoPlayerRenderer desde el contenedor de inyección de dependencias, que también resolverá e inyectará el tipo Logger en el constructor VideoPlayerRenderer.

El constructor VideoPlayerRenderer de la plataforma Android es algo más complicado, ya que requiere un argumento Context además del ILogger:

public VideoPlayerRenderer(Context context, ILogger logger) : base(context)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

El siguiente ejemplo muestra el método RegisterTypes en la plataforma Android:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterTypeWithParameters<FormsVideoLibrary.Droid.VideoPlayerRenderer>(typeof(Android.Content.Context), this, typeof(ILogger), "logger");
    App.BuildContainer();
}

En este ejemplo, el método App.RegisterTypeWithParameters registra el VideoPlayerRenderer con el contenedor de inyección de dependencias. El método de registro asegura que la instancia MainActivity se inyectará como argumento Context, y que el tipo Logger se inyectará como argumento ILogger.

Registro de efectos

La aplicación de muestra incluye una página que usa un efecto de supervisión táctil para arrastrar instancias de BoxView por la página. El Effect se agrega al BoxView usando el siguiente código:

var boxView = new BoxView { ... };
var touchEffect = new TouchEffect();
boxView.Effects.Add(touchEffect);

La clase TouchEffect es una RoutingEffect que es implementada en cada plataforma por una clase TouchEffect que es una PlatformEffect. La clase TouchEffect de la plataforma proporciona la funcionalidad para arrastrar el BoxView por la página. Para más información sobre estas clases de efectos, consulte Invocar eventos desde efectos.

En todas las plataformas, la clase TouchEffect tiene el siguiente constructor, que requiere un argumento ILogger:

public TouchEffect(ILogger logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

En todas las plataformas, el registro de tipos en el contenedor de inyección de dependencias se realiza mediante el método RegisterTypes, que se invoca antes de que la plataforma cargue la aplicación con el método LoadApplication(new App()). El siguiente ejemplo muestra el método RegisterTypes en la plataforma Android:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterType<TouchTracking.Droid.TouchEffect>();
    App.BuildContainer();
}

En este ejemplo, el tipo concreto Logger se registra a través de una asignación con sus tipos de interfaz, y el tipo TouchEffect se registra directamente sin una asignación de interfaz. Cuando el usuario navegue a la página que contiene una instancia BoxView que tiene adjunta la TouchEffect, se invocará al método de resolución de dependencias para resolver el tipo de plataforma TouchEffect desde el contenedor de inyección de dependencias, que también resolverá e inyectará el tipo Logger en el constructor TouchEffect.

Registro de implementaciones de DependencyService

La aplicación de muestra incluye una página que usa implementaciones de DependencyService en cada plataforma para permitir al usuario elegir una foto de la biblioteca de imágenes del dispositivo. La interfaz IPhotoPicker define la funcionalidad que implementan las implementaciones de DependencyService, y se muestra en el siguiente ejemplo:

public interface IPhotoPicker
{
    Task<Stream> GetImageStreamAsync();
}

En cada proyecto de plataforma, la clase PhotoPicker implementa la interfaz de IPhotoPicker usando las API de la plataforma. Para más información sobre estos servicios de dependencia, consulte Seleccionar una foto de la biblioteca de imágenes.

En iOS y UWP, las clases PhotoPicker tienen el siguiente constructor, que requiere un argumento ILogger:

public PhotoPicker(ILogger logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

En todas las plataformas, el registro de tipos en el contenedor de inyección de dependencias se realiza mediante el método RegisterTypes, que se invoca antes de que la plataforma cargue la aplicación con el método LoadApplication(new App()). El siguiente ejemplo muestra el método RegisterTypes en UWP:

void RegisterTypes()
{
    DIContainerDemo.App.RegisterType<ILogger, Logger>();
    DIContainerDemo.App.RegisterType<IPhotoPicker, Services.UWP.PhotoPicker>();
    DIContainerDemo.App.BuildContainer();
}

En este ejemplo, el tipo concreto Logger se registra mediante una asignación con su tipo de interfaz, y el tipo PhotoPicker también se registra mediante una asignación de interfaz.

El constructor PhotoPicker de la plataforma Android es algo más complicado, ya que requiere un argumento Context además del ILogger:

public PhotoPicker(Context context, ILogger logger)
{
    _context = context ?? throw new ArgumentNullException(nameof(context));
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

El siguiente ejemplo muestra el método RegisterTypes en la plataforma Android:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterTypeWithParameters<IPhotoPicker, Services.Droid.PhotoPicker>(typeof(Android.Content.Context), this, typeof(ILogger), "logger");
    App.BuildContainer();
}

En este ejemplo, el método App.RegisterTypeWithParameters registra el PhotoPicker con el contenedor de inyección de dependencias. El método de registro asegura que la instancia MainActivity se inyectará como argumento Context, y que el tipo Logger se inyectará como argumento ILogger.

Cuando el usuario navega hasta la página de selección de fotos y elige seleccionar una foto, se ejecuta el controlador OnSelectPhotoButtonClicked:

async void OnSelectPhotoButtonClicked(object sender, EventArgs e)
{
    ...
    var photoPickerService = DependencyService.Resolve<IPhotoPicker>();
    var stream = await photoPickerService.GetImageStreamAsync();
    if (stream != null)
    {
        image.Source = ImageSource.FromStream(() => stream);
    }
    ...
}

Cuando se invoque al método DependencyService.Resolve<T>, se invocará al método de resolución de dependencias para resolver el tipo PhotoPicker del contenedor de inyección de dependencias, que también resolverá e inyectará el tipo Logger en el constructor PhotoPicker.

Nota:

El método Resolve<T> debe usarse al resolver un tipo del contenedor de inyección de dependencias de la aplicación a través del DependencyService.