Dependency Services on Xamarin Forms
While Xamarin Forms has evolved dramatically, and more and more plugins are created for us to be able to use native functionality directly from the .NET Standard Library, there will be from time to time a specific functionality that is not yet available through shared code.
This doesn’t mean that you must think of an alternative, say native Xamarin. Xamarin Forms after all still has those native projects. So what do you do? You code the native functionality and inject it over to the .NET Standard Library using Dependency Injection.
The Scenario
Now, I know what you are thinking. Dependency Injection can sound scary and complicated. Thankfully Dependency Services on Xamarin Forms make it a breeze to use.
In the example that I show you here, we have a page that lists some data and displays a share button. This share button must create a text file out of the data that the page lists, and open the native share interface for the user to use whatever app they may need to share that text file.
Creating the file can be done with shared code, but let’s imagine that there is no way to share from the .NET Standard Library, we must create platform-specific code.
The Interface
The implementation of dependency services starts with the definition of an interface. The classes that we inject have to implement this interface. This way we make sure that the classes have all the members that we need. Besides, the .NET Standard Library doesn’t even need to know about those classes, platform-specific classes only need to inject them, and the .NET Standard Library merely looks for classes that implement the interface.
So for my example, I only need the interface to contain one member: a Show method.
public interface IShare { Task Show(string title, string message, string filePath); }
So the classes that implement this interface need to have a method that is a Task is called Show and receives three strings. This way we make sure, from the .NET Standard Library, that the injected classes contain the members that we need.
The Platform-Specific Functionality
In both the Android and the iOS projects, I create a Dependencies folder (optional), and inside I define a Share class. In both cases, the Share class must implement the IShare interface, which means that both classes have a Show method. The actual share functionality is not essential for the context of Dependency Services, but in case you are wondering, here is the Android implementation:
public class Share : IShare { public Task Show(string title, string message, string filePath) { var intent = new Intent(Intent.ActionSend); intent.SetType("text/plain"); var fileUri = FileProvider.GetUriForFile(Forms.Context.ApplicationContext, "com.lalorosas.ExpensesExample.provider", new Java.IO.File(filePath)); intent.PutExtra(Intent.ExtraStream, fileUri); intent.PutExtra(Intent.ExtraTitle, title); intent.PutExtra(Intent.ExtraText, message); var chooser = Intent.CreateChooser(intent, title); chooser.SetFlags(ActivityFlags.GrantReadUriPermission); Android.App.Application.Context.StartActivity(chooser); return Task.FromResult(true); } }
Also, here the iOS implementation:
public class Share : IShare { public async Task Show(string title, string message, string filePath) { var viewController = GetVisibleViewController(); var items = new NSObject[] { NSObject.FromObject(title), NSUrl.FromFilename(filePath), NSString.FromObject(message) }; var activityController = new UIActivityViewController(items, null); //? Needed? if (activityController.PopoverPresentationController != null) activityController.PopoverPresentationController.SourceView = viewController.View; await viewController.PresentViewControllerAsync(activityController, true); } private UIViewController GetVisibleViewController() { var rootViewController = UIApplication.SharedApplication.KeyWindow.RootViewController; if (rootViewController.PresentedViewController == null) return rootViewController; if (rootViewController.PresentedViewController is UINavigationController) return ((UINavigationController)rootViewController.PresentedViewController).TopViewController; if (rootViewController.PresentedViewController is UITabBarController) return ((UITabBarController)rootViewController.PresentedViewController).SelectedViewController; return rootViewController.PresentedViewController; } }
The code is, of course, somewhat different, but both classes implement the interface and have the Share method, which is what matters to the .NET Standard Library.
Injecting the Dependency
Now is where the Dependency Service comes into play. It simplifies everything. To inject these two dependencies over to the .NET Standard Library, all you need to know is add the following line on top of the namespace that holds the Share class in each project:
[assembly: Dependency(typeof(Share))]
Adding this code means that the Android file where you defined the Share class starts something like this:
using ExpensesExample.Droid.Dependencies; [assembly: Dependency(typeof(Share))] namespace ExpensesExample.Droid.Dependencies { public class Share : IShare { } }
Something similar has to happen in the iOS file:
using ExpensesExample.iOS.Dependencies; [assembly: Dependency(typeof(Share))] namespace ExpensesExample.iOS.Dependencies { public class Share : IShare { } }
Notice how I do need to add that first using directive since the namespace that contains the Share class is not recognized outside of its definition, even if on the same file.
Using the Instances
Injecting these types means that there is now a correct instance of a class that implements the interface, depending on the app that is running. Back in the .NET Standard Library, if you want to get an instance of the classes that implement your interface, you only need to do this:
IShare shareDependency = DependencyService.Get<IShare>();
Notice that I use the interface and not the class to define the variable. The actual class won’t matter, so long as it implements the IShare interface. Of course, this variable contains the Show method, which then uses the appropriate native class to share the text file that I created. If curious about the whole method on sharing that txt file, here it is:
async void ShareReport(bool obj) { // Todo: create file with progresses IFileSystem fileSystem = FileSystem.Current; var rootFolder = fileSystem.LocalStorage; var reportsFolder = await rootFolder.CreateFolderAsync("reports", CreationCollisionOption.OpenIfExists); var reportFile = await reportsFolder.CreateFileAsync("report.txt", CreationCollisionOption.ReplaceExisting); using (StreamWriter sw = new StreamWriter(reportFile.Path)) { foreach (var progress in Progresses) { sw.WriteLine($"{progress.Name} - {progress.ProgressValue:p}"); } } IShare shareDependency = DependencyService.Get<IShare>(); await shareDependency.Show("Reporte de Gastos", "Reporte de gastos por categoría", reportFile.Path); }
It does use the Progresses property -which contains the whole data- to add content to the file, but aside from what you see in this method, there is nothing else needed to share a text file.