11 KiB
Goal
-
Set up development of an Android app using a .NET language.
- Either in Visual Studio or using the command line
dotnet
tool.
- Either in Visual Studio or using the command line
-
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 toMSBuild.exe
program file.- For example,
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe
- For example,
-
Set the environment variable
BLUEBONNET_DIR
to point to a directory containingBluebonnet.exe
,Baselib.jar
andAndroid.dll
. -
These files can be downloaded from the 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:<ItemGroup> <Reference Include="Android"> <HintPath>$(BLUEBONNET_DIR)\Android.dll</HintPath> </Reference> </ItemGroup>
-
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.
- This generates activity definitions in the
-
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 anandroid
section.
-
-
Optional: Between the
android
section, and thedependencies
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 theMSBuild.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 (...); <init>(...); }
-
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, inRelease
configuration, with the preprocessor defineANDROID
-
Run
Bluebonnet
on the resultingdotnet.dll
to createdotnet.jar
-
-
The
buildDotNet
task was set to execute before the GradlepreBuild
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 callssetContentView
to inflate this layout.
-
-
The Logcat tab should show the debug log message printed by
MainActivity.onCreate()