Compartilhar via


Resolução de dependência em Xamarin.Forms

Este artigo explica como injetar um método de resolução de dependência para Xamarin.Forms que o contêiner de injeção de dependência de um aplicativo tenha controle sobre a criação e o tempo de vida de renderizadores personalizados, efeitos e implementações de DependencyService.

No contexto de um Xamarin.Forms aplicativo que usa o padrão MVVM (Model-View-ViewModel), um contêiner de injeção de dependência pode ser usado para registrar e resolver modelos de exibição e para registrar serviços e injetá-los em modelos de exibição. Durante a criação do modelo de exibição, o contêiner injeta todas as dependências necessárias. Se essas dependências não tiverem sido criadas, o contêiner criará e resolverá as dependências primeiro. Para obter mais informações sobre injeção de dependência, incluindo exemplos de injeção de dependências em modelos de exibição, consulte Injeção de dependência.

O controle sobre a criação e o tempo de vida de tipos em projetos de plataforma é tradicionalmente executado pelo Xamarin.Forms, que usa o Activator.CreateInstance método para criar instâncias de renderizadores, efeitos e DependencyService implementações personalizados. Infelizmente, isso limita o controle do desenvolvedor sobre a criação e o tempo de vida desses tipos e a capacidade de injetar dependências neles. Esse comportamento pode ser alterado injetando um método de resolução de dependência que Xamarin.Forms controla como os tipos serão criados – pelo contêiner de injeção de dependência do aplicativo ou pelo Xamarin.Forms. No entanto, observe que não há necessidade de injetar um método de resolução de dependência no Xamarin.Forms. Xamarin.Forms continuará a criar e gerenciar o tempo de vida dos tipos em projetos de plataforma se um método de resolução de dependência não for injetado.

Observação

Embora este artigo se concentre em injetar um método de resolução de dependência que Xamarin.Forms resolve tipos registrados usando um contêiner de injeção de dependência, também é possível injetar um método de resolução de dependência que usa métodos de fábrica para resolver tipos registrados.

Injetando um método de resolução de dependência

A DependencyResolver classe fornece a capacidade de injetar um método de resolução de dependência em Xamarin.Forms, usando o ResolveUsing método. Então, quando Xamarin.Forms precisar de uma instância de um tipo específico, o método de resolução de dependência terá a oportunidade de fornecer a instância. Se o método de resolução de dependência retornar null para um tipo solicitado, Xamarin.Forms voltará a tentar criar a própria instância de tipo usando o Activator.CreateInstance método.

O exemplo a seguir mostra como definir o método de resolução de dependência com o ResolveUsing método:

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);
        ...
    }
    ...
}

Neste exemplo, o método de resolução de dependência é definido como uma expressão lambda que usa o contêiner de injeção de dependência Autofac para resolver todos os tipos que foram registrados no contêiner. Caso contrário, null será retornado, o que resultará na Xamarin.Forms tentativa de resolver o tipo.

Observação

A API usada por um contêiner de injeção de dependência é específica para o contêiner. Os exemplos de código neste artigo usam o Autofac como um contêiner de injeção de dependência, que fornece os IContainer tipos and ContainerBuilder . Contêineres de injeção de dependência alternativos também podem ser usados, mas usariam APIs diferentes das apresentadas aqui.

Observe que não há nenhum requisito para definir o método de resolução de dependência durante a inicialização do aplicativo. Pode ser definido a qualquer momento. A única restrição é que precisa saber sobre o método de resolução de dependência no momento em que Xamarin.Forms o aplicativo tenta consumir tipos armazenados no contêiner de injeção de dependência. Portanto, se houver serviços no contêiner de injeção de dependência que o aplicativo exigirá durante a inicialização, o método de resolução de dependência precisará ser definido no início do ciclo de vida do aplicativo. Da mesma forma, se o contêiner de injeção de dependência gerenciar a criação e o tempo de vida de um determinado Effect, Xamarin.Forms precisará saber sobre o método de resolução de dependência antes de tentar criar uma exibição que use esse Effect.

Aviso

Registrar e resolver tipos com um contêiner de injeção de dependência tem um custo de desempenho devido ao uso de reflexão do contêiner para criar cada tipo, especialmente se as dependências estiverem sendo reconstruídas para cada navegação de página no aplicativo. Se houver muitas dependências ou se elas forem profundas, o custo da criação poderá aumentar significativamente.

Registrando tipos

Os tipos devem ser registrados no contêiner de injeção de dependência antes que ele possa resolvê-los por meio do método de resolução de dependência. O exemplo de código a seguir mostra os métodos de registro que o aplicativo de exemplo expõe na App classe, para o contêiner 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();
    }
    ...
}

Quando um aplicativo usa um método de resolução de dependência para resolver tipos de um contêiner, os registros de tipo normalmente são executados em projetos de plataforma. Isso permite que os projetos de plataforma registrem tipos para renderizadores, efeitos e DependencyService implementações personalizados.

Após o registro de tipo de um projeto de plataforma, o objeto deve ser criado, o IContainer que é feito chamando o BuildContainer método. Esse método invoca o ContainerBuilder método do Build Autofac na instância, que cria um novo contêiner de injeção de dependência que contém os registros que foram feitos.

Nas seções a seguir, uma Logger classe que implementa a interface é injetada ILogger em construtores de classe. A Logger classe implementa a funcionalidade de log simples usando o Debug.WriteLine método e é usada para demonstrar como os serviços podem ser injetados em renderizadores, efeitos e DependencyService implementações personalizados.

Registrando renderizadores personalizados

O aplicativo de exemplo inclui uma página que reproduz vídeos da Web, cuja origem XAML é mostrada no exemplo a seguir:

<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>

A VideoPlayer exibição é implementada em cada plataforma por uma VideoPlayerRenderer classe, que fornece a funcionalidade para reproduzir o vídeo. Para obter mais informações sobre essas classes de renderizador personalizadas, consulte Implementando um player de vídeo.

No iOS e na Plataforma Universal do Windows (UWP), as VideoPlayerRenderer classes têm o seguinte construtor, que requer um ILogger argumento:

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

Em todas as plataformas, o registro de tipo com o contêiner de injeção de dependência é executado pelo método, que é invocado RegisterTypes antes de a plataforma carregar o aplicativo com o LoadApplication(new App()) método. O exemplo a seguir mostra o RegisterTypes método na plataforma iOS:

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

Neste exemplo, o Logger tipo concreto é registrado por meio de um mapeamento em relação ao seu tipo de interface, e o VideoPlayerRenderer tipo é registrado diretamente sem um mapeamento de interface. Quando o usuário navega até a página que contém a VideoPlayer exibição, o método de resolução de dependência será invocado para resolver o VideoPlayerRenderer tipo do contêiner de injeção de dependência, que também resolverá e injetará o Logger VideoPlayerRenderer tipo no construtor.

O VideoPlayerRenderer construtor na plataforma Android é um pouco mais complicado, pois requer um Context argumento além do ILogger argumento:

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

O exemplo a seguir mostra o RegisterTypes método na plataforma Android:

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

Neste exemplo, o App.RegisterTypeWithParameters método registra o VideoPlayerRenderer com o contêiner de injeção de dependência. O método de registro garante que a MainActivity instância será injetada como o Context argumento e que o Logger tipo será injetado como o ILogger argumento.

Registrando efeitos

O aplicativo de exemplo inclui uma página que usa um efeito de controle de toque para arrastar BoxView instâncias pela página. O Effect é adicionado ao BoxView usando o seguinte código:

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

A TouchEffect classe é implementada RoutingEffect em cada plataforma por uma TouchEffect classe que é um PlatformEffect. A classe platform TouchEffect fornece a funcionalidade para arrastar a BoxView página. Para obter mais informações sobre essas classes de efeito, consulte Invocando eventos de efeitos.

Em todas as plataformas, a TouchEffect classe tem o seguinte construtor, que requer um ILogger argumento:

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

Em todas as plataformas, o registro de tipo com o contêiner de injeção de dependência é executado pelo método, que é invocado RegisterTypes antes de a plataforma carregar o aplicativo com o LoadApplication(new App()) método. O exemplo a seguir mostra o RegisterTypes método na plataforma Android:

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

Neste exemplo, o Logger tipo concreto é registrado por meio de um mapeamento em relação ao seu tipo de interface, e o TouchEffect tipo é registrado diretamente sem um mapeamento de interface. Quando o usuário navega até a página que contém uma BoxView instância que tem o anexado TouchEffect a ela, o método de resolução de dependência será invocado para resolver o tipo de plataforma TouchEffect do contêiner de injeção de dependência, que também resolverá e injetará o Logger TouchEffect tipo no construtor.

Registrando implementações de DependencyService

O aplicativo de exemplo inclui uma página que usa DependencyService implementações em cada plataforma para permitir que o usuário escolha uma foto da biblioteca de imagens do dispositivo. A IPhotoPicker interface define a funcionalidade implementada DependencyService pelas implementações e é mostrada no exemplo a seguir:

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

Em cada projeto de plataforma, a classe implementa PhotoPicker a interface usando APIs de IPhotoPicker plataforma. Para obter mais informações sobre esses serviços de dependência, consulte Escolhendo uma foto da biblioteca de imagens.

No iOS e na UWP, as PhotoPicker classes têm o seguinte construtor, que requer um ILogger argumento:

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

Em todas as plataformas, o registro de tipo com o contêiner de injeção de dependência é executado pelo método, que é invocado RegisterTypes antes de a plataforma carregar o aplicativo com o LoadApplication(new App()) método. O exemplo a seguir mostra o RegisterTypes método na UWP:

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

Neste exemplo, o Logger tipo concreto é registrado por meio de um mapeamento em relação ao seu tipo de interface, e o PhotoPicker tipo também é registrado por meio de um mapeamento de interface.

O PhotoPicker construtor na plataforma Android é um pouco mais complicado, pois requer um Context argumento além do ILogger argumento:

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

O exemplo a seguir mostra o RegisterTypes método na plataforma Android:

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

Neste exemplo, o App.RegisterTypeWithParameters método registra o PhotoPicker com o contêiner de injeção de dependência. O método de registro garante que a MainActivity instância será injetada como o Context argumento e que o Logger tipo será injetado como o ILogger argumento.

Quando o usuário navega até a página de seleção de fotos e opta por selecionar uma foto, o OnSelectPhotoButtonClicked manipulador é executado:

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);
    }
    ...
}

Quando o DependencyService.Resolve<T> método for invocado, o método de resolução de dependência será invocado para resolver o PhotoPicker tipo do contêiner de injeção de dependência, que também resolverá e injetará o Logger PhotoPicker tipo no construtor.

Observação

O Resolve<T> método deve ser usado ao resolver um tipo do contêiner de injeção de dependência do aplicativo por meio do DependencyService.