4

Platform-Specific Services and Dependency Injection

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:

  • The need for dependency injection in multi-platform mobile app development
  • Implementing IoC and dependency injection using a third-party library in place of Xamarin.Forms DependencyService
  • Creating, injecting, and using platform-specific services
  • Updating our TripLog app to use platform-specific services through dependency injection

We'll get started by looking into why and how dependency injection plays an important role in mobile app development.

Inversion of control and dependency injection in mobile apps

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.

Xamarin.Forms DependencyService versus third-party alternatives

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.

Creating and using platform-specific services

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.

Creating a location service

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:

  1. Create a new class named GeoCoords in the Models folder of the core library.
  2. Add two double properties to the GeoCoords class named Latitude and Longitude:
    public class GeoCoords
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
    }
    
  1. Create a new interface named 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.

Using the location service on the New Entry Page

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:

  1. First, add a read-only property to the NewEntryViewModel to hold an instance of the location service:
    public class NewEntryViewModel : BaseValidationViewModel
    {
        readonly ILocationService _locService;
        // ...
    }
    
  2. Next, update the 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;
    }
    
  3. Finally, update the 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.

Adding the location service implementation

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.

  1. First, create a new folder in the TripLog.iOS project named Services.
  2. Next, create a new class file in the 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
            };
        }
    }
    
  3. Next, update the iOS app's 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.
  4. Next, create a new folder in the TripLog.Android project named Services.
  5. Next, create a new class file in the 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
            };
        }
    }
    
  1. Next, update the Android app's 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.

Registering dependencies

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.

Registering the platform-service implementations

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:

  1. Add the Portable.Ninject NuGet package to each of the platform-specific projects.
  2. Next, create a new folder in the TripLog.iOS project named Modules.
  3. Create a new class in the Modules folder named TripLogPlatformModule that inherits from Ninject.Modules.NinjectModule:
    public class TripLogPlatformModule : NinjectModule
    {
        // ...
    }
    
  4. Override the 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();
        }
    }
    
  5. Next, create a folder in the TripLog.Android project named Modules, then create a new class named TripLogPlatformModule within it that inherits from Ninject.Modules.NinjectModule:
    public class TripLogPlatformModule : NinjectModule
    {
        // ...
    }
    
  1. Finally, override the 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.

Registering the ViewModels

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:

  1. Add the Portable.Ninject NuGet package to the core project.
  2. Create a new folder in the core project named Modules.
  3. Create a new class in the core project Modules folder named TripLogCoreModule that inherits from Ninject.Modules.NinjectModule:
    public class TripLogCoreModule : NinjectModule
    {
        // ...
    }
    
  1. Override the 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.

Registering the navigation service

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:

  1. First, remove the 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.

  2. Create a new class in the core project's Modules folder named TripLogNavModule that inherits from Ninject.Modules.NinjectModule:
    public class TripLogNavModule : NinjectModule
    {
        // ...
    }
    
  3. Override the Load() method of the NinjectModule class to instantiate a new XamarinFormsNavService object:
    public class TripLogNavModule : NinjectModule
    {
        public override void Load()
        {
            var navService = new XamarinFormsNavService();
        }
    }
    
  4. Remove the ViewModel-to-View mappings from the 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));
    }
    
  5. Finally, update the 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.

Updating the TripLog app

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:

  1. Update the App constructor to take in INinjectModule parameters:
    public App(params INinjectModule[] platformModules)
    {
        // ...
    }
    
  2. Next, add a public IKernel property named Kernel to the App class:
    public partial class App : Application
    {
        public IKernel Kernel { get; set; }
        // ...
    }
    
  3. Next, update the body of the 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.

  4. Next, we need to update the 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()));
    
  1. Finally, repeat the previous step in the 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.

Updating the navigation service to handle ViewModel creation and dependency injection

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:

  1. First, remove the code from the constructor of each Page that sets its BindingContext property to a new ViewModel instance.
  2. Next, update the 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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.255.162