articles, February 13, 2024

3 Ways to Inject Platform-Specific Dependencies in .NET MAUI Projects

Artem Korda | .NET Engineer at Coherent Solutions Poland

3 Ways to Inject Platform-Specific Dependencies in .NET MAUI Projects

Dependency injection (DI) is a technique where an object, or a client, receives all the necessary objects it depends on, such as services. In simple terms, an injector is responsible for passing a service to its client object.

Dependency injection is a specific version of a pattern known as inversion of control, where the inverted task is the process of obtaining the necessary dependencies. With dependency injection, a service class is responsible for injecting dependencies into an object at runtime.

Implementing dependency injection in .NET MAUI projects offers several benefits, such as minimizing the interdependence between classes and dependencies, creating reusable code, and enhancing testability and maintainability.

Before injecting dependencies into an object, we first need to register those dependencies. This is done by providing the container with an interface and the concrete type (class) that implements it.

The main steps of DI typically involve the following:

-Define dependency abstractions (interfaces).
-Implement the dependencies (concrete classes).
-Configure the injector (DI container).
-Inject the dependencies into the client objects.

In the following sections, I will provide step-by-step instructions for implementing DI in .NET MAUI using different approaches.

Approach 1: Shared Namespace

In this Dependency Injection (DI) approach, we leverage a shared namespace and the MAUI program for resolution. In this example, our goal is to illustrate the process of displaying a popup with a specific message on Android and iOS devices. We will follow a straightforward 4-step plan to implement DI.

1. Define dependency abstraction

namespace App.Services 
{ 
    public interface IToastHelper  
    { 
        void ShowToast(string message); 
    } 
}

2. Implement the dependencies


To implement the dependencies successfully, it’s crucial that they share a common namespace, such as “App.Platforms.Source,” and possess identical class names, like ToastHelper.

2.1 Android implementation


Add the Android implementation to the following location: App/Platforms/Android/Source/ToastHelper.cs.

namespace App.Platforms.Source 
{
    public class ToastHelper: IToastHelper
    { 
      public void ShowToast(string message) 
      {
        //Android implementation        
      }
    }
}

2.2. iOS implementation


Add the iOS implementation to the following location: App/Platforms/iOS/Source/ToastHelper.cs.

namespace App.Platforms.Source 
{
    public class ToastHelper: IToastHelper
    { 
      public void ShowToast(string message) 
      {
        //iOS implementation      
      }
    }
}

3. Configure the injector


Register your dependency in the MAUI program class:

builder.Services.AddTransient<IToastHelper, ToastHelper>();

Note: It is essential to call AddTransient() once for each type of page that utilizes a constructor to pass parameters used by the injector. Failing to do so may result in a runtime exception when attempting to navigate to that particular page._

4. Inject the dependencies


Utilize the constructor of the view to pass an interface to the view model.

View (PopupPage.Xaml.cs):

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PopupPage: ContentPage
{
  public PopupPage(IToastHelper helper)
  {
    InitializeComponent();
    BindingContext = new PopupViewModel(helper);
  }
}

ViewModel (PopupViewModel.cs):

public class PopupViewModel
{
  private readonly IToastHelper _helper;

  public Command PopUpCommand { get; }

  public PopupViewModel(IToastHelper helper)
  {
    _helper = helper;
    PopUpCommand = new Command(PopupImplementation);
  }

  private void PopupImplementation(object obj)
  {
     _helper.ShowToast(“Popup test”);
  }
}

Approach 2: Conditional Compilation


This approach utilizes conditional compilation directives to customize code for different platforms. In this example, we utilize the IDataStore interface, resolving it differently for Android and iOS. For Android, the implementation relies on SqLiteDataStore, while for iOS, we utilize the MockDataStore.

Here is the implementation of the conditional compilation approach, following the initial 4-step plan for DI.

1. Define dependency abstraction


public interface IDataStore<T>
{
   Task<T> GetItemAsync(string id); 
}

2. Implement the dependencies


2.1. Android implementation

public class SqLiteDataStore : IDataStore<Item>
{
     private SQLiteAsyncConnection _database =
         new SQLiteAsyncConnection("database.db7");



     public Task<Item> GetItemAsync(string id)
     {
       return _database.Table<Item>().FirstOrDefaultAsync(x => x.Id.Equals(id));
     }
}

2.2. iOS implementation


public class MockDataStore: IDataStore<Item>
{   
  public async Task<Item> GetItemAsync(string id)
  {
     //return mock
  }
}

3. Configure the injector


In the constructor of the App.Xaml.cs class, add the following code snippet after invoking the InitializeComponent() method:

public App()
{
     InitializeComponent();


     #if ANDROID
            DependencyService.Register<SqLiteDataStore>();
     #elif IOS
            DependencyService.Register<MockDataStore
     #endif
}

4. Inject the dependencies


In the data consumer where you intend to utilize the IDataStore, add a property:

public IDataStore<Item> DataStore => DependencyService.Get<IDataStore<Item>>();

As a result, at runtime, the injector will utilize the SqLiteDataStore implementation to resolve dependencies for Android and the MockDataStore implementation for iOS when we call the DataStore property.

Approach 3: Platform-Specific Projects

In the previous example, our implementations were located within a single MAUI project. However, if we want to incorporate platform-specific logic in a separate project targeting net7.0-android or net7.0-ios, and then dynamically add a reference to it based on the selected target platform for a MAUI project, we can achieve this using conditional compilation. Let’s delve into how this can be accomplished with logic placed in different platform-specific projects.

In this example, we’ll employ the IGetData interface to retrieve data, incorporating Android and iOS platform-specific mobile app development implementations.

1. Define dependency abstraction


Firstly, let’s augment the solution with a new shared code library project, named “App.Services,” targeted for netstandard2.1. Following that, add an interface to the new project:

namespace App.Services
{
  public interface IGetData
  {
    string GetData();
  }
}

2. Implement the dependencies


2.1. Android implementation


Add a new code library project named “AndroidLib,” targeted for net7.0-android. Include a reference to the “App.Services” project and implement the interface:

public class GetDataAndroid: IGetData
{
     public string GetData()
     {
       return "Test Android";
     }
}

2.2. iOS implementation


Add a new code library project named “iOSLib,” targeted for net7.0-ios. Include a reference to the “App.Services” project and implement the interface:

public class GetDataIOS: IGetData
{
     public string GetData()
     {
       return "Test iOS";
     }
}

3. Configure the injector


Open the .csproj file of your base MAUI project and add the following code:

<ProjectReference Include="..\MauiDemoAppClassLibrary\MauiDemoAppClassLibrary.csproj" />


<!-- Android -->

<ItemGroup Condition="$(TargetFramework.Contains('-android')) != false ">
    <ProjectReference Include="..\AndroidLib\AndroidLib.csproj" />
</ItemGroup>


<!-- iOS -->

 <ItemGroup Condition="$(TargetFramework.Contains('-ios')) != false">
     <ProjectReference Include="..\iOSLib\iOSLib.csproj" />
</ItemGroup>

In the App.Xaml.cs file, incorporate the following code into the usings section:

#if ANDROID
      using  AndroidLib;
#elif IOS 
      using iOSLib;
#endif

In the App.Xaml.cs file, add the following code within the constructor after calling the InitializeComponent() method:

#if ANDROID
      DependencyService.Register<GetDataAndroid>();
#elif IOS
      DependencyService.Register<GetDataIOS>();
#endif

4. Inject the dependencies


In the class where you require the IGetData implementation, add a new property:

public IGetData GetData => DependencyService.Get<IGetData>()

Now, our dependencies will automatically link to platform-specific implementations located in their respective platform-targeted projects.

In the article, we’ve briefly explored the concept of dependency injection, highlighted its benefits, and walked through the main steps on how to implement it in .NET MAUI projects.

I hope this guide was helpful. Happy coding!

Share article

Get a Free Consultation with Our Experts!

Simply fill out our contact form below, and we will reach out to you within 1 business day to schedule a free 1-hour consultation covering platform selection, budgeting, and project timelines.

This field is required. Maximum 100 characters.
This field is required. Maximum 100 characters.
This field is required.
Only digits are applicable.
Maximum 2000 characters.
* Required field

An error occurred sending your message.
Try again or contact us via webinforequest@coherentsolutions.com.

Message sent!

Here's what happens next:

  1. Our sales rep will contact you within 1 day to discuss your case in more detail.
  2. Next, we will arrange a free 1-hour consultation with our experts on platform selection, budgeting, and timelines.
  3. After that, we’ll need 1-2 weeks to prepare a proposal, covering solutions, team requirements, cost & time estimates.
  4. Once approved, we will launch your project within 1-2 weeks.