On library versioning: Minimum version and AndroidX migration

On library versioning: Minimum version and AndroidX migration
https://github.com/roubachof/Sharpnado.Presentation.Forms

Last version of Sharpnado lib includes MaterialFrame with Acrylic mode, but also the compatibility with Xamarin.Forms 4.5 and AndroidX.

Since version 4.5, Xamarin.Forms android platform dll is compiled against AndroidX when targeting Android 29 and Android.Support libs on others targets.

xf compilation

Resulting in the following nuget package:

xf nuget

So what will happen if a user is using XF 4.5 with Android 29 (so compiled against AndroidX) as a target and Sharpnado 1.4 (was compiled against Android.Support libs)?

This:

sharpnado issue

The user config was this:

Target: MonoAndroid v10.0

Xamarin.Forms 4.5.0.356 (MonoAndroid v10.0)
  |
  |---> AndroidX libs

Sharpnado 1.4 (MonoAndroid)
  |
  |---> Xamarin.Forms >= Xamarin.Forms 3.4.0

So it's a perfectly valid nuget setup, but remember Sharpnado 1.4 was built against Android.Support libs.
And in this configuration there is no reason the nuget process will get the Android.Support libs.
So when the user is trying to build the solution, it cannot find the assemblies needed by the Sharpnado libs.

So should I update the Sharpnado dependency to XF 4.5?

Choosing a minimal version for your dependencies

When you are developing a library, you want it to be useful for the largest number of people.
It means that it must be compatible with the largest amount of projects out there.

Let's take the example of Sharpnado.Presentation.Forms

The only Third-party library it depends on is Xamarin.Forms.

So I want the Xamarin.Forms version to be as low as possible to be sure that most developers can use it.

But how to choose this version you would ask me?

It's easy it will be constrained by a feature I need for building my library.
In my case I needed the ImageButton control to implement my TabButton.
And it was first available in the version 3.4 of Xamarin.Forms.

xamarin forms dependency

Good reasons to update your dependencies versions

  • When you are telling your users that your library supports XF version starting 3.4.0, it means that you have to process all issues from 3.4.0 to the latest version: it could be a lot of work
  • You maybe also know that your library is only really stable starting from a more recent version
  • You want to use a feature that is only available in a more recent library version

Bad reasons

Sometimes, I see library releases with the following note:

Updated to the latest version of Xamarin.Forms.

Some developer think that updating their lib to the latest version of XF is better cause it will embed more performant/less buggy code.

It's a mistake.

When you are building your library against another dependency, it doesn't include code from this dependency, it just redirect code to the dependency.

Let's take the example of the MaterialFrame from the Sharpnado 1.5 library:

using System.ComponentModel;
using Android.Content;
using Android.Graphics.Drawables;

#if __ANDROID_29__
using AndroidX.Core.View;
#else
using Android.Support.V4.View;
#endif

using Sharpnado.Presentation.Forms.Droid.Renderers;
using Sharpnado.Presentation.Forms.RenderedViews;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using FrameRenderer = Xamarin.Forms.Platform.Android.AppCompat.FrameRenderer;

[assembly: ExportRenderer(typeof(MaterialFrame), typeof(AndroidMaterialFrameRenderer))]

namespace Sharpnado.Presentation.Forms.Droid.Renderers
{
    /// <summary>
    /// Renderer to update all frames with better shadows matching material design standards.
    /// </summary>
    public class AndroidMaterialFrameRenderer : FrameRenderer
    {
        private GradientDrawable _mainDrawable;

        private GradientDrawable _acrylicLayer;

        public AndroidMaterialFrameRenderer(Context context)
            : base(context)
        {
        }

        private MaterialFrame MaterialFrame => (MaterialFrame)Element;

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {

Now let's have a look at the IL code when compiled in RELEASE:

material frame il

And this line in particular:

Sharpnado.Presentation.Forms.Droid.Renderers.AndroidMaterialFrameRenderer
    extends [Xamarin.Forms.Platform.Android]Xamarin.Forms.Platform.Android.AppCompat
                .FrameRenderer

This is how an external dll is referenced in IL. The assembly name followed by the complete class name.
We can now understand, that my renderer will search its Xamarin.Forms.Platform.Android.AppCompat.FrameRenderer parent type in the Xamarin.Forms.Platform.Android dll.
It doesn't include any code from Xamarin.Forms but just redirect the code to the needed assembly.

So if we compile our library against Xamarin.Forms 3.4 but our users use Xamarin.Forms 4.4 it will totally work AND it will use all the performance improvements and bug fixes included in Xamarin.Forms 4.4.

On the other hand, if a user is stuck with XF 3.4 because of its corporate policy for example (only this version has been validated by the security department), he can still use it.
The Xamarin.Forms team is doing a super great job at not breaking the API.

They even create shim classes to prevent cracks in our existing renderers:

using System;
using System.ComponentModel;
using Android.Content;

namespace Xamarin.Forms.Platform.Android.AppCompat
{
    // This version of FrameRenderer is here for backward compatibility with anyone referencing 
    // FrameRenderer from this namespace
    public class FrameRenderer : FastRenderers.FrameRenderer
    {
	    public FrameRenderer(Context context) : base(context)
	    {
		    
	    }

	    [Obsolete("This constructor is obsolete as of version 2.5. Please use FrameRenderer(Context) instead.")]
		[EditorBrowsable(EditorBrowsableState.Never)]
		public FrameRenderer()
	    {
		   
	    }
    }
}

So why force our users to update to the latest version of Xamarin.Forms?

It should be their choice not ours.

Migrating to AndroidX

Back to our migration issues.

Finally I managed to still have a minimal XF version of 3.4.0.

Compilation

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>portable</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
</PropertyGroup>
  
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'ReleaseAndroid9.0|AnyCPU'">
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <Optimize>true</Optimize>
    <DebugType>portable</DebugType>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <ErrorReport>prompt</ErrorReport>
    <TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
</PropertyGroup>

...

<Choose>
    <When Condition=" '$(TargetFrameworkVersion)' == 'v10.0' ">
      <PropertyGroup>
        <OutputPath>$(OutputPath)\monoandroid10.0\</OutputPath>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Xamarin.Forms">
          <Version>4.5.0.495</Version>
        </PackageReference>
        <PackageReference Include="Xamarin.Google.Android.Material">
          <Version>1.0.0</Version>
        </PackageReference>
      </ItemGroup>
    </When>
    <Otherwise>
      <PropertyGroup>
        <OutputPath>$(OutputPath)\monoandroid90\</OutputPath>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Xamarin.Forms">
          <Version>4.5.0.495</Version>
        </PackageReference>
        <PackageReference Include="Xamarin.Android.Support.Core.Utils">
          <Version>28.0.0.3</Version>
        </PackageReference>
        <PackageReference Include="Xamarin.Android.Support.Core.UI">
          <Version>28.0.0.3</Version>
        </PackageReference>
        <PackageReference Include="Xamarin.Android.Support.Compat">
          <Version>28.0.0.3</Version>
        </PackageReference>
        <PackageReference Include="Xamarin.Android.Support.v7.CardView">
          <Version>28.0.0.3</Version>
        </PackageReference>
        <PackageReference Include="Xamarin.Android.Support.v7.RecyclerView">
          <Version>28.0.0.3</Version>
        </PackageReference>
      </ItemGroup>
    </Otherwise>
</Choose>
using System.ComponentModel;
using Android.Content;
using Android.Graphics.Drawables;

#if __ANDROID_29__
using AndroidX.Core.View;
#else
using Android.Support.V4.View;
#endif

using Sharpnado.Presentation.Forms.Droid.Renderers;
using Sharpnado.Presentation.Forms.RenderedViews;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using FrameRenderer = Xamarin.Forms.Platform.Android.AppCompat.FrameRenderer;

So I have 2 releases for droid platform:

  • On MonoAndroid v9.0 it's compiled against Android.Support libs
  • On MonoAndroid v10.0 against AndroidX libs

Nuget package

sharpnado nuget

So if a user is targeting MonoAndroid v10.0 (api 29), but still using Xamarin.Forms < 4.5 (so built against Android.Support libs), it will still retrieve the AndroidX libs maintaining a correct setup \o/

Example
Target: MonoAndroid v10.0

Xamarin.Forms 4.0.0.709238 (MonoAndroid v8.1)
  |
  |---> Android.Support libs 28

Sharpnado 1.5.1 (MonoAndroid v10.0)
  |
  |---> Xamarin.Forms >= Xamarin.Forms 3.4.0
  |---> AndroidX libs