Posted: 18th November 2018

Author: Ric

Tagged: Tutorials

Creating a Corona Native plugin with Android Studio

We picked up a new client project recently. Without going into too much detail, in a nutshell the brief is to develop a mobile app that incorporates a measurement tool, and will eventually need releasing on both Android and iOS. Weighing up the pros and cons of building such an app in the various cross-platform frameworks available, I opted for Corona SDK. I love Corona - it's built around the Lua programming language which I find really intuitive and quick to develop in, and although it's prodominantly a game programming kit, there's no reason why general apps can't be built with it too.

For the measurement tool we need users to be able to see what they're pointing their phone at, and to do this we want to show a camera preview in the background. Corona has a really nifty feature for this - you can simply create a standard rectangular display object and fill it with the camera preview. I'd planned this part of the app around this ability, but it turns out this only works on iOS, and we need Android support first...

The Android API does offer camera device access at this level, so with native Android code (Java) it's possible to receive a camera preview image to a surface object. One really cool feature of Corona is the ability to bridge between Lua code and native device code, by creating native plugins. On Android, a native plugin is a Java library that you load in to your Corona project during the compile, opening up its methods to your Lua code. As far as I understand it, when you build a Corona project, Lua is compiled into native Java when building for Android, or native C when building for iOS, so native plugins are simply pre-compiled Java or C classes that get added in during this phase, which is pretty logical really.

Since Corona doesn't offer a native camera preview method for Android, I decided to learn native plugin development. This proved to be a difficult process as there aren't any concise tutorials available, and the documentation and forum posts I managed to find were exceptionally brief and/or out of date, making references to things like Corona Native which is a tool for Mac users only, or Corona Enterprise which is now discontinued. Put simply, I struggled to figure out a lot of the steps needed or how to work with Android Studio, despite the fact that creating a native plugin is actually pretty easy.

So, if like me, you're trying to figure out how to build a native Android Java plugin for Corona SDK, here's a proper, concise tutorial concatenating the various steps I found throughout the documentation, my own findings, and some much appreciated guidance from Corona staff member, Vlad. This tutorial assumes that you're using Windows and building for Android. The steps for Mac users building for iOS differ.

Setting up

  1. Corona provides a handy template to start from and it's available within your Corona SDK installation. Copy \Native\Project Template\App from your Corona install directory and paste it somewhere writable. On Windows, Corona would usually be installed to \Program Files (x86)\Corona Labs\Corona.

  2. Rename App to your project name. For the purpose of this tutorial, I'll assume you called it My First Plugin.

  3. Install and launch Android Studio.

  4. Android Studio will launch to a menu screen. Choose Open existing Android Studio project and select the android folder from inside the My First Plugin folder you just created.

  5. Android Studio will start processing some Gradle files and might result in errors because the template references older versions of various resources, like SDK build tools. Use the Update [foobah] version and sync project links to resolve.

  6. From the icons in the top right, select SDK Manager and make sure at least one SDK platform is installed. I installed API 28, Android 9 (Pie).

  7. Also from the icons in the top right, select AVD Manager and create a new virtual device using the installed API 28 image. At the time of writing, API 25 was the last to include an AMD compatible image so if like me, you're using an AMD system, the virtual device won't actually work. Instead, the easiest work-around for AMD users is to enable Developer Mode on a real Android device. To do that, open Settings -> About Phone and tap Build Number 7 times, then go into the Settings -> Developer Options menu that appears and switch on USB debugging. Now when you plug your device into your PC, you'll be able to build to it instead of to a virtual device.

  8. If you opted to use a real device instead of a virtual, navigate to File -> Settings -> Build, Execution, Deployment -> Instant Run and untick to disable. Unfortunately Instant Run prevents a few core libraries from copying over properly and in turn causes Corona apps to produce errors on start-up. Most notably, the Corona splash screen won't be included which causes the app to terminate immediately.

  9. Finally, open the Project tab from the left of Android Studio and unfold android -> plugin -> src -> main -> java so that you can see plugin.library. Right-click on this and then Refactor -> Rename to replace with something more fitting. For the purpose of this tutorial, I'll assume you called it myFirstPlugin. A simulation will happen first, resulting in a Do refactor button at the bottom of Android Studio. Tap this to process.

The Java plugin itself

  1. Uncollapse plugin.myFirstPlugin and open the contained LuaLoader file. In a nutshell, this is the root of your Java plugin. It's the main Java class that Corona calls up when you require your plugin from the Lua code. Read over the notes throughout this file to understand the process better. You might also want to unfold the comment block at the very top and update the copyright info that it contains.

  2. At the top of this file, the first line should be a package name that matches the name you renamed plugin.library to. Refactoring in Android Studio churns through all of the project files to replace old references with new references when you rename things, so lines like this throughout Java classes, Android manifests, and Gradle configurations should be kept in sync for you automatically.

  3. Scroll down to the invoke method and add the following lines to the NamedJavaFunction block, to register a couple of new functions that we're going to write. Android Studio will flag these in red for now, because they don't exist.
new start(),
new stop(),
new status()
  1. Right-click your plugin.myFirstPlugin folder again, and this time tap New -> Java Class. Name the class start and add the following code:
package plugin.myFirstPlugin;

import com.naef.jnlua.LuaState;
import com.naef.jnlua.NamedJavaFunction;
import com.ansca.corona.CoronaLua;
import com.ansca.corona.CoronaRuntime;
import com.ansca.corona.CoronaRuntimeListener;
import com.ansca.corona.CoronaRuntimeTask;
import com.ansca.corona.CoronaRuntimeTaskDispatcher;
import com.ansca.corona.storage.FileContentProvider;
import com.ansca.corona.storage.FileServices;

public class start implements com.naef.jnlua.NamedJavaFunction {
    // This reports a class name back to Lua during the initiation phase.
    @Override
    public String getName() {
        return "start";
    }

    // This is what actually gets invoked by the Lua call
    @Override
    public int invoke(final LuaState luaState) {
        shared.status = "running";

        return 1;
    }
}
  1. Repeat the process to add a stop class:
package plugin.myFirstPlugin;

import com.naef.jnlua.LuaState;
import com.naef.jnlua.NamedJavaFunction;
import com.ansca.corona.CoronaLua;
import com.ansca.corona.CoronaRuntime;
import com.ansca.corona.CoronaRuntimeListener;
import com.ansca.corona.CoronaRuntimeTask;
import com.ansca.corona.CoronaRuntimeTaskDispatcher;
import com.ansca.corona.storage.FileContentProvider;
import com.ansca.corona.storage.FileServices;

public class stop implements com.naef.jnlua.NamedJavaFunction {
    // This reports a class name back to Lua during the initiation phase.
    @Override
    public String getName() {
        return "stop";
    }

    // This is what actually gets invoked by the Lua call
    @Override
    public int invoke(final LuaState luaState) {
        shared.status = "waiting";

        return 1;
    }
}
  1. And a status class:
package plugin.myFirstPlugin;

import com.naef.jnlua.LuaState;
import com.naef.jnlua.NamedJavaFunction;
import com.ansca.corona.CoronaLua;
import com.ansca.corona.CoronaRuntime;
import com.ansca.corona.CoronaRuntimeListener;
import com.ansca.corona.CoronaRuntimeTask;
import com.ansca.corona.CoronaRuntimeTaskDispatcher;
import com.ansca.corona.storage.FileContentProvider;
import com.ansca.corona.storage.FileServices;

public class status implements com.naef.jnlua.NamedJavaFunction {
    // This reports a class name back to Lua during the initiation phase.
    @Override
    public String getName() {
        return "status";
    }

    // This is what actually gets invoked by the Lua call
    @Override
    public int invoke(final LuaState luaState) {
        luaState.pushString(shared.status);

        return 1;
    }
}
  1. The scope of this tutorial isn't to teach Java, but note the use of luaString.pushString() in that final class. This returns a string to Lua when Lua calls the status method. The start and stop methods just change the value of the variable returned. Note how the variable reference is shared.status and shared is currently rendered in red. In a nutshell, this is trying to load a public variable from the shared class, which doesn't yet exist.

  2. Repeat the process once more to add a shared class:
package plugin.myFirstPlugin;

public class shared {
    // global variables, basically
    public static String status = "waiting";
}
  1. At this point, we have a Java plugin consisting of start(), stop(), and status() methods which effectively just update a shared variable when start() or stop() are called, and pass that variable back when status() is called. The red entries throughout should now all be resolved and your first Java plugin is effectively complete. We still need to package it up and drop it into a Lua project though.

The Lua app

  • Outside of the Android Studio project, within the folder that you originally renamed from App there's another folder called Corona which contains a main.lua file as well as the usual build.settings and config.lua files. This is the app that Gradle is configured to build, so it's where your Java plugin can be tested. Open the main.lua up in whatever editor you'd usually use to develop Corona apps.

  • The file will already include some sample code, but currently is expecting to find plugin.library, which we renamed to plugin.myFirstPlugin. So edit the require() at the top of this file accordingly.

  • Save all, and select Run from the icons at the top. If it doesn't open automatically, select Build from the tabs at the bottom of Android Studio to watch the process. After all tasks are complete, the app should open in your virtual device or on your plugged in device - whichever set-up was performed in the above initial steps. The main.lua test app by default calls a show() function within LuaLoader.java which opens a web page and displays it in a Corona activity, so this is what you should see happen.

  • If this doesn't happen, choose Clean project from the build menu and once complete, try the run process again. I find that a Clean needs to be performed any time files are edited outside of Android Studio so you might need to do this often.

  • Now replace all of the code in main.lua to test the new Java classes that we added:
local library = require "plugin.myFirstPlugin"

print("Status is currently: " .. library.status())

print("Calling our start() function!")
library.start()

print("Status is currently: " .. library.status())

print("Calling our stop() function!")
library.stop()

print("Status is currently: " .. library.status())

print("Done!")
  1. Repeat the Clean and Run processes and this time, from the bottom of Android Studio open the logcat view. From the second drop-field you should be able to select your app as the debug process, and from the filters drop-field on the right you should be able to switch to only viewing output from this process. Now the logcat should only show console outputs related to your test app and, once the build process completes and the app is launched, the above code should return status changes that prove your plugin works.

  2. You now have a working Java plugin and a working Corona app that bridges Lua and Java thanks to its built in JNLua implementation. Meaning you can extend the capabilities of your Lua app by writing native Java code as methods that can be invoked with just a couple of lines in Lua.

  3. When you're ready to deploy your plugin to a real app, open the build.gradle file from inside the android -> plugin tree and scroll down to the exportPluginJar task. Note the reference to /outputs/jar. Uncollapse android -> plugin -> build -> outputs and in theory, you should find a compiled .jar file ready to copy over to your Lua project at this location when you perform the Run process. Unfortunately, at the time of this writing with Android Studio 3.2.1 the exportPluginJar task is broken so as a workaround, open the Gradle tab from the right of Android Studio, navigate to android -> :plugin -> Tasks -> build, and double-click assembleRelease to run manually. Once complete, navigate again to android -> plugin -> build -> outputs and this time you should see an aar folder containing plugin-release.aar. Copy this and paste somewhere outside of your project.

  4. Extract the pasted aar file. You'll need something like 7Zip for that. Within the resulting files you'll find a classes.jar - rename to myFirstPlugin.jar. This is your new, native Java plugin, ready to be compiled in to a Lua project! Since the compile process of Corona apps is handled by Corona servers, you'll need to package your plugin up as per the documentation to get it into the Corona marketplace, and then you can require() it in to any of your Corona apps exactly like with the test app. Alternatively, you could continue to compile your Corona apps through Android Studio by replacing the test app, but you wouldn't then be able to use the Corona Simulator, and if your plugin is useful to others it should be in the marketplace anyway.

That's it, you're done!

Blog posts written by former QWeb employees are not necessarily an accurate indication of the current opinions of QWeb Ltd and the information provided in tutorials might be biased or subjective, or might become out of date.

Discuss this post

Leave a comment

Your email address is used to notify you of new comments to this thread, and also to pull your Gravatar image. Your name, email address, and message are stored as encrypted text and you won't be added to any mailing list and your details won't be shared with any third party.