It has been exactly one year that I have been involved mobile application development with Xamarin and I would like to share an important experience that I have gained in this period. Xamarin is a powerful framework for cross-platform mobile application development which uses a modern and powerful language like C# but in the same time it offers access to the native Platform APIs and User Interface controls.
Another key aspect of Xamarin Framework is the ability to use native libraries (native is a relative term and in the Xamarin world it means developed in Java or Obj-C/Swift). In case of Java jar libraries the usage is made possible by either creating a Binding Library or by direct access to the Java Native Interface aka JNI. The former is almost quite straightforward: you create a Binding Project and then Xamarin uses predefined templates to generate Managed Callable Wrappers (MCW) that interact with JNI. The MCWs encapsulate the JNI usage and hide the underlying complexity. Also the MCWs enable Java sub-classing and the possibility to provide C# implementation of Java interfaces.
On the other hand, working directly with JNI does not have any practical limitation but the official Xamarin Guide for JNI Bindings ends with the dramatic đ± summary:
Dealing directly with JNI is a terrible experience that should be avoided at all costs. Unfortunately, itâs not always avoidable; hopefully this guide will provide some assistance when you hit the unbound Java cases with Mono for Android.
So the automatic template generation for C# wrappers is the way to go, but it comes with one cost: the limitations. In fact the MCWs are a convenient tool provided by Xamarin, but they have some limitations. The limitations and the complexity to put everything correctly in place makes the binding process require high volatility time estimation. I would like to define the overall experience of the binding process as Infinite Improbability Drive.
In this post I would like to cover one of the most annoying limitations: the lack of Java Generics Support. It is useful to stress out that this is not a limitation of the binding templates, but it is intrinsic to the Java Language. Generic Java types are erased at compile time so the automatic binding templates little can do to generate the correct types Java.Lang.Object
-s it encounters in the jar library. The Java classes that extend/implement generic classes or interfaces cannot be bound by the automatic generated templates. For the curious ones the code generation framework can be browsed in the official Github Xamarin.Android Repo.
So how to handle this situation1? Again there is the risky JNI road but I will propose a simpler solution. I got this idea from this Xamarin bugzilla post. The author exposes an issue when trying to bind Generic Interfaces. See code snippet:
public interface TopInterface<T> {
abstract public void onRun(T object);
}
public interface SecondInterface extends TopInterface<StringContainer> {
}
public class StringContainer extends java.lang.Object {
public String contents;
}
The problem is that the MCW for ITopInterface
is not Generic! If one tries to use the ISecondInterface
binding from a Xamarin.Android app:
public class ImplementsSecondInterface : Java.Lang.Object, ISecondInterface
{
public void OnRun(StringContainer container)
{
container.Contents = "Hello, World!";
}
}
}
the compilation error arises:
Error: testapp.ImplementsSecondInterface is not abstract and does not override abstract method onRun(com.example.testandroidlib.StringContainer) in com.example.testandroidlib.TopInterface
The author suggestion for this limitation is to:
ITopinterface, ISecondInterface, Consumer
) by adding generics to the erased types,Additions
folder of the binding project (so there is no need to hand-edit on each build),Java.Lang.Object
,monodroid.exe
will generate faulty ACW for the implementing class,$ProjectDir/obj/Debug/android/src/md5...
. The ACW class has too implement only the SecondInterface
with the correct types (e.g. String instead of Object),[Register("testapp/ImplSecondInterface", DoNotGenerateAcw = true)]
to the implementing class,AndriodJavaSource
Build Action.Although it may seem pretty straightforward step 2 and 7 are quite cumbersome for most of the developers who do not want to spend time learning the JNI interactions. Moreover this simple case does not represent all real world scenarios where generics types are entangled with other classes and interfaces. Thus, maintaining that code might not be an easy task as the amount of knowledge to be transferred to other members of the team is huge.
The idea that my team and I came up with, is to create a thin
wrapper in Java for the Interfaces and Classes that use generics and completely hide the Generics by exposing a new API. This new Java library will contain all possible combinations that would derive from the Generic types, because each of them will explode to a corresponding dedicated type. In my opinion the resulting Java Wrapper library is more maintainable than the manually edited MCW/ACW and this blog post aims at minimising the amount of code used to wrap the original library. To support my thesis I have created a demo Java library which exposes Generics in its API. One can find the Java library source code here. It is the poor manâs Event Bus Architecture Pattern
but it is only for demonstration purposes. The overall library architecture is described in the following UML diagram (I hate it but it was the easiest way by using SimpleUML Android Studio Plugin). The classes/interfaces with a red asterisk contain generic types.
Now the main class here SimplePlayer
is quite simple, but in a normal scenario it might have too many dependencies. In order to use it, one has to register to certain Events
and implement the callback for when the event is raised. The EventDispatcher
interface defines a Generic method to register to certain types of events. For the sake of simplicity, I have added two types of Events MediaLoadEvent
and MediaUnloadEvent
both of which inherit from the abstract class PlayerEvent
.
public abstract class PlayerEvent<H> {
public abstract void dispatch(H paramH);
public abstract PlayerEvent.Type<H> getType();
public static class Type<H> {
private final String eType;
public Type(String eType){this.eType = eType;}
public String toString(){return eType;}
}
}
public class MediaLoadEvent extends PlayerEvent<PlayerEventListener<MediaLoadEvent>> {
private final int value;
public int getValue(){return value;}
public MediaLoadEvent(int value){this.value = value;}
static PlayerEvent.Type<PlayerEventListener<MediaLoadEvent>> TYPE = new PlayerEvent.Type("LoadEvent");
public static PlayerEvent.Type<PlayerEventListener<MediaLoadEvent>> getAssociatedType(){
return TYPE;
}
public PlayerEvent.Type<PlayerEventListener<MediaLoadEvent>> getType(){
return TYPE;
}
public void dispatch(PlayerEventListener<MediaLoadEvent> listener){
listener.onPlayerEvent(this);
}
}
Now the fact that the various Events
are subclasses of PlayerEvent
gives an advantage on the elegant definition of the PlayerEventListener
interface which allows the client code to listen to only the necessary Events
that are needed. Unfortunately both of these interfaces cannot be bound automatically.
public interface PlayerEventListener<E extends PlayerEvent> {
public void onPlayerEvent(E param);
}
public interface EventDispatcher {
//H is PlayerEventListener<E extends PlayerEvent>
<H> ListenerRegistration addEventListener(PlayerEvent.Type<H> type, H handler);
}
Putting the jar directly to a binding project will fail with the following problems due to the type erasure explained above:
.../Com.Example.Awesomelibrary.AP.Events.Impl.MediaLoadEvent.cs(23,23): Error CS0534:
`Com.Example.Awesomelibrary.AP.Events.Impl.MediaLoadEvent' does not implement inherited abstract member
`Com.Example.Awesomelibrary.AP.Events.PlayerEvent.RawType.get' (CS0534) (FailedBinding)
.../Com.Example.Awesomelibrary.AP.Events.Impl.MediaLoadEvent.cs(23,23): Error CS0534:
`Com.Example.Awesomelibrary.AP.Events.Impl.MediaLoadEvent' does not implement inherited abstract member
`Com.Example.Awesomelibrary.AP.Events.PlayerEvent.Dispatch(Java.Lang.Object)' (CS0534) (FailedBinding)
#the same for MediaUnloadEvent...
The first step is to identify the classes and interfaces that needs to be edited. Afterwards we have to masquerade the generics types on the wrapper library by wrapping each Class
on a new dedicated type called WrapperType
. This can be obtained by placing the original class as a private field in the wrapper class. The creator of the private field might be the wrapper class itself in most of the cases by providing a constructor with same parameters in the wrapper class. Or, if the wrapped class cannot be created by client code, the wrapper might expose a constructor with the wrapped class. This constructor will not be bound in C# code and will be used only in the thin library. See two examples below:
//constructor with wrapped class
public class MediaLoadEventWrapper {
private MediaLoadEvent event;
//the following constructor will not be bound as MediaLoadEvent will be removed from binding
public MediaLoadEventWrapper(MediaLoadEvent ev){ this.event = ev; }
public int getValue(){return event.getValue();}
}
//constructor with parameters as the wrapped class
public class PlayerWrapper implements HasEventDispatcherWrapper, HasSettings {
private SimplePlayer player;
public PlayerWrapper(ViewGroup playerView){ player = new SimplePlayer(playerView); }
//...
}
In the new library we need to eliminate the generics from the subclasses of PlayerEvent
class. Each generic Event will be wrapped in a dedicated WrapperType. Ok, but in this case we cannot use anymore the elegant interface PlayerEventListener
. This can be fixed â up by adding a wrapper to the PlayerEventListener
and EventDispatcher
interfaces respectively. The new PlayerEventListenerWrapper
class will not contain only a single generic method to handle all Events, but one method for each possible Event. This is clearly a disadvantage since the user code, in order to register to events, has to implement all callbacks (we cannot mark as optional methods on an interface in Java/C#).
public interface PlayerEventListenerWrapper {
void onPlayerEvent(MediaLoadEventWrapper event);
void onPlayerEvent(MediaUnLoadEventWrapper event);
}
public interface EventDispatcherWrapper {
ListenerRegistrationWrapper addEventListener(PlayerEventListenerWrapper handler);
}
As an alternative we might explode the PlayerEventListener
interface in a set of interfaces but in my opinion it creates more confusion. At this point we need to wrap the Player
class in order to implement the new EventDispatcherWrapper
interface instead of EventDispatcher
. I have not decided to wrap the rest of the classes that are not impacted by these changes e.g. HasSettings
interface remained intact. The diagram below depicts the resulting wrapper library structure:
At this point we can create the Android Bindings project as usual on Xamarin. I have created a Demo Project in github. We have to add the original library jar as well as our new wrapper jar in the bindings project; both jars must have âEmbeddedJarâ as Build Action. We need to bind the original library jar because we need the C#
mapping for the original classes that were not wrapped e.g. Settings
. In this way our effort on the wrapper library is minimal. We will still encounter the Binding error for the original jar, but we can avoid that by excluding certain packages from the binding project. In this case I have added the following directive to the Metadata.xml
file in the Transforms
folder of the Binding Project:
<remove-node path="/api/package[@name='com.example.awesomelibrary.ap.events.impl']" />
If everything goes well the Bindings project will output a .dll
which will contain the mappings for both jars each in its own namespace. The namespace is the same as the Java package name converted with first letter uppercase. The following code snippet shows the usage of the two APIs2 in the client code i.e. a sample Android application:
//Wrapper Lib
using Com.Example.Thineventswrapper;
using Com.Example.Thineventswrapper.Wrapper.Events.Data;
using Com.Example.Thineventswrapper.Wrapper.Events;
//Original Lib
using Com.Example.Awesomelibrary.AP.Models;
[Activity(Label = "DemoApp", MainLauncher = true, Icon = "@mipmap/icon")]
public class MainActivity : Activity, IPlayerEventListenerWrapper
{
private Button button;
private IListenerRegistrationWrapper regHandler;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.Main);
button = FindViewById<Button>(Resource.Id.myButton);
//Create the PlayerWrapper
PlayerWrapper pv = new PlayerWrapper(null);
//Register to events from the Wrapper Lib
regHandler = pv.AsEventDispatcherWrapper().AddEventListener(this);
//Read settings from the Original Lib
PlayerSettings settings = pv.AsSettings();
if (settings.Protection == DRMProtection.PlayReady) {
Console.WriteLine("PlayerReady protection");
}
}
protected override void OnDestroy()
{
regHandler.Remove();
base.OnDestroy();
}
public void OnPlayerEvent(MediaUnLoadEventWrapper p0){}
public void OnPlayerEvent(MediaLoadEventWrapper p0)
{
RunOnUiThread(() =>
{
button.Text = "Loaded " + p0.Value;
});
}
}
And thatâs it! Hope this guide might be helpful for someone like me that struggled for some time in this problem of Xamarin framework. Happy coding!
ap
Emmanuele Villa
Paolo Sala
Mirko Gitto
Now one can argue that it would be not useful to waste time on binding libraries that have generic types on their APIs, but in some occasions the library to be bound is the kernel of the Mobile App itself. A good example is the Player Library. If one has to develop a mobile media consuming app that relies it has to rely on some Player SDK and the logic that the player has implemented is higher in 3-4 orders of magnitude in terms of lines of code. ↩︎
If the original library has third party jars dependencies they must be included in the client code too. The Build Action for these type of jars is AndroidJavaLibrary
, in this example I have included the Gson library as dependency of the original jar. ↩︎