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
.