DependencyResolver en Xamarin Forms

¡Hola!
Hace unos días echamos un vistazo al Dependency Service de Xamarin Forms (en esta entrada) y cómo podíamos ampliar un poco sus capacidades para sacarle un poco más de jugo (código). Pues a raíz de eso, me di cuenta que en las nuevas versiones de Xamarin Forms (en concreto a partir de la 3.1) se ha añadido una nueva forma de resolver las instancias (aquí el issue en GitHub), junto con una nueva clase estática llamada DependencyResolver.
La nueva forma que tenemos para resolver las instancias a través del Dependency Service tiene la siguiente pinta:

1
2
 
    public static T Resolve<T>(DependencyFetchTarget fallbackFetchTarget = DependencyFetchTarget.GlobalInstance) where T : class

Es casi igual (salvo por el nombre del parámetro) a la forma que utilizamos habitualmente:

1
2
 
    public static T Get<T>(DependencyFetchTarget fetchTarget = DependencyFetchTarget.GlobalInstance) where T : class

Entonces, ¿qué tiene de diferente una de otra? Pues si no hacemos nada diferente a lo que hacemos habitualmente para especificar cómo resolver las dependencias, nada. Vamos a ver que, si registramos las dependencias y solicitamos una instancia indicando que sea DependencyFetchTarget.GlobalInstance, nos devolverá la misma instancia con el método Resolve que con Get:

1
2
3
4
5
6
    DependencyService.Register<IService,Service>();
 
    var serviceResolvedWithGetMethod = DependencyService.Get<IService>(DependencyFetchTarget.GlobalInstance);
    var serviceResolvedWithResolveMethod = DependencyService.Resolve<IService>(DependencyFetchTarget.GlobalInstance);
 
    Assert.Equal(serviceResolvedWithGetMethod, serviceResolvedWithResolveMethod);

Vamos a echar un vistazo al nuevo método Resolve:

1
2
3
4
5
6
    public static T Resolve<T>(DependencyFetchTarget fallbackFetchTarget = DependencyFetchTarget.GlobalInstance) where T : class
    {
        var result = DependencyResolver.Resolve(typeof(T)) as T;
 
	return result ?? Get<T>(fallbackFetchTarget);
    }

Como vemos, es normal que nos estuviese devolviendo la misma instancia en el ejemplo anterior. Al final lo que está haciendo es solicitando la instancia al DependencyResolver y si no la encuentra (como es nuestro caso ya que no hemos configurada nada todavía), utiliza el método Get habitual del DependencyService. Con esto también nos está diciendo que el parámetro que pasamos de tipo DependencyFetchTarget sólo se utiliza en caso de que no pueda resolver la instancia con el DependencyResolver y la resuelva con el método habitual Get.
Ahora que sabemos esto, el siguiente paso es ver qué podemos configurar en el DependencyResolver, que ya adelanto que, aunque no es mucho, es bastante potente. En concreto sólo tenemos acceso a dos métodos, realmente es un sólo método con dos sobrecargas:

1
2
3
 
    public static void ResolveUsing(Func<Type, object[], object> resolver)	
    public static void ResolveUsing(Func<Type, object> resolver)

¿Para qué sirven estos dos métodos? Pues muy sencillo, nos dan la posibilidad de configurar una función que reciba un tipo (Type) o un tipo y un array de objects (object[]) y devuelva un objeto. Realmente es un poco más sencillo, puesto que en el caso del DependencyService al llamar al método DependencyResolver.Resolve sólo le pasa un parámetro (el tipo), por lo que sólo vamos a utilizar el segundo método ResolveUsing que hemos visto:

1
2
 
    public static void ResolveUsing(Func<Type, object> resolver)

Dicho esto, vamos a crear la implementación tonta de esta función:

1
2
3
4
5
6
7
    private object DummyResolver(Type type)
    {
        if(type == typeof(IService))
            return new Service();
 
        return null;
    }

El siguiente paso será configurar el DependencyResolver para que utilice nuestro DummyResolver y veremos que cuando resolvamos nuestras dependencias con el método Resolve utilizará nuestro DummyResolver:

1
2
3
    Xamarin.Forms.Internals.DependencyResolver.ResolveUsing(DummyResolver);
    DependencyService.Register<IService,Service>();
    var service = DependencyService.Resolve<IService>(DependencyFetchTarget.GlobalInstance);

Y ya está, realmente no tiene mucho más que explicar. Hemos visto un ejemplo muy tonto, pero esto nos puede interesar bastante cuando la construcción de nuestras dependencias tiene, por ejemplo, parámetros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    public interface IService
    {
 
    }
 
    public class Service : IService
    {
        private readonly int value1;
        private readonly string value2;
 
        public Service(int value1, string value2)
        {
            this.value1 = value1;
            this.value2 = value2;
        }
    }

Ahora vamos a modificar nuestro DummyResolver:

1
2
3
4
5
6
    private object DummyResolver(Type type)
    {
        if(type == typeof(IService))
            return new Service(2, "other parameter!");
        return null;
    }

Si ahora intentamos resolver la dependencia no nos lanzará ninguna excepción. En caso de que la intentásemos resolver con el método DependencyService.Get (el tradicional) sí que va a lanzar excepción, pues va a utilizar la forma habitual de resolver la instancia (Activator.CreateInstance). Esto nos viene a decir que, si vamos a añadir un resolver para configurar alguna instancia, quizá deberíamos plantearnos utilizar en todo el proyecto el método Resolve para evitar problemas, o más que problemas, ir a buscar cada vez que resolvamos una dependencia si está gestionada en nuestro método resolver o no. También hay que tener en cuenta otra cosa, y es que el utilizar siempre el método Resolve no nos libra del todo de saber cómo se está resolviendo la instancia, ya que como vimos, en caso de que la creación de la instancia se haga a través de nuestro Resolver, el parámetro DependencyFetchTarget fallbackFetchTarget no se va a utilizar, es decir, somos nosotros los encargados de gestionar el ciclo de vida de la instancia desde nuestro Resolver y no tenemos acceso a ese valor. Un ejemplo muy claro de esto es el que acabamos de ver; yo estaba solicitando al método Resolve una instancia de IService indicando que fuese una instancia global (GlobalInstance) y en mi resolver estoy haciendo new Service(…), peliagudo asunto… Es decir, que si empezamos a utilizar el método Resolve, nos deberíamos olvidar un poco de ese parámetro porque nos puede dar bastantes problemas.
Algo que puede ser bastante interesante es utilizar ese método Resolve para añadir una capa por encima de nuestro DependencyService. Podemos crear nuestra clase contenedor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public class Container
    {
        private static Container instance;
        public static Container Instance
        {
            get { return instance ?? (instance = new Container()); }
        }
 
 
        private readonly IDictionary<Type,Func<object>> resolvers = 
            new Dictionary<Type, Func<object>>();
 
        public object Resolver(Type type)
        {
            return resolvers.ContainsKey(type) ? resolvers[type]?.Invoke() : null;
        }
 
        public void Register<T>(Func<object> resolver)
        {
            resolvers.Add(typeof(T),resolver);
        }
    }

Ahora tendremos que indicar a nuestro DependencyResolver que su resolver sea nuestro contenedor:

1
2
 
    Xamarin.Forms.Internals.DependencyResolver.ResolveUsing(Container.Instance.Resolver);

y configuraremos la creación de los objetos desde nuestro contenedor:

1
2
 
    Container.Instance.Register<IService>(() => new Service(1, "value"));

Y por último, para resolver las dependencias, igual que lo hacíamos antes:

1
2
 
    var service = DependencyService.Resolve<IService>();

Claro, ahora mismo tenemos el problema de que no estamos pudiendo controlar el tiempo de vida de las instancias. Podemos añadir un par de métodos al contenedor para indicar cómo queremos que sean nuestras instancias, globales o siempre nuevas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public class Container
    {
        private static Container instance;
        public static Container Instance
        {
            get { return instance ?? (instance = new Container()); }
        }
 
 
        private readonly IDictionary<Type, IInstanceScope> resolvers =
            new Dictionary<Type, IInstanceScope>();
 
        public object Resolver(Type type)
        {
            return resolvers.ContainsKey(type) ? resolvers[type].GetOrCreate() : null;
        }
 
        public void RegisterAsTransient<T>(Func<object> resolver)
        {
            resolvers.Add(typeof(T), new Transient(resolver));
        }
        public void RegisterAsSingleton<T>(Func<object> resolver)
        {
            resolvers.Add(typeof(T), new Singleton(resolver));
        }
 
    }

Y el resto del código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    public interface IInstanceScope
    {
        object GetOrCreate();
    }
    public class Singleton : IInstanceScope
    {
        private readonly Func<object> resolver;
        private object instance;
 
        public Singleton(Func<object> resolver)
        {
            this.resolver = resolver;
        }
 
        public object GetOrCreate()
        {
            return instance ?? (instance = resolver.Invoke());
        }
    }
 
    public class Transient : IInstanceScope
    {
        private readonly Func<object> resolver;
 
        public Transient(Func<object> resolver)
        {
            this.resolver = resolver;
        }
        public object GetOrCreate()
        {
            return resolver.Invoke();
        }
    }

Hecho esto, ya podríamos registrar como Singleton:

1
2
3
4
5
6
    Container.Instance.RegisterAsSingleton<IService>(() => new Service(1, "value"));
 
    var service = DependencyService.Resolve<IService>();
    var service1 = DependencyService.Resolve<IService>();
 
    Assert.Equal(service,service1);

o como Transient:

1
2
3
4
5
6
    Container.Instance.RegisterAsTransient<IService>(() => new Service(1, "value"));
 
    var service = DependencyService.Resolve<IService>();
    var service1 = DependencyService.Resolve<IService>();
 
    Assert.NotEqual(service,service1);

Con esto ya estaríamos sacando bastante partido al DependencyResolver.
Incluso podríamos darle una vuelta más, y conectar el DependencyResolver a algún contenedor como Autofac… si es que realmente quieres hacer eso..

Conclusiones

Hemos podido ver que algo bastante simple como exponer una función que podemos interceptar a la hora de resolver las dependencias nos ha dado mucha flexibilidad para dar algo más de vida al DepedencyService, pero como contras tiene que al final los consumidores del DependencyService tienen que tener mucho cuidado al resolver las dependencias para no llevarse sorpresas, y si mezclamos la utilización del método DependencyService.Get y DependencyService.Resolve, junto con DependencyFetchTarget, puede que haya problemas principalmente relacionados con el ciclo de vida de las instancias de los que no nos demos cuenta en un primer momento.

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *