#### Goal
- Set up development of an Android app using a .NET language.
- Either in Visual Studio or using the command line `dotnet` tool.
- Use Android Studio to build the app.
- With a Gradle task to build the .NET project.
- And a Gradle task to convert the .NET code to Java compiled form.
- Most of development should be possible on Windows without requiring an Android device.
- Platform-specific parts can be qualified with preprocessor constants.
#### Environment Variables
- Set the environment variable `MSBUILD_EXE` to point to `MSBuild.exe` program file.
- For example, `C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe`
- Set the environment variable `BLUEBONNET_DIR` to point to a directory containing `Bluebonnet.exe`, `Baselib.jar` and `Android.dll`.
- These files can be downloaded from the [Bluebonnet releases](https://github.com/spaceflint7/bluebonnet/releases) page.
#### .NET Project Using DotNet Tool
- Create a new directory for the project, and change to this directory.
- Create a .NET Core project called `DotNet`:
- `dotnet new console -n DotNet`
- You may use some other project type or language instead of a C# console app.
- The project should be named `DotNet`.
- Edit `DotNet.csproj` to add a reference to Android DLL created by Bluebonnet:
$(BLUEBONNET_DIR)\Android.dll
- Or, you can create the project using Visual Studio:
#### .NET Project Using Visual Studio
- Create a new project.
- Select a __C# Console Application__ template (.NET Framework or .NET Core).
- Specify `DotNet` for the project name, and check the box to __Place solution and project in the same directory__.
- Or specify some other project name and clear that checkbox, but make sure:
- The solution name is `DotNet`.
- In __Project Settings__ in Visual Studio, set the __Assembly name__ to `DotNet`.
- Complete the creation of the project and solution.
- Add a reference to `Android.dll` using Visual Studio, or by editing the project file as shown above.
#### Activity Class
- Leave the `Program.cs` file as it is.
- Otherwise you may get a compilation error about a missing `Main` method.
- The class in this file will be discarded from the output, unless explicitly referenced.
- Create a new file named `MainActivity.cs` and paste the following into it:
#if ANDROID
namespace com.whatever.example
{
public sealed class MainActivity : android.app.Activity
{
protected override void onCreate (android.os.Bundle savedInstanceState)
{
android.util.Log.i("EXAMPLE", ">>>>>>>> EXAMPLE ACTIVITY <<<<<<<<");
base.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
#pragma warning disable IDE1006 // Must begin with uppercase letter
#pragma warning disable CA2211 // Non-constant fields should not be visible
[java.attr.Discard] // discard in output
public class R
{
[java.attr.Discard] // discard in output
public class layout
{
[java.attr.RetainType] public static int activity_main;
}
}
}
#endif
#### Android Project in Android Studio
- Create a new project.
- Select the __Empty Activity__ template.
- This generates activity definitions in the `AndroidManifest.xml` file.
- For this example, set __Package name__ to `com.whatever.example`.
- Set the __Save location__ to the project root directory.
- Ignore the warning that this directory is not empty.
- Select __Java__ for the language and a reasonable minimum platform API version (e.g. __18__).
- Complete the creation of the project.
- Delete the Java class generated by Android Studio for the main activity.
- It should be located in the project class `app/java/com.whatever.example/MainActivity`
- Alternatively, delete the entire directory of Java source files - `app/src/main/java`
#### Gradle Build Script
- Open the __module__-specific build script.
- In Android Studio, this is the `build.gradle` file annotated with __Module__ (rather than Project).
- In the directory structure, this is `app/build.gradle` (rather than the top-level file with the same name).
- Note that the right file begins with a `plugins` section followed by an `android` section.
- Optional: Between the `android` section, and the `dependencies` section, insert:
buildDir = "${project.rootDir}/build/${project.name}"
- This sets the output build directory just below the top-level of the project.
- Otherwise the default is the `app/build` directory.
- At the top of the `dependencies` section, insert:
implementation files("$buildDir/dotnet/dotnet.jar")
implementation files("${System.env.BLUEBONNET_DIR}/baselib.jar")
- After the `dependencies` section, append:
task buildDotNet {
doLast {
delete("${buildDir}/dotnet/dotnet.jar")
exec {
workingDir "${project.rootDir}"
commandLine System.env.MSBUILD_EXE ?: 'msbuild.exe',
'DotNet', '-r',
'-p:OutputType=Library',
'-p:Configuration=Release',
'-p:DefineConstants=ANDROID',
"-p:OutputPath=$buildDir/dotnet",
"-p:IntermediateOutputPath=$buildDir/dotnet/intermediate/"
}
exec {
commandLine "${System.env.BLUEBONNET_DIR}/Bluebonnet.exe",
"${buildDir}/dotnet/dotnet.dll",
"${buildDir}/dotnet/dotnet.jar"
}
}
}
preBuild.dependsOn buildDotNet
- Note the use of environment variables in the build script:
- `MSBUILD_EXE` should specify the path to the `MSBuild.exe` program.
- `BLUEBONNET_DIR` should specify the directory containing __Bluebonnet__ binaries.
- These environment variables should be made visible to Android Studio.
#### Gradle Build Script - Sample
- After updating your `app/build.gradle` file, it should look (more or less) like this:
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.whatever.example"
minSdkVersion 18
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
buildDir = "${project.rootDir}/build/${project.name}"
dependencies {
implementation files("$buildDir/dotnet/dotnet.jar")
implementation files("${System.env.BLUEBONNET_DIR}/baselib.jar")
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
task buildDotNet {
doLast {
delete("${buildDir}/dotnet/dotnet.jar")
exec {
workingDir "${project.rootDir}"
commandLine System.env.MSBUILD_EXE ?: 'msbuild.exe',
'DotNet', '-r',
'-p:OutputType=Library',
'-p:Configuration=Release',
'-p:DefineConstants=ANDROID',
"-p:OutputPath=$buildDir/dotnet",
"-p:IntermediateOutputPath=$buildDir/dotnet/intermediate/"
}
exec {
commandLine "${System.env.BLUEBONNET_DIR}/Bluebonnet.exe",
"${buildDir}/dotnet/dotnet.dll",
"${buildDir}/dotnet/dotnet.jar"
}
}
}
preBuild.dependsOn buildDotNet
#### ProGuard Rules
- If you wish to minify your release build using Android R8 (ProGuard), enter the following settings into your `app/proguard-rules.pro` file:
#
# these rules prevent discarding of generic types
#
-keepclassmembers class * implements system.IGenericEntity {
public static final java.lang.String ?generic?variance;
public static final *** ?generic?info?class;
private system.RuntimeType ?generic?type;
public static final *** ?generic?info?method (...);
(...);
}
- Only if you wish to use F#, then enter the settings below as well. They are needed with C# code.
#
# F# printf
#
-keepclassmembers class microsoft.fsharp.core.PrintfImpl$ObjectPrinter {
*** GenericToString? (...);
}
-keepclassmembers class microsoft.fsharp.core.PrintfImpl$Specializations* {
*** * (...);
}
-keep class microsoft.fsharp.core.CompilationMappingAttribute { *; }
-keep class **$Tags { *; }
-keepclassmembers class * implements java.io.Serializable {
*** get_* ();
}
-keepattributes InnerClasses
#### To Summarize of All of the Above
- In place of Java source files, a new dependency was added on a JAR file - `dotnet.jar`.
- The `buildDotNet` task was defined to perform the following build commands:
- Run `MSBuild` on the .NET project, in `Release` configuration, with the preprocessor define `ANDROID`
- Run `Bluebonnet` on the resulting `dotnet.dll` to create `dotnet.jar`
- The `buildDotNet` task was set to execute before the Gradle `preBuild` task.
- ProGuard rules were added to prevent stripping fields and methods used by Bluebonnet to support .NET generic types.
#### Test It
- Compile and run the project in Android Studio.
- If all goes well, the example app should start in the emulator, and display Hello World!
- This layout comes from the `activity_main.xml` layout file, generated by Android Studio.
- `onCreate()` in the C# `MainActivity` class calls `setContentView` to inflate this layout.
- The __Logcat__ tab should show the debug log message printed by `MainActivity.onCreate()`