I was recently out at a client site and they asked me how to conditionally deploy resources in Xamarin Android. They have a situation where they have one base application that they are creating and depending on the brand deploy a completely different set of resources. It is important to note that they only want to deploy the resources that would be particular to that brand so as not to bloat the overall application with a bunch of images/strings that would never be used. At the time this seemed like an interesting problem but one that I had never tried. With a little digging I found an answer on the Xamarin site:
Xamarin - Build Process
The Xamarin instructions are at a pretty high level and sometimes they need some experimentation to figure out all the wrinkles. I decided to try this out and I wanted a solution that would meet the following criteria:
- It should allow for one set of resources to be shared for all brands and some resources to be brand specific
- It should be easy to specify building for one brand or the other
- It should support all types of resources, strings, views, images, etc
- It should be easy to setup and maintain
From the above article this will require some manual editing of the project file in something like notepad. This means right off the bat my fourth requirement of "It should be easy to setup and maintain" is going to be a bit tenuous (note to self: perhaps a Visual Studio plugin could be made for this so it can be done through the UX?).
First thing I did was set up a new project called MyResourceTest and renamed Activity1.cs to MainActivity. That creates an Android project that looks like this:
I want to have a shared image and a shared string. I use the default icon as my shared image and in the default strings.xml I change the string resources to contain a single value, SharedString:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="SharedString">Shared string used by all</string>
</resources>
Then I modify the Main.axml to display the shared string:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/lblShared"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/SharedString" />
</LinearLayout>
Finally I remove the code for the button that was created by default in the MainActivity.cs:
using Android.App;
using Android.OS;
namespace MyResourceTest
{
[Activity(Label = "", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
}
}
}
When I run this I get a very unexciting application, nothing new here:
This is where the fun starts. I want to add an image and a couple of strings that vary as far as what gets deployed based on what configuration I choose. My application brands are going to be named AppOne and AppTwo and so I need to create four new configurations Debug-AppOne, Debug-AppTwo, Release-AppOne and Release-AppTwo. I copy the default Debug and Release configurations to my new configurations as appropriate. When I’m done I remove the default Debug and Release configurations because I won’t need them anymore. The complete configuration setup looks like this:
Now I’ll set up the directory structure to support my new applications. In the project I’m going to create two new directories from the root of the project, Resources-AppOne, Resources-AppTwo. Under each of these projects will be a Drawable and Values folder:
Note: That there are now three resources directories, one for our shared resources, one for the brand resources specific to AppOne and one for resources specific to AppTwo. I could just as easily add a Layout folder to each of the new resources folders to have brand specific views.
To the Resources-AppOne\Drawable folder I add an image for AppOne called MainLogo.png and add an AppTwo MainLogo.png picture to the Resources-AppTwo\Drawable folder. These will be two different images with the same name. Be careful, if you add an image to one brand specific folder you will have to have an equivalent version with the same name in the other brand specific folders or have code in your application to handle the case where it might be missing.
To each of the brand specific Values folder I add a new xml file for my strings called strings-app.xml. This needs to have a different name than any of the files under the default Resources\Values folder or you will have a collision at compile time. In each of the files I set up two string values, ApplicationName, AppDescription.
File for Resources-AppOne\Values\Strings-App.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ApplicationName">First Application</string>
<string name="AppDescription">The First Application</string>
</resources>
File for Resources-AppTwo\Values\ Strings-App.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ApplicationName">Second Application</string>
<string name="AppDescription">The Second Application</string>
</resources>
Note: I make sure the build action for all of these files is set to AndroidResource for them to be used as Android Resources by the compiler.
I want to use my new image and strings in my view, Main.xml. I change it to be as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/lblShared"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/SharedString" />
<TextView
android:id="@+id/lblDescription"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/AppDescription" />
<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/imgMain"
android:src="@drawable/MainLogo"
android:scaleType="fitCenter"/>
</LinearLayout>
Finally I also want the application’s title to use my brand specific ApplicationName string resource. To do this I override the MainActivity’s OnAttachedToWindow event to use the ApplicationName string resource:
public override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
this.Window.SetTitle(GetString(Resource.String.ApplicationName));
}
If I decide to try and run this now I’m going to get a couple of errors, the compiler doesn’t know how to deal with the Resources-AppOne or Resources-AppTwo folders. Errors similar to the following appear:
This is where it gets a little difficult. We can’t do what we want in the IDE, instead we need to edit the project file directly in a tool like Notepad. If I open the project in Notepad and view my newly added brand specific files this is what I find:
I want to pull out the AppOne and AppTwo resources into their own ItemGroups that are conditionally included in the project if one of the AppOne or AppTwo configurations we added before are selected. When done your project file should look similar to this:
<ItemGroup Condition="'$(Configuration)'=='Debug-AppOne'Or'$(Configuration)'=='Release-AppOne'">
<AndroidResource Include="Resources-AppOne\Drawable\MainLogo.png" />
<AndroidResource Include="Resources-AppOne\Values\Strings-App.xml">
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug-AppTwo'Or'$(Configuration)'=='Release-AppTwo'">
<AndroidResource Include="Resources-AppTwo\Drawable\MainLogo.png" />
<AndroidResource Include="Resources-AppTwo\Values\Strings-App.xml">
<SubType>AndroidResource</SubType>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<Content Include="Properties\AndroidManifest.xml" />
</ItemGroup>
There is one more thing we need to do. If you recall the compiler won’t see the Resources-AppOne or Resources-AppTwo directories as being valid. We need to map them correctly in the project file. Add two new conditional PropertyGroups, one for AppOne and one for AppTwo, to correctly map the directories:
<PropertyGroup Condition="'$(Configuration)'=='Debug-AppOne'Or'$(Configuration)'=='Release-AppOne'">
<MonoAndroidResourcePrefix>Resources;Resources-AppOne</MonoAndroidResourcePrefix>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug-AppTwo'Or'$(Configuration)'=='Release-AppTwo'">
<MonoAndroidResourcePrefix>Resources;Resources-AppTwo</MonoAndroidResourcePrefix>
</PropertyGroup>
If I save the project file and reopen it, the project will use the correct resources for the configuration I have selected. If I select Debug-AppOne and run the application this is what I see:
If I then select the Debug-AppTwo profile I see this:
Remember, this is doing more than just using the correct resources at run time like when you have language specific resources, it is only compiling in the resources specified by the selected configuration. To verify this you can examine the obj output folders for the appropriate configuration. Compiling in an alternate set of resources is as simple as changing the selected configuration from Debug-AppOne to Debug-AppTwo. In general this satisfies all my conditions other than it does take some time to setup and to maintain. That is, if new drawable resources are added to the brand specific resource directories you will still need to modify the project file directly and make sure they show up under the correct conditional ItemGroups.
There are probably other ways to do this such as using your source control solution with some sort of branching strategy but that seems too difficult to manage. As I indicated earlier at some point a Visual Studio plugin could be added to make this easier to do through the UX but at the present time as indicated on the Xamarin site this would seem to be the easiest way to do it.