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!