Xamarin Android for Android Developers

I’ve been aware of Xamarin for quite a while, but given that I write my Android apps in AndroidStudio and Kotlin I’ve always ignored it. I think I always though of Xamarin as being another attempt at a cross platform UI, I have played around with these on and off over the last 30 years going all the way back to Zinc, and I’ve never found any of them to be great experiences. It turns out there are two flavours of Xamarin for Android developers Xamarin Forms (soon to be called MAUI) is a cross platform UI, Xamarin Android is a framework than enables running .NET code using Mono on Android phones by providing wrappers and bridges to calling the OS and allows the regular XML layout files and assets to be used.

The Software

PodcastUtilities is a .NET Framework podcast cache manager. It is designed to have a minimal UI or be used as a headless API. It is old code, most of it was written over 10 years ago, the only significant development done recently has been make the main projects multi-platform so that they target .NET Framework and .NET Core/.NET Standard. Using .NET Core enabled Podcast Utilities to be run on Mac, Windows and Linux. It was fun to see it running on a Raspberry PI 1 using Mono but that was where it was left except for minor maintenance releases to code with TLS changes.

Its still used extensively to download and manage a podcast cache on a server, then the podcasts can be played from there or synced to a phone.

The Developer

I worked as a .NET developer for 15 years, on both desktop and web, using WPF, WinForms, WebForms and MVC. For the last 7 years I have worked as an Android developer using Java, Kotlin and AndroidX. In other words I have experience of the pretty standard toolchain on both platforms.

I pretty much stopped using .NET around the time .NET Core was really getting started. This meant that I didn’t really understand about Xamarin Android and didn’t really take Mono seriously.

The Project

When I looked into Xamarin Android and saw that it was a wrapper for .NET code using Mono as a runner and I thought about the work we had done to get PodcastUtilities API compiled as a .NET Standard DLL it made me think that most of the heavy lifting has been done and surely that this would be a good test to see if the toolchain could be taken seriously.

The app was sufficiently complex and big that not having to write it again on Android would be a real time saver. Phones now have more storage available, 50GB+ is not uncommon, they certainly have enough storage to keep a cache locally. Running the utilities on the phone and downloading directly, rather than download to a server and syncing by the increasingly forgotten MTP protocol was also attractive.

Current Structure

The current structure of PodcastUtilities as distributed on chocolatey can be illustrated by this diagram

Desktop Structure

There are four main utilities, DownloadPodcasts, SyncPodcasts, GeneratePlaylist and PurgePodcasts. They all read their configuration from an XML file and make use of all the logic in PodcastUtilities.Common. The raw objects in this assembly can be used directly however its much more usual to access then via PodcastUtilities.Ioc. Although the boxes on the diagram are the same size the bulk of the code is in PodcastUtilities.Common, PodcastUtilities.Ioc is an access layer and the console apps read the config, call the heavy lifters in PodcastUtilities.Common and channel the output to the console.

For example the object that finds episodes in a feed is in PodcastUtilities.Common and looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// <summary>
/// discover items to be downloaded from a feed
/// </summary>
public class EpisodeFinder : IEpisodeFinder
{
    private readonly IDirectoryInfoProvider _directoryInfoProvider;
    private readonly IFileUtilities _fileUtilities;
    private readonly IPodcastFeedFactory _feedFactory;
    private readonly IWebClientFactory _webClientFactory;
    private readonly ITimeProvider _timeProvider;
    private readonly IStateProvider _stateProvider;
    private readonly ICommandGenerator _commandGenerator;

    /// <summary>
    /// discover items to be downloaded from a feed
    /// </summary>
    public EpisodeFinder(
        IFileUtilities fileFinder, 
        IPodcastFeedFactory feedFactory, 
        IWebClientFactory webClientFactory, 
        ITimeProvider timeProvider, 
        IStateProvider stateProvider, 
        IDirectoryInfoProvider directoryInfoProvider, 
        ICommandGenerator commandGenerator)
    {
        _fileUtilities = fileFinder;
        _commandGenerator = commandGenerator;
        _directoryInfoProvider = directoryInfoProvider;
        _stateProvider = stateProvider;
        _timeProvider = timeProvider;
        _webClientFactory = webClientFactory;
        _feedFactory = feedFactory;
    }

This object could be instantiated by a client directly or by making use of the objects in PodcastUtilities.Ioc like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Program
{
    private static IIocContainer _iocContainer;
    private static ReadOnlyControlFile _control;

    private static IIocContainer InitializeIocContainer()
    {
        var container = IocRegistration.GetEmptyContainer();

        IocRegistration.RegisterSystemServices(container);
        IocRegistration.RegisterPortableDeviceServices(container);
        IocRegistration.RegisterFileServices(container);
        IocRegistration.RegisterFeedServices(container);

        return container;
    }

    static void Main(string[] args)
    {
        _control = new ReadOnlyControlFile(args[0]);
        _iocContainer = InitializeIocContainer();
        var podcastEpisodeFinder = _iocContainer.Resolve<IEpisodeFinder>();
        foreach (var podcastInfo in _control.GetPodcasts())
        {
            var episodesInThisFeed = podcastEpisodeFinder.FindEpisodesToDownload(
                _control.GetSourceRoot(), 
                _control.GetRetryWaitInSeconds(), 
                podcastInfo, 
                _control.GetDiagnosticRetainTemporaryFiles());
            allEpisodes.AddRange(episodesInThisFeed);
        }

It might seem cumbersome to setup but once it is setup then the IoC container will wire up all dependencies and the dependent dependencies and so on.

By using IoC via an interface rather than referencing a concrete implementation enables us to replace the container if needed. The interface looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
namespace PodcastUtilities.Common
{
    /// <summary>
    /// supports the ability to register objects in an IoC container
    /// </summary>
    public interface IIocContainer
    {
        /// <summary>
        /// register a service
        /// </summary>
        /// <typeparam name="TService">the service to be registered, usually an interface</typeparam>
        /// <typeparam name="TImplementor">the concrete implementation</typeparam>
        void Register<TService, TImplementor>()
            where TService : class
            where TImplementor : class, TService;

        /// <summary>
        /// register a service
        /// </summary>
        /// <typeparam name="TService">the service to be registered, usually an interface</typeparam>
        /// <typeparam name="TImplementor">the concrete implementation</typeparam>
        /// <param name="lifecycle">The lifecycle of the registered implementation</param>
        void Register<TService, TImplementor>(IocLifecycle lifecycle)
            where TService : class
            where TImplementor : class, TService;

        ///<summary>
        /// Register a type as both the service type and implementing type.
        ///</summary>
        ///<param name="serviceTypeToRegisterAsSelf">The service/implementing type to register</param>
        void Register(Type serviceTypeToRegisterAsSelf);

        ///<summary>
        /// Register an instance as a service.
        ///</summary>
        ///<param name="instance">The service/implementing instance to register</param>
        void Register<TService>(TService instance) where TService : class;

        ///<summary>
        /// Resolve a service
        ///</summary>
        ///<typeparam name="TService"></typeparam>
        ///<returns></returns>
        TService Resolve<TService>();
    }

Again this might seem like overkill but when we wrote PodcastUtilities we used .NET Framework and one of the leading IoC Containers, LinFu. Ten years later and LinFu is no longer in active development and we needed a different IoC container for .NET core, Microsoft.Extensions. The code that makes use of PodcastUtilities.Ioc does not need to know which container is being used.

The Target

The idea is that we can run PodcastUtilities on an android phone, they have the capacity these days, and it will also to avoid having to sync files from a local server cache.

After some trial and error with getting the hang of how the tooling worked the structure I ended up with was this.

Android Structure

PodcastUtilities.Common and PodcastUtilities.Ioc are exactly the same DLLs that are distributed for the desktop console apps. At the moment they are produced using Visual Studio 2017, they could be updated to a later version but that is a separate task. I would be using Visual Studio 2019 for the android app as it included more up to date components and was also pretty stable.

I the manner of modern android apps the UI components, activities, views and fragments would be relativity simple using IoC to provide a ViewModel for the logic. The UI components went in the main app, the ViewModels and supporting logic went in an AndroidLogic DLL which could then be referenced by the tests. In “real” android instrumentation tests the tests just reference the main app directly however received wisdom is that having test runner apps reference you main app was not a great plan in Xamarin Android, also see this answer on Stackoverflow, so I created a common shared assembly as it doesnt really make much difference to the build.

I was expecting to fall into a number of traps along the way, in fact I think I suspected I would find things that made the whole idea a non-starter.

Next time I’ll start to dig into the steps I took and traps I fell into.