Sometimes I find myself wanting to evaluate some Android code without launching an app on a device or an emulator (and without adding log statements in random components). For example Java has a public static void main
method that can be added in any class and launched right from the IDE. Can this be done for Android?
public class MainActivity extends Activity {
public static void main(String[] args) {
final String input = "tel:+34 666 55 52 22";
System.out.printf("input: %s, uri: %s%n", input, Uri.parse(input));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Unfortunately this will fail with somewhat famous exception:
Exception in thread "main" java.lang.RuntimeException:
at android.net.Uri.parse(Uri.java:293)
at io.noties.psvm.MainActivity.main(MainActivity.java:11)
This happens because our code is compiled with android.jar
dependency (that can be found in $ANDROID_SDK/platforms/android-${version}/android.jar
) that has only public API definitions. All API method signatures are present but their bodies contain a single statement: throw new RuntimeException("Stub!")
. android.jar
here is a runtime dependency. We still can operate on public API during development, but actual implementation is provided by a device or an emulator when an app is launched.
I labeled the java.lang.RuntimeException: Stub!
as somewhat famous because you might've seen it whilst trying to launch java unit tests that contain Android code. This problem is long gone with the help of amazing Robolectric testing framework. But wait... this must mean that Robolectric has a proper android.jar
with all the methods present. Can we use it?
It turns out that we can. We should substitute the default mocked android.jar
with the one that comes with Robolectric. Add this to your build.gradle
:
dependencies {
debugCompileOnly 'org.robolectric:android-all:9-robolectric-4913185-2'
}
You can find different versions of android-all
artifact on Robolectric maven page. As you might've noticed artifact version contains Android platform version in it, so 9-robolectric-4913185-2
would mean Android Pie (9)
. You can go with any Android version down to 4.1.2_r1-robolectric-0
(Jelly Bean!) which can be helpful if you want to evaluate code on a specific platform.
We've used the debugCompileOnly
when adding Robolectric dependency so we do not actually introduce a compile-time dependency on full android.jar
. But the good thing is — Intellij still allows to bundle runtime dependencies when launched from the main
method.
With that in place let's launch our main method again, click the green play icon
to the left of your main method definition:
And whoops:
Exception in thread "main" java.lang.RuntimeException:
at android.net.Uri.parse(Uri.java:293)
at io.noties.psvm.MainActivity.main(MainActivity.java:11)
There is actually another step that is required in order to launch the main method with Robolectric android.jar
. After you have clicked the launch
button, IDE created a launch configuration which we need to modify. Expand the configurations menu and click Edit Configurations...
Then,
- Make sure
Include dependencies with "Provided" scope
is checked - Then locate the
JRE
menu option and expand it - Select appropriate Java version (instead of
Android API...
)
Apply the changes to the configuration and close the window. Now, with that in place we finally can launch the main method.
input: tel:+34 666 55 52 22, uri: tel:+34 666 55 52 22
Process finished with exit code 0
The good thing is this does not interfere with your Robolectric unit tests and both can be used inside a single module. Another good thing is this allows you to evaluate code that contain Android specifics without launching it on a device or an emulator.