Xamarin.Forms has revolutionized the way we think about cross-platform mobile development. By allowing developers to share up to 90% of their code across iOS and Android, it provides an efficiency that was once unthinkable. However, every mobile developer eventually hits a wall: the “Platform Gap.”
You are building a beautiful cross-platform app, and suddenly you need to access the device’s battery level, fetch the unique device ID, or trigger a platform-specific toast notification. Xamarin.Forms doesn’t have a built-in cross-platform API for every single native feature because iOS and Android handle these tasks in fundamentally different ways. This is where the DependencyService comes into play.
In this comprehensive guide, we will dive deep into the world of the Xamarin.Forms DependencyService. We will explore how it works, why it is essential for professional-grade applications, and walk through detailed, real-world examples that you can implement in your projects today. Whether you are a beginner looking to understand the basics or an intermediate developer seeking best practices, this article is your definitive resource.
What is DependencyService?
At its core, the DependencyService is a service locator that enables applications to fetch platform-specific implementations of an interface from shared code. In a Xamarin.Forms solution, you typically have a Shared Project (or .NET Standard Library) and several platform-specific projects (Android, iOS, UWP).
The Shared Project contains your UI logic and business rules. The Platform Projects contain the code that talks directly to the underlying operating system. The DependencyService acts as a bridge, allowing the Shared Project to say: “I need someone who knows how to show a toast message,” and the DependencyService finds the correct Android or iOS class to do the job.
The Three Pillars of DependencyService
To use the DependencyService, you must follow a specific pattern consisting of three distinct steps:
- The Interface: Defined in the Shared Project. This specifies what the functionality does.
- The Implementation: Created in each Platform Project. This specifies how the functionality is performed on that specific OS.
- Registration: A metadata attribute that tells Xamarin.Forms where to find the implementation.
Step 1: Defining the Interface
The first step is to define an interface in your shared code. This interface represents the capability you want to access. Let’s take the example of a Device Information Service that retrieves the battery status of the phone.
Why start with an interface? Because the Shared Project doesn’t know about Android.OS.BatteryManager or iOS UIDevice.CurrentDevice. It only knows what it needs: a string or an integer representing the battery level.
// Located in the Shared Project (e.g., MyProject.Interfaces)
using System;
namespace MyProject.Services
{
public interface IDeviceInfoService
{
// Returns the current battery percentage (0-100)
int GetBatteryLevel();
// Returns whether the device is currently charging
bool IsDeviceCharging();
}
}
Step 2: Implementing the Android Version
Now, we move to the Android-specific project. Here, we have access to the full Android SDK. We create a class that implements IDeviceInfoService using Android-specific APIs. In Android, battery information is usually retrieved via a BatteryManager or by registering a BroadcastReceiver for the BatteryChanged intent.
// Located in the Android Project
using Android.Content;
using Android.OS;
using MyProject.Droid.Services;
using MyProject.Services;
using Xamarin.Forms;
// CRITICAL: This attribute registers the implementation with DependencyService
[assembly: Dependency(typeof(DeviceInfoService_Android))]
namespace MyProject.Droid.Services
{
public class DeviceInfoService_Android : IDeviceInfoService
{
public int GetBatteryLevel()
{
// Access the Android Intent for Battery Status
var filter = new IntentFilter(Intent.ActionBatteryChanged);
var battery = Android.App.Application.Context.RegisterReceiver(null, filter);
int level = battery.GetIntExtra(BatteryManager.ExtraLevel, -1);
int scale = battery.GetIntExtra(BatteryManager.ExtraScale, -1);
return (int)Math.Floor(level * 100D / scale);
}
public bool IsDeviceCharging()
{
var filter = new IntentFilter(Intent.ActionBatteryChanged);
var battery = Android.App.Application.Context.RegisterReceiver(null, filter);
int status = battery.GetIntExtra(BatteryManager.ExtraStatus, -1);
return status == (int)BatteryStatus.Charging || status == (int)BatteryStatus.Full;
}
}
}
Step 3: Implementing the iOS Version
Similarly, in the iOS project, we implement the same interface. iOS uses UIDevice.CurrentDevice to manage hardware interactions. Note that on iOS, you must enable battery monitoring explicitly before you can read the level.
// Located in the iOS Project
using UIKit;
using MyProject.iOS.Services;
using MyProject.Services;
using Xamarin.Forms;
// CRITICAL: Register the class
[assembly: Dependency(typeof(DeviceInfoService_iOS))]
namespace MyProject.iOS.Services
{
public class DeviceInfoService_iOS : IDeviceInfoService
{
public int GetBatteryLevel()
{
// Enable monitoring
UIDevice.CurrentDevice.BatteryMonitoringEnabled = true;
// iOS returns battery level as a float from 0.0 to 1.0
float level = UIDevice.CurrentDevice.BatteryLevel;
return (int)(level * 100);
}
public bool IsDeviceCharging()
{
UIDevice.CurrentDevice.BatteryMonitoringEnabled = true;
var state = UIDevice.CurrentDevice.BatteryState;
return state == UIDeviceBatteryState.Charging || state == UIDeviceBatteryState.Full;
}
}
}
Step 4: Calling the Service from Shared Code
Now that we have defined the interface and provided implementations for both platforms, we can use it in our ViewModels or Code-Behind files within the Shared Project. We use DependencyService.Get<T>() to resolve the instance.
// Located in the Shared Project (e.g., MainPage.xaml.cs)
using MyProject.Services;
using Xamarin.Forms;
namespace MyProject
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
void OnCheckBatteryClicked(object sender, EventArgs e)
{
// Resolve the platform-specific implementation
var batteryService = DependencyService.Get<IDeviceInfoService>();
if (batteryService != null)
{
int level = batteryService.GetBatteryLevel();
bool isCharging = batteryService.IsDeviceCharging();
DisplayAlert("Battery Status",
$"Level: {level}% \nCharging: {isCharging}", "OK");
}
else
{
DisplayAlert("Error", "Could not find the battery service.", "OK");
}
}
}
}
Advanced Usage: Passing Parameters and Using Events
A common mistake is thinking DependencyService only supports simple data retrieval. In reality, you can pass complex parameters and even define events in your interfaces to create a two-way communication channel between native code and shared code.
Example: Native Toast Notifications with Custom Duration
Imagine you want to show a native “Toast” (Android) or a custom “Snackbar” (iOS) and you want to pass the message and duration from your ViewModel.
// Shared Project Interface
public interface IMessageService
{
void ShowShortMessage(string message);
void ShowLongMessage(string message);
}
On Android, the implementation is straightforward using the Android.Widget.Toast class:
[assembly: Dependency(typeof(MessageService_Droid))]
namespace MyProject.Droid
{
public class MessageService_Droid : IMessageService
{
public void ShowShortMessage(string message)
{
Android.Widget.Toast.MakeText(Android.App.Application.Context, message, Android.Widget.ToastLength.Short).Show();
}
public void ShowLongMessage(string message)
{
Android.Widget.Toast.MakeText(Android.App.Application.Context, message, Android.Widget.ToastLength.Long).Show();
}
}
}
Common Pitfalls and How to Fix Them
Even experienced developers run into issues with the DependencyService. Here are the most frequent errors and how to solve them:
1. DependencyService.Get Returns Null
This is the most common issue. If DependencyService.Get<T>() returns null, check the following:
- Missing Attribute: Ensure the
[assembly: Dependency(typeof(YourClassName))]attribute is placed outside the namespace declaration in your platform project. - Type Mismatch: Double-check that the type passed to
Dependencyin the platform project exactly matches the implementation class and that it inherits from the interface. - Platform Implementation Missing: If you are testing on an Android emulator but only implemented the service in the iOS project, it will return null.
2. No Parameterless Constructor
DependencyService requires a public, parameterless constructor to instantiate your platform-specific class. If you need to pass dependencies into your implementation, you may need to look into a full Dependency Injection (DI) container like Autofac or Unity, or use a static initialization method.
3. Context Issues on Android
Many Android APIs require a Context or an Activity. Since the DependencyService implementation is a simple class, it doesn’t automatically have access to the current Activity. You can use Android.App.Application.Context for many things, but for UI-bound tasks (like showing a Dialog), you might need to use the Current Activity Plugin or a static reference to your MainActivity.
Best Practices for DependencyService
To keep your code maintainable and professional, follow these guidelines:
- Check for Null: Always check if
DependencyService.Getreturns null before using it to prevent the app from crashing. - Keep Implementations Lean: The implementation class should only handle platform-specific logic. Keep business logic in the Shared Project.
- Interface Segregation: Don’t create one giant
INativeService. Instead, create specific interfaces likeIBatteryService,IFileSystemService, andINotificationService. - Use Xamarin.Essentials First: Before writing a custom DependencyService, check Xamarin.Essentials. It already contains cross-platform implementations for battery, connectivity, file system, and dozens of other features.
Comparison: DependencyService vs. Dependency Injection (DI)
Many developers ask: “Is DependencyService just a poor man’s Dependency Injection?” Not exactly. While they both solve the problem of decoupling, they serve different purposes in the Xamarin ecosystem.
| Feature | DependencyService | Dependency Injection (e.g., Autofac) |
|---|---|---|
| Setup | Built-in, very easy to set up. | Requires external libraries and configuration. |
| Lifetime Management | Mostly singletons (instantiated once). | Highly configurable (Transient, Scoped, Singleton). |
| Parameters | Cannot easily pass constructor parameters. | Excellent support for constructor injection. |
| Use Case | Platform-specific hardware/API access. | General architecture and ViewModel decoupling. |
Real-World Example: Accessing Native File Paths
One of the most frequent uses for DependencyService is finding where to store files. Android and iOS have completely different file systems and sandbox rules.
// Shared Interface
public interface IPathService
{
string GetDatabasePath(string filename);
}
// Android Implementation
[assembly: Dependency(typeof(PathService_Droid))]
namespace MyProject.Droid
{
public class PathService_Droid : IPathService
{
public string GetDatabasePath(string filename)
{
string path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
return System.IO.Path.Combine(path, filename);
}
}
}
// iOS Implementation
[assembly: Dependency(typeof(PathService_iOS))]
namespace MyProject.iOS
{
public class PathService_iOS : IPathService
{
public string GetDatabasePath(string filename)
{
string docFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
string libFolder = System.IO.Path.Combine(docFolder, "..", "Library", "Databases");
if (!System.IO.Directory.Exists(libFolder))
{
System.IO.Directory.CreateDirectory(libFolder);
}
return System.IO.Path.Combine(libFolder, filename);
}
}
}
Summary and Key Takeaways
The DependencyService is a powerful tool in the Xamarin.Forms toolbox. It allows you to maintain a clean architecture while still leveraging the full power of the underlying mobile operating systems. By defining an interface in your shared code and providing native implementations in your platform projects, you can bridge any gap that Xamarin.Forms doesn’t cover out of the box.
- Abstraction: Use interfaces to keep your shared code platform-agnostic.
- Registration: Never forget the
[assembly: Dependency]attribute. - Platform Specifics: Only write the code that must be different for iOS or Android in the implementation classes.
- Transition to MAUI: Note that in .NET MAUI (the successor to Xamarin.Forms), DependencyService is still available but modern Dependency Injection via
MauiProgram.csis the preferred method for resolving services.
Frequently Asked Questions (FAQ)
1. Can I use DependencyService in a .NET Standard library?
Yes! In fact, it is the recommended way to handle platform-specific code when using .NET Standard libraries in Xamarin.Forms. You define the interface in the .NET Standard library and the implementations in the platform projects.
2. Is DependencyService a Singleton?
By default, Xamarin.Forms caches the instance of your dependency. The first time you call DependencyService.Get<T>(), it creates the object. Subsequent calls return the same instance. This makes it act like a Singleton.
3. What happens if I don’t implement the service for one platform?
If you call DependencyService.Get<T>() on a platform where no implementation is registered, it will return null. Your app will not crash immediately, but you will likely encounter a NullReferenceException if you don’t check the result before using it.
4. Should I use DependencyService for everything?
No. If a feature is available in Xamarin.Essentials, use that instead. If you are doing complex architectural decoupling (like injecting ViewModels into Views), use a dedicated DI container like Autofac or Prism.
5. Can I pass parameters to the constructor of my implementation?
No, the DependencyService requires a parameterless constructor. If you need parameters, you must provide them via a method call (e.g., an Init(config) method) after the service has been resolved.
