This chapter will not teach you everything there is to know about inversion of control (IoC) and dependency injection, as there are numerous resources available that strictly focus on these topics alone. Instead, this chapter will focus on how these patterns apply to mobile development and, more specifically, how to implement them in a Xamarin.Forms mobile app.
The following is a quick look at what we'll cover in this chapter:
DependencyService
We'll get started by looking into why and how dependency injection plays an important role in mobile app development.
In software development, IoC and dependency injection solve many problems. In the world of mobile development, particularly multi-platform mobile development, they provide a great pattern to handle platform- and device-specific code.
One of the most important aspects of multi-platform mobile development is the idea of sharing code. Not only does development become easier and quicker when code can be shared across apps and platforms, but so does maintenance, management, feature parity, and so on. However, there are always parts of an app's code base that simply cannot be shared due to its strict tie-in with the platform's APIs. In most cases, an app's user interface represents a large portion of this non-sharable code. It is because of this that the MVVM pattern makes so much sense in multi-platform mobile development—it forces the separation of user interface code (Views) into individual, platform-specific libraries, making it easy to then compartmentalize the rest of the code (ViewModels and Models) into a single, shareable library.
However, what if the code in the shared ViewModels needs to access the device's physical geolocation, or leverage the device's camera to take a photo? Since the ViewModels exist in a single platform-agnostic library, they can't call the platform-specific APIs. This is where dependency injection saves the day.
In addition to providing the core building blocks for the MVVM pattern, Xamarin.Forms also includes a very basic service that handles dependency registration and resolution, called the DependencyService
. We actually used this service in the previous chapter to register and resolve our custom navigation service. Like many of the services and components built into the Xamarin.Forms toolkit, DependencyService
is designed to help get developers up and running quickly by providing an easy-to-use basic implementation. It is in no way the only way of handling dependencies in a Xamarin.Forms mobile app and, in most complex apps, you will quickly outgrow the capabilities of the Xamarin.Forms DependencyService
. For example, the Xamarin.Forms DependencyService
doesn't provide a way of doing constructor injection.
There are several third-party alternatives to the DependencyService
that allow much greater flexibility, such as Autofac, TinyIoC, Ninject, and Unity. Each of these libraries are open source and, in most cases, community maintained. They all implement the patterns in slightly different ways and offer different benefits depending on the architecture of your app.
In the next couple of sections, we will build two new platform-specific services, and use the Ninject library to register and use them in our TripLog app. We will also update the navigation service from Chapter 3, Navigation, to be registered in Ninject, instead of the Xamarin.Forms DependencyService
.
We have already created a service to handle navigation in the previous chapter. That custom navigation service specification was provided by the INavService
interface and there is a property of that interface type in the BaseViewModel
so that a concrete implementation of the service can be provided to the ViewModels as needed.
The benefit of using an interface to define platform-specific or third-party dependency services is that it can be used in an agnostic way in the ViewModels, and the concrete implementations can be provided via dependency injection. Those concrete implementations can be actual services, or even mocked services for unit testing the ViewModels, as we'll see in Chapter 8, Testing.
In addition to navigation, there are a couple of other platform-specific services our TripLog app could use to enrich its data and experience. In this section, we will create a location service that allows us to get specific geocoordinates from the device. The actual platform-specific implementation of the location service is fairly trivial, and there are tons of resources on how to do this. We will create a basic implementation without going too deep, so that we can keep the focus on how we leverage it as a dependency in a Xamarin.Forms architecture.
Similar to the approach we took for the navigation service, we will first start out by creating an interface for the location service, and then create the actual platform-specific implementations.
The first step to allowing our app to take advantage of the device's geolocation capabilities, is to provide an interface in the core library that can be used by the ViewModels in a device- and platform-agnostic manner. When receiving the geolocation back from a device, each platform could potentially provide coordinates in a platform-specific data structure. However, each structure will ultimately provide two double
values representing the coordinate's latitude and longitude.
There are a couple of ways to ensure that the results are returned in a platform-agnostic manner, which we will need since we are working in a non-platform-specific library.
One way to ensure this is to pass the values back via a callback method. Another approach we will be employing is to use a custom object, which we will define in our Models
namespace, as shown in the following steps:
GeoCoords
in the Models
folder of the core library.double
properties to the GeoCoords
class named Latitude
and Longitude
:public class GeoCoords
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
ILocationService
in the Services
folder of the core library. The interface should have one async
method, which returns Task<GeoCoords>
:public interface ILocationService
{
Task<GeoCoords> GetGeoCoordinatesAsync();
}
Now that we have an interface that defines our location service, we can use it in the core project of our TripLog app.
The main place we will need to capture location in the app is on the New Entry Page, so coordinates can be attached to log entries when they are added. Since we want to keep our app logic separate from the user interface, we will use the location service in the new entry page's ViewModel, and not on the Page itself.
In order to use the ILocationService
interface in the NewEntryViewModel
, perform the following steps:
NewEntryViewModel
to hold an instance of the location service:public class NewEntryViewModel : BaseValidationViewModel
{
readonly ILocationService _locService;
// ...
}
NewEntryViewModel
constructor to take an ILocationService
instance, and set its read-only ILocationService
property:public NewEntryViewModel(INavService navService, ILocationService locService)
: base(navService)
{
_locService = locService;
Date = DateTime.Today;
Rating = 1;
}
NewEntryViewModel
Init()
method to use the location service to set the Latitude
and Longitude
double properties:public override async void Init()
{
try
{
var coords = await _locService.GetGeoCoordinatesAsync();
Latitude = coords.Latitude;
Longitude = coords.Longitude;
}
catch (Exception)
{
// TODO: handle exceptions from location service
}
}
Notice how we can completely work with the location service in the ViewModel, even though we haven't actually written the platform-specific implementations. Although, if we were to run the app, we would get a runtime error because the implementation doesn't actually exist, but it's useful to be able to work with the service through abstraction to fully build out and test the ViewModel.
Now that we have created an interface for our location service and updated the ViewModel, we need to create the concrete platform-specific implementations. Create the location service implementations using Xamarin.Essentials as follows:
NOTE
The following steps use the Xamarin.Essentials library, which is available on NuGet. The latest versions of Visual Studio include this package in the Xamarin.Forms templates so it is likely you already have the library added to your projects. If you don't, add the Xamarin.Essentials NuGet package to the core project and each of the platform projects before proceeding.
TripLog.iOS
project named Services
.Services
folder named LocationService
that implements the ILocationService
interface we created earlier in the chapter:public class LocationService : ILocationService
{
public async Task<GeoCoords> GetGeoCoordinatesAsync()
{
var location = await Xamarin.Essentials.Geolocation.GetLocationAsync();
return new GeoCoords
{
Latitude = location.Latitude,
Longitude = location.Longitude
};
}
}
Info.plist
file by adding a new entry to request access to the device's location services. For example, add the Privacy - Location When In Use Usage Description
property along with a reason explaining why or how the device's location will be used.TripLog.Android
project named Services
.Services
folder named LocationService
that implements the ILocationService
interface for Android:public class LocationService : ILocationService
{
public async Task<GeoCoords> GetGeoCoordinatesAsync()
{
var location = await Xamarin.Essentials.Geolocation.GetLocationAsync();
return new GeoCoords
{
Latitude = location.Latitude,
Longitude = location.Longitude
};
}
}
AndroidManifest.xml
file to require ACCESS_COARSE_LOCATION
and/or ACCESS_FINE_LOCATION
permissions.These are extremely over-simplified location service implementations that simply leverage the Xamarin.Essentials library and its geolocation API. Most real-world scenarios will require more logic; however, for the purposes of demonstrating platform-specific service dependency injection, this implementation will suffice.
Xamarin.Essentials is an open source library created by the Xamarin team at Microsoft. The library exposes lots of common native APIs in a single cross-platform package. While the package is cross-platform and could easily be called directly from ViewModel code, it is still a good idea to abstract it out into a service as we've done here. This keeps the implementation details in a single place and also continues to ensure your ViewModels remain testable. For more details on the Xamarin.Essentials library, visit www.github.com/xamarin/essentials and docs.microsoft.com/en-us/xamarin/essentials.
Now that we have created a platform-dependent service, it is time to register it into an IoC container so that we can use it throughout the rest of the code. In the next section, we will use Ninject to create registrations between both our location service interface and the actual platform-specific implementations. We will also update the custom navigation service that we created in Chapter 3, Navigation, to use Ninject in place of the default Xamarin.Forms DependencyService
.
As mentioned earlier, each dependency injection library implements the pattern slightly differently. In this section, we will use Ninject to start adding dependency injection capabilities to our TripLog app. Ninject allows you to create modules that are responsible for adding services to the IoC container.
The modules are then added to a Kernel that is used to resolve the services in other areas of the app.
You can create a single Ninject module or many, depending on how your app is structured and how you want to organize your services. For the TripLog app, we will have a Ninject module in each platform project, which is responsible for registering that platform's specific service implementations. We will also create a Ninject module in the core library, which will be responsible for registering dependencies that live in the core library, such as ViewModels and data access services, which we will add later in Chapter 6, API Data Access, when we start working with live data.
We will start by creating Ninject modules in each of the platform projects, which will be responsible for registering their respective platform's specific service implementations, as shown in the following steps:
Portable.Ninject
NuGet package to each of the platform-specific projects.TripLog.iOS
project named Modules
.Modules
folder named TripLogPlatformModule
that inherits from Ninject.Modules.NinjectModule
:public class TripLogPlatformModule : NinjectModule
{
// ...
}
Load()
method of the NinjectModule
class and use the Ninject Bind()
method to register the iOS-specific implementation of ILocationService
as a singleton:public class TripLogPlatformModule : NinjectModule
{
public override void Load()
{
Bind<ILocationService>()
.To<LocationService>()
.InSingletonScope();
}
}
TripLog.Android
project named Modules
, then create a new class named TripLogPlatformModule
within it that inherits from Ninject.Modules.NinjectModule
:public class TripLogPlatformModule : NinjectModule
{
// ...
}
Load()
method of the NinjectModule
class and use the Ninject Bind()
method to register the Android-specific implementation of ILocationService
as a singleton:public class TripLogPlatformModule : NinjectModule
{
public override void Load()
{
Bind<ILocationService>()
.To<LocationService>()
.InSingletonScope();
}
}
We now have an IoC container that can hold and resolve all of our dependencies. In the next section, we will register our ViewModels in the IoC container, like we just did with our location service.
We can also use our IoC container to hold our ViewModels. It is a slightly different model than the one used to register the concrete implementations of our service interfaces—instead of mapping them to an interface, we will simply register them to themselves. Since our ViewModels are in our core library, we will create another Ninject module in the core library that will register them, as shown in the following steps:
Portable.Ninject
NuGet package to the core project.Modules
.Modules
folder named TripLogCoreModule
that inherits from Ninject.Modules.NinjectModule
:public class TripLogCoreModule : NinjectModule
{
// ...
}
Load()
method of the NinjectModule
class, and use the Ninject Bind()
method to register each of the ViewModels:public class TripLogCoreModule : NinjectModule
{
public override void Load()
{
// ViewModels
Bind<MainViewModel>().ToSelf();
Bind<DetailViewModel>().ToSelf();
Bind<NewEntryViewModel>().ToSelf();
}
}
With our location service and ViewModels all registered in the IoC container, the only remaining dependency to register is our navigation service, which we will accomplish in the next section.
In the previous chapter, we created a custom navigation service and used the Xamarin.Forms DependencyService
to register and resolve the navigation service. Now that we have introduced Ninject, we can swap Xamarin.Forms DependencyService
out for a Ninject module instead, in order to register the navigation service so that it can be resolved and used just like our location service and ViewModels:
assembly
attribute that was originally added above the class's namespace:// Remove assembly attribute
// [assembly: Dependency(typeof(XamarinFormsNavService))]
public class XamarinFormsNavService : INavService
{
// ...
}
We originally instantiated the navigation service and registered view mappings within the core App
class. We can now move all of that logic into a new Ninject module whose overridden Load
method will handle creating the service, creating the view mappings, and then registering the service into the IoC container.
Modules
folder named TripLogNavModule
that inherits from Ninject.Modules.NinjectModule
:public class TripLogNavModule : NinjectModule
{
// ...
}
Load()
method of the NinjectModule
class to instantiate a new XamarinFormsNavService
object:public class TripLogNavModule : NinjectModule
{
public override void Load()
{
var navService = new XamarinFormsNavService();
}
}
App
class and place them in the TripLogNavModule.Load()
override method:public override void Load()
{
var navService = new XamarinFormsNavService();
// Register view mappings
navService.RegisterViewMapping(typeof(MainViewModel), typeof(MainPage));
navService.RegisterViewMapping(typeof(DetailViewModel), typeof(DetailPage));
navService.RegisterViewMapping(typeof(NewEntryViewModel), typeof(NewEntryPage));
}
TripLogNavModule.Load()
override method to use the Ninject Bind()
method to register the XamarinFormsNavService
as a singleton:public override void Load()
{
var navService = new XamarinFormsNavService();
// Register view mappings
navService.RegisterViewMapping(typeof(MainViewModel), typeof(MainPage));
navService.RegisterViewMapping(typeof(DetailViewModel), typeof(DetailPage));
navService.RegisterViewMapping(typeof(NewEntryViewModel), typeof(NewEntryPage));
Bind<INavService>()
.ToMethod(x => navService)
.InSingletonScope();
}
Platform-specific services are good candidates for singleton objects because, typically, we do not want to create new instances of the services each time we reference them. ViewModels can also be singletons, but typically should not be as they should usually start with a fresh state each time a Page is visited.
Now that our platform services, navigation service, and ViewModels have all been registered in the IoC container, we will need to add the Ninject modules that we created to the Ninject Kernel. We will do this in our main Xamarin.Forms.Application
class in the next section.
In order to get our platform modules into the App
class, which is in our core library, we simply update the App
constructor to take in
INinjectModule
parameters. Then, each platform-specific project will be responsible for passing in its respective Ninject module when it loads the App
at startup, as shown in the following steps:
App
constructor to take in INinjectModule
parameters:public App(params INinjectModule[] platformModules)
{
// ...
}
IKernel
property named Kernel
to the App
class:public partial class App : Application
{
public IKernel Kernel { get; set; }
// ...
}
App
constructor. In the previous section, we moved the bulk of the existing App
constructor logic into the navigation Ninject module. Now, the App
constructor should only be responsible for creating the main Page and initializing the Ninject Kernel with the various modules that we have created:public partial class App : Application
{
public IKernel Kernel { get; set; }
public App(params INinjectModule[] platformModules)
{
// ...
// Register core services
Kernel = new StandardKernel(
new TripLogCoreModule(),
new TripLogNavModule());
// Register platform specific services
Kernel.Load(platformModules);
SetMainPage();
}
void SetMainPage()
{
var mainPage = new NavigationPage(new MainPage())
{
BindingContext = Kernel.Get<MainViewModel>()
};
var navService = Kernel.Get<INavService>() as XamarinFormsNavService;
navService.XamarinFormsNav = mainPage.Navigation;
MainPage = mainPage;
}
// ...
}
Notice how we get an instance of the MainViewModel
from the IoC container and use it to set the BindingContext
(ViewModel) of the MainPage
. In the next section, we'll update the navigation service to do this every time we navigate to the other Pages in the app.
App
instantiation in the AppDelegate
class of our iOS project to pass in a new instance of TripLog.iOS.Modules.TripLogPlatformModule
:LoadApplication(new App(new TripLogPlatformModule()));
MainActivity
class of the Android project to pass in an Android platform-specific Ninject module instance to the App
constructor.Now that the app is updated with an IoC container for resolving dependencies, we can update our navigation service to automatically instantiate ViewModels when we navigate to them.
Currently, in the TripLog app, each Page is responsible for creating its own ViewModel instance. However, because we provide a ViewModel's dependencies through its constructor, we would have to manually resolve each dependency within the Page
class and then pass them into the ViewModel instantiation. Not only is this going to be messy code, it is also difficult to maintain, and doesn't promote loose coupling. Since we have registered our ViewModels in our IoC container, we can completely remove the ViewModel instantiations from our Pages and set our navigation service up to handle resolving the ViewModels from the IoC container, automatically supplying their dependencies through constructor injection, as shown in the following steps:
BindingContext
property to a new ViewModel instance.NavigateToView()
private method in the XamarinFormsNavService
to handle setting the ViewModels of the Pages automatically as they are navigated to. After the Page (View) is created using the Invoke()
method, simply get a new instance of the specified ViewModel and assign it to the BindingContext
property of the Page:async Task NavigateToView(Type viewModelType)
{
// ...
var view = constructor.Invoke(null) as Page;
var vm = ((App)Application.Current)
.Kernel
.GetService(viewModelType);
view.BindingContext = vm;
await XamarinFormsNav.PushAsync(view, true);
}
After making this small change, the Pages are no longer responsible for instantiating their own ViewModel instances. Instead, when a Page is navigated to, the ViewModel for it is retrieved from the IoC container and set as the Page's BindingContext
. By doing this, the ViewModel's dependencies are automatically resolved and injected into the ViewModel's constructor. This is much cleaner and easier to maintain than manually instantiating each dependency and passing it into the ViewModel's constructor.
In this chapter, we explored the benefits of IoC and the dependency injection pattern in mobile development, and how they help solve the problem of working with platform-specific APIs from shared code. We also made some significant improvements to our Xamarin.Forms TripLog app by adding a new platform-specific service and introducing the Ninject dependency injection library, resulting in a code base that is more flexible and easier to test.
In the next chapter, we will shift our focus back to the View layer of our app and enhance the user experience with some customizations, and leverage some of the platform capabilities we are now showcasing through our ViewModels.