Sharing code between iOS and Android using J2ObjC
This article is part of a talk done at the App Builders conference in Lugano that took place on the 30th of April 2019. Another part talking about code sharing between iOS and Android using C++
is available here.
Why sharing code?
At Ubique, we like to write native apps, in our opinion the best UX is achieved with fluid and native UI. If the presentation layer needs to be native, on the contrary the business logic can be shared. When business logic is shared then it is written once, therefore reducing maintenance, and allowing us to enforce a similar behaviour between iOS and on Android.
What is J2ObjC?
As the official docs says, J2ObjC is a transpiler or in other words source-to-source compiler. It will convert your Java code into Objective-C code. So at the end of the day, your code will run natively in Java on Android and in Objective-C on iOS. A more in depth explanation can be found at https://developers.google.com/j2objc/ alongside a detailed installation and different setup.
What’s this article about?
This article will illustrate how the engineering department at Ubique used the J2ObjC setup to achieve code sharing between our Android projects and iOS. It will show how we installed it and some sample code.
Bear in mind there might be different techniques to achieve the same result (some are better of course) but that’s the way that worked for us and we would be happy to see in the comments the approach you took.
How does it work?
The guys behind J2ObjC took most of the Java Runtime Environment and translated it into Objective-C. So when you return an ArrayList
from your Java code, what J2ObjC will do is to translate that Java code to return a subclass of IOSArray
. So under the hood there is no Java involved, only Objective-C code that is written to offer the same functionalities as the JRE library.
Requirement
You will need a Mac running OS X 10.11 or higher with Xcode 7 or up installed. The documentation asks for a JDK of 1.8 or higher, but digging through their code I found that only the JDK 1.8 is supported.
Installation
- Head to the app store and download Xcode.
- Check your Java version by entering
java -version
in a terminal. It should be 1.8. If non is present or you have a different version you can always install the JDK 1.8 from this link https://www.oracle.com/technetwork/java/javaee/downloads/jdk8-downloads-2133151.html - We need to get a compiled version of the JRE translated into Objective-C, we can do that by downloading any release ZIP file from https://github.com/google/j2objc/releases. There is also the possibility to build from source but we will not cover it here. At the time of writing, we used release 2.3.1
- Optional: Rename the folder to j2objc and move it to a location where all your projects can access it.
Setting up Xcode
Create or open an Xcode Project. In our case we created a single view iOS application project called J2ObjCTest.
Linking the JRE:
- Open the Project Editor and make sure the application target is selected. In your case your iOS app.
- Navigate to the Build Settings tab and search for Other Linker Flags. If it is not showing make sure the All tab is selected (as shown in the screenshot below in blue)
- Select the row and add:
-ljre_emul -ljre_util -liconv
This will link the JRE emulation library.
Specifying JRE Home path
In order for Xcode to know about the JRE and to compile we need to specify where the JRE is.
First, we need to define a path to our JRE that we downloaded earlier.
If you want to specify a JRE path per project, then follow theses steps. If you want to have only one JRE for all projects then, skip to the next method.
1st Method: On a project base
- In the Build Settings hit the + (near the search bar) and select Add User-Defined Setting.
- Name the setting to
J2OBJC_HOME
and set the value to the J2objC folder (in our case j2objc)
2nd Method: Global path
- Open Xcode preference
- Navigate to the Locations tab
- In the Locations tab check for a sub-tab named Custom Paths
- Hit the + in the bottom
- Name the path and the display name as
J2OBJC_HOME
- In the Path filed input the path to the JRE
Updating the Search Path
Once we have a variable that holds the JRE we can update our search paths.
In the Build Settings under Search Paths append to the:
- Framework Search Path:
${J2OBJC_HOME}/frameworks
- Library Search Path:
${J2OBJC_HOME}/lib
- User Header Search Paths:
${J2OBJC_HOME}/include
Where to put the Java shared code?
There are multiple ways to host and distribute the shared Java code. At Ubique, we have it hosted as a separate Git repository and use submodules to integrate it in our Xcode project. I will not go into details, but here is another article illustrating how to do that https://medium.com/@aestusLabs/using-git-submodules-with-xcode-tutorial-for-ios-dcfc28a82c20
The main point is to have your Java code under your Xcode project. Later we will be adding the Java files to the target and letting Xcode run J2ObjC for us every time we compile.
For this example I just created a folder in my project and named it shared
and put inside my Java code. But you can see how this shared
folder can be coming from a Git repo.
Transpiling Java code with a Build Rule
Now we need to tell Xcode that if it encounters a Java file it needs to run the J2ObjC command line tool. This is done via the Build Phase.
- Make sure that all Java files to transpile are included in your target
- Navigate to your Project
- Select your app target
- Navigate to the Build Rules tab.
- Hit the + icon on top (near All | Custom toggles)
- Rename the new rule to reflect it’s the J2ObjC rule (Optional)
- Select Process: Java source files
- Select Using: Custom script:
- Paste the following (We will explain it later)
if [ ! -f "${J2OBJC_HOME}/j2objc" ]; then echo "J2OBJC_HOME not correctly defined in Settings.xcconfig, currently set to '${J2OBJC_HOME}'"; exit 1; fi;"${J2OBJC_HOME}/j2objc" --swift-friendly -v -g -d ${DERIVED_FILE_DIR} -sourcepath "${PROJECT_DIR}/shared" --no-package-directories ${INPUT_FILE_PATH};
- Modify the option
-sourcepath
to match your shared Java code repo - Add to the Output Files below the script part:
${DERIVED_FILE_DIR}/${INPUT_FILE_BASE}.h
and${DERIVED_FILE_DIR}/${INPUT_FILE_BASE}.m
- Add to the compiler flags
-fno-objc-arc
next to the.m
entry. We will be transpiling code with manual reference counting as recommended by J2ObjC team here. Therefore we want to disable ARC only for the generated files, but keep ARC for the rest of the application.
Script Explanation
- The first line makes sure that we have our J2ObjC path set correctly.
- The next line is executing the
j2objc
script specifying that it should be swift friendly (--swift-friendly
), verbose (-v
), generate Java source debugging support (-g
) we will see later how we can have breakpoints in our Java code and creating no folder per package, omit creating a folder structure on a package base (--no-package-directories
), convert Javadoc to Xcode comments (--doc-comments
). - Important: Select correctly the source path (
-sourcepath
) of your Java files otherwise the Java transpilation will not work. In our case we put the shared Java code in ashared
folder inside the root.
Calling Java transpiled code
Finally, after all the setup we’ve reached a place where we can start to be productive. As you might have seen, J2ObjC will output Objectve-C code (Duh!).
Objective-C
If your project consists of only Objective-C code, then I would recommend having a prefix header file (PCH file) to your project and adding#import "JreEmulation.h"
. Otherwise, you can import it in each .m
file.
Swift
If your project is a Swift project, it’s a bit trickier to set up as we need to add a Bridging Header to the project. (If you don’t know how to add a bridging header: Either just create an Objective-C class, then Xcode will automatically propose to create one for you, or follow this link to create it manually)
Next, open your bridging header and add the following
// Import the Java Runtime Environment
#import "JreEmulation.h"// Import all the Java classes below
#import "SharedJavaCode.h"
As you can see, you need to import all the classes that have been tranpiled from Java otherwise they will not be visible in Swift.
After that you should be able to call your shared code from within Swift.
NB: If you are running the project the first time or after a Clean Build, you need to:
- Comment out all the imports in the Bridging header.
- Build, (it will fail don’t be surprised)
- Comment back in the imports
- Build, this time it succeeds
Explanation: This step is necessary as there appears to be a build order issue with Xcode in that it will attempt to compile Swift code before dependent code (i.e. the dependent Java code which is to be translated via a build rule). This is really annoying but so fare we where not able to find a solution for that problem.
Things to worry about
Reference Cycles
Let’s take for example this Java code
// Filename Car.java
package ch.ubique;
public class Car {
Person owner;
}// Filename Person.java
package ch.ubique;
public class Person {
Car vehicle;
}
This code will generate a reference cycle, but the Java garbage collector can handle that and remove both object if no longer used (unreachable objects).
When transpiled into Objective-C this code will leak. One way to solve this is to use the annotation@Weak
before the property.
// Filename Person.java
package ch.ubique;import com.google.j2objc.annotations.*;public class Person {
@Weak Car vehicle;
}
There are other ways to break a cycle and to detect cycles in your Java code. This article explains in detail the problems and solutions when it comes to retain cycles http://j2objc.blogspot.com/2017/01/breaking-retain-cycles-with-weak-and.html
Multithreading and Memory access safety
J2ObjC support the translation of the Java Thread
class, thus allowing some sort of multithreading. This is useful if you are building a shared code that handles lengthy calls or work can be split into multiple independent pieces.
This raises an issue with sharing memory and accessing it from different threads. J2ObjC maps the synchronized
keyword to the Objective-C @synchronized
.
If you are building iOS apps for iPhones 5s and newer (Apple Watch 2 and newer or Apple TV 4 and newer) than all types (int, long, double…) are read and write atomic. There is no guarantee for 32-bit architecture and the use of volatile
is needed.
Also J2ObjC provide full support for java.util.concurrent.atomic
You can read more about J2ObjC memory model from https://developers.google.com/j2objc/guides/j2objc-memory-model
Confidence in the transpiled code and testing
After transpiling the code, there is no guarantee that it will behave like the original Java code. This is why it is encouraged to write JUnit test for the shared Java code and use J2ObjC to translate these tests and run them on Objective-C code. This verifies the semantics of the code.
You can find examples on how to translate JUnit tests on the J2ObjC official website https://developers.google.com/j2objc/guides/translating-junit-tests
A practical Example
Before we jump into an example, you can read about the conventions for translating methods and properties here.
So for this example I have chosen the open source project Game Of Life written in in Java by Roman Ring. Please download the project from https://github.com/inoryy/game-of-life-java
This implementation in particular is suited for our case because it has 2 logical parts: The core logic and the display.
What is interesting for our case is to be able to reuse the core of an iOS application.
Copy the core into the project and make sure to include all the Java files to the current target. Your project should look like that:
Now open the bridging header and edit it to look like that:
// Import Java Runtime Environment
#import "JreEmulation.h"// Import Game of Life Core
#import "Board.h"
#import "Cell.h"
#import "DisplayDriver.h"
We are ready to include the logic in a UIViewController. Open the ViewController.swift file and modify it to look like that:
If you Run the project you should get something similar to this:
Summary
We used the core that was provided as a Java code to run an iOS interface. This was possible because Xcode is transpiling our Java code into Objective-C code that Xcode is compiling afterwards. The Bridging Header file exposes these classes to Swift. As you can see, the business logic was implemented in Java (reused from another project) and the UI is implemented in Swift.
Github Repository
We have a sample project available at https://github.com/joseph-elmallah/J2OBJCBase
Custom names for functions and packages
Sometimes the default naming of Java function is not clear when used in Objective-C or Swift. This is why J2ObjC gives us the possibility to specify the an alternate naming.
- Start by importing
annotations
- Add
@ObjectiveCName("<your renamed function>")
// We need this import otherwise the Java code will not recognise the annotationimport com.google.j2objc.annotations.*;@ObjectiveCName("getNearbyStationWithLatitude:longitude:")
public void getNearbyStation(float latitude, float longitude) { // Your Java code}
In case you want to change how J2ObjC rename classes with packages, then you need to specify a prefixes.config
file that you pass to the j2objc
script.
In the Build Rules add to the command this parameter with the path to your config file.
--prefixes ${PROJECT_DIR}/path/to/prefixes.config
The prefix.config
file should look something like that:
com.vals.a2ios.sqlighter=
com.vals.a2ios.sqlighter.intf=
ch.ubique.viadi.stations=ViadiStations
All packages that are equal to an empty string will not generate a naming with the package appended. So if we have a Java class Request
in the package com.vals.a2ios.sqlighter
by default the class name in Objective-C will be ComValsA2iosSqlighterRequest
. But after specifying the line com.vals.a2ios.sqlighter=
the transpiled class name in Objective-C becomes Request
. Same for the line ch.ubique.viadi.stations=ViadiStations
every class in the ch.ubique.viadi.stations
will have the prefix ViadiStations
.
Documenting your code
If your shared Java code contains Javadoc documentation then you can generate Xcode documentation by adding the flag --doc-comments
to the j2objc
command in the Build Rules.
Debugging
If the flag -g
is set, then the j2objc
command will generate debugger support for stepping into the Java code. In your Xcode project you can select a Java source file and place breakpoints like you do in any .m
or .swift
file.
When debugging, the App will break and show the .java
file where you can step through the Java code. Of course in the Variable View you will see the Objective-C equivalent objects and not the Java objects.
Conclusion
Using J2ObjC should be primarily for sharing business logic between Android and iOS apps. We saw using the simple example how a code written in Java could be integrated into an iOS app.
Advantages
- Same logic shared between iOS and Android = Same behaviour and less divergent
- Shared code = Faster implementation, code once use twice ;)
- One code = Less maintenance and faster bug fixing
- Forces the team to think in reusability and modularity
- Any developer with Java skills can write code. Great in case of shortage of iOS developers
- Can debug Java code in Xcode while running an iOS app
Disadvantages
- The code is transpiled so there is a risk of being wrongly translated. The only way to ensure that the translation worked is via Unit tests. J2ObjC can also transpile JUnit tests, read more about it here
- There is a lot of casting when used in Swift. Unless you want to have tons of
guard
statement, force unwrapping is the only option - In mixed projects (Objective-C and Swift) sometimes the auto-completion will only work for one language and not the other
- Iterating over java arrays can be painful if they do not implement the NSFastEnumeration protocol
- Slowing down build time and increasing your app size
- Forcing the use of Objective-C through bridging headers in pure Swift projects
- Most of the time the function name is not clear enough and need manual redeclaration to be readable in Swift and Objective-C
- We are limited to use only the available translated JRE classes
- Third party libraries should be also compatible with J2ObjC and not all libraries can be transpiled and used