Ocean Village - A dev blog

How to - AAR + Unity

May 11, 2020 - Monday evening

I want to replace the default Unity launcher with my own to kick off some tasks before Unity loads, as well as start up a service to make some api calls on exiting. To do this, I decided on creating an Android library and including it into my Unity build as an AAR.

Unity supports Kotlin files as assets, so you could also just create your MainActivity.kt file and drop it into Assets and override with an AndroidManifest.xml - but you'd be missing Android Studio's intellisense, and you'd have to rebuild the Unity app to see your changes on device. If you don't need to change too much, this is definitely the easiest path.

The best way I could find to include an AAR into Unity is to publish the library module to maven local, and add it as a dependency by overriding the default gradle templates. If you plan to include an AAR with no dependencies, you could also just drag and drop the AAR file like Unity docs say. AAR's don't come with their dependencies - though you could include them as JAR's and manually update them whenever they change. Publishing and adding as a dependency generates the pom.xml file for you and pulls in those transitive dependencies automatically when Unity runs the gradle build.

Note: If you do try the drag and drop route and run into `Unable to instantiate activity`, `ClassNotFoundException` it may actually be a dependency issue. This is not a well-named exception and led me on a goose chase trying to understand Android manifest merging, digging through the apk with ClassyShark and double-checking names for way too long.

🔢 To include an Android library as an AAR, by the numbers

  1. Export your Unity project and import with Android Studio

  2. Create your Android library module

  3. Copy unity-classes.jar from the unityLibrary module's /libs folder into your module's /libs, then include it as a compileOnly dependency

  4. If you're using Kotlin, you may need to add a Kotlin dependency to the root build.gradle. To use maven-publish as in the next step, you'll also need to use Gradle 3.6+

    allprojects {
        buildscript {
            ext {
                kotlin_version = '1.3.61'
            }
    
            ...
    
            dependencies {
                classpath 'com.android.tools.build:gradle:3.6.3'
                classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
            }
        }
    }
  5. Add to your module build.gradle (you can also override other properties)

    apply plugin: 'maven-publish'
    
    version = "1.0.0"
    group = "com.example.customplayer"
    
    ...
    
    afterEvaluate {
        publishing {
            publications {
                release(MavenPublication) {
                    from components.release
                }
            }
        }
    }
  6. Add your module as an implementation dependency to the launcher module and run

  7. ??? (development goes here)

  8. Run the publish release to maven local gradle task

  9. Make sure you're on Unity version 2019.3.7f1 and up. Go to project settings -> player -> publishing and enable:

    • custom main manifest (if you're replacing the launcher activity)
    • main gradle template
    • base gradle template

    These options should create some files in Assets/Plugins/Android

  10. Add to baseProjectTemplate.gradle

    allprojects {
        repositories {
            ...
    
            mavenLocal()
        }
    }
  11. Add to mainTemplate.gradle

    dependencies {
        ...
        implementation ("com.example.customplayer:customplayer:1.0.0@aar") {
            transitive=true
        }
    }
  12. Unity will overwrite your settings.gradle when you export to the same folder, you can add a settingsTemplate.gradle to replace the default one. Adding INCLUDES as mentioned in docs will break the build as it doesn't get replaced. You can check unityRoot/Temp/gradleOut/settings.gradle to see what's produced and add your module to it. This works:

    include ':launcher', ':unityLibrary', ':customplayer'
  13. If you're replacing the Unity launcher activity, modify AndroidManifest.xml in Assets/Plugins/Android with your activity's class name.

    <manifest ...>
        <application>
            <activity android:name="com.example.customplayer.CustomPlayer" .../>
            ...
        </application>
    </manifest>

During development:

  • To see Unity changes as you're working in Android Studio - export the Unity project to the Android project folder again.

  • To build the Unity app with your Android library updates - run the publish to maven local task again.

That's it!


🔥 Now for a hot Kotlin tip

As someone new to Kotlin, concurrency felt very complicated, especially coming from languages with simple primitives like async / await. You have to generate scopes and manage coroutine structures manually. The Kotlin language guide starts off with GlobalScope as the way to go, but don't stop there as the internet says this is bad to actually use - especially in an Android application.

It took a bit of searching and trial, but what I found to be the simplest way to just make an API call from an Activity is this (using kotlinx-coroutines):

class CustomPlayer :
    UnityPlayerActivity(),
    CoroutineScope by CoroutineScope(Dispatchers.Default) {

    val queue: RequestQueue by lazy {
        Volley.newRequestQueue(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        launch {
            val googleAsAString = getGoogle()
            Log.i("CUSTOM_PLAYER", "Here's google: $googleAsAString")
        }
    }

    suspend fun getGoogle() = suspendCoroutine<String> {
        val req = StringRequest(
                Request.Method.GET,
                "https://www.google.com",
                Response.Listener {
                    result -> it.resumeWith(Result.success(result))
                },
                Response.ErrorListener {
                    error ->  it.resumeWith(Result.failure(error))
                })

        queue.add(req)
    }
}

Lines 3, 12 and 18 get you async. It's another reason why Android development needs to be in Android Studio - without the await (come on Kotlin, you save one keyword!) you'll need the editor's icon to help you figure out where execution pauses (hint, line 13).

Other than my struggling with concurrency a bit, Kotlin has been great so far. I wish Volley had some documentation, an online API reference would be really useful - well, Kotlin documentation in general could use some TLC (though this is true of almost all documentation, everywhere).

It feels nicely expressive and safe, but also like there's a large sprinkling of magic throughout. If your last argument is a callback, you can add the lambda outside of the parameter list. Why? Why not. If you use $ in a string, it will stringify the variable - but only up to the first dot. I don't feel like I 'get it' yet, but it's definitely a lot nicer to write than Java. Not that Java's bad, especially the modern stuff.

Of course, everything pales in comparison to F# (lolol). We all have our preferences!


Say hi! Enable JS to see my email (or send to this domain)