Port Native Android App to iOS: Part II
Here in Part II, we will build the skeleton of the app.
Join the DZone community and get the full member experience.
Join For FreeIn Part I of this series we discussed the port of Swiftnotes to iOS by porting it to Codename One and covered what Codename One is and how it differs from Android.
As a reminder, the finished app is on Apple iTunes, Google Play, and the Microsoft store as well as online with a JavaScript build which you can see here.
The full source code of the app is here: https://github.com/codenameone/SwiftnotesCN1
Porting Tool
A while back we announced an open source project to ease the porting process cn1 android importer. This is a very basic tool that just scaffolds the new Codename One project from an Android project without really duplicating the UI or converting the code.
We can obviously improve this tool significantly but to do so we need to gauge community interest which so far has been "underwhelming". I will start by using this tool as it is now to get started quickly and then port the code.
The Porting Process
First, we need to get the pre-requisites:
- Download the Android importer from https://github.com/shannah/cn1-android-importer/blob/master/dist/ - note that at this time the files from the lib directory should also be downloaded but this should hopefully be resolved
- Download and unzip the Swiftnotes sources from https://github.com/adrianchifor/Swiftnotes/archive/master.zip
- Download NetBeans and install Codename One if you haven’t already
Step 1: Create a New Codename One Project
The instructions are for NetBeans but should work for IntelliJ as is. I’m not sure about Eclipse.
We create a standard Codename One project:
Figure 1. In the new project wizard, we select Codename One
The project name doesn’t matter much I just used SwiftnotesCN1
Figure 2. The project name and location
We then pick the class/package name, the package name should match the existing Android package name if you want to replace the original project. I also picked a native theme for simplicity and the barebone application to start from scratch:
Figure 3. We should use the same package name as the android project
After doing this I chose to refactor the app to the com.codename1 package space so we can run it side by side with the native app |
Step 2: Run the Conversion Tool
The conversion tool is very simplistic and preliminary. It had issues with some of the UI elements even with it’s limited support for these features.
If you show enough interest, file issues we’ll fix them and move this tool forward |
I used the following command to convert the project:
java -jar AndroidImporter.jar import-project -i Swiftnotes-master/app/src/main/res -o ~/dev/SwiftnotesCN1 -p com.moonpi.swiftnotes
The first argument is the resource directory for the original Android project. Followed by the output directory (the new Codename One project) and the package name where GUI files should be created.
This placed the localization bundles and imported the images, it also generated the GUI XML files.
To generate the GUI sources right-click the project and select "build" this will generate GUI source files for all the XML files.
Step 3: Bind Localization Code
This is really trivial and will allow us to see something running almost at once, open the main class in our case SwiftnotesCN1
. Edit the init(Object)
method to load the localization Strings:
public void init(Object context) {
theme = UIManager.initFirstTheme("/theme");
Map<String, String> v = theme.getL10N("strings", L10NManager.getInstance().getLanguage());
if(v == null) {
v = theme.getL10N("strings", "en");
}
UIManager.getInstance().setBundle(v);
// Enable Toolbar on all Forms by default
Toolbar.setGlobalToolbar(true);
// Pro only feature, uncomment if you have a pro subscription
Log.bindCrashProtection(true);
}
To see/edit the Strings in the app double click the
theme.res
file in the root of the project and select the localization section.
Interlude: The GUI Builder
The wizard generates GUI builder XML files that are "hidden" under the res/guibuilder
directory and must correspond to Java source files. They carry the .gui
extension and use a relatively simple format of hierarchy/layout.
You can edit the XML files but if you remove the java files they will be regenerated. You need to remove/move the XML & Java files together if you want to work with both.
Currently, the generator doesn’t generate much as it can’t replicate the layout and Android is too different from Codename One, but it’s a starting point.
Step 4: Fix the Main Activity Form
The generated code derives from Container
instead of Form
since Android doesn’t have an equivalent of Codename One’s concept of a top level component. We can just edit the ActivityMain class to derive from Form
instead of Container
(you can also do that in the .gui
XML file but it’s not essential).
We can now right-click the MainActivity.java
file and select the GUI builder option which should open this UI:
Figure 4. Main Activity UI as it is generated
This looks a bit weird but if we look at the Android XML this starts to make sense:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:context=".MainActivity"
android:background="@color/background_white" >
<include
android:id="@+id/toolbarMain"
layout="@layout/toolbar"/>
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/listView"
android:divider="@null"
android:dividerHeight="8dp"
android:drawSelectorOnTop="true"
android:fastScrollEnabled="true"
android:scrollbarStyle="outsideOverlay"
android:paddingRight="16dp"
android:paddingLeft="16dp"
android:clipToPadding="false"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_below="@+id/toolbarMain"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<ImageButton
android:layout_width="65dp"
android:layout_height="65dp"
android:id="@+id/newNote"
android:scaleType="fitXY"
android:background="@drawable/ic_new_selector"
android:layout_marginBottom="30dp"
android:layout_marginRight="30dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:contentDescription="@string/new_note_content_description" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/noNotes"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/no_notes_text"
android:textColor="@color/theme_primary"
android:textStyle="bold"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:gravity="center"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_centerInParent="true"
android:visibility="invisible" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/shadow_elevation"
android:layout_below="@+id/toolbarMain"
android:layout_alignParentRight="true"
android:layout_alignParentLeft="true"
android:background="@drawable/drop_shadow" />
</RelativeLayout>
Above we have these elements:
Toolbar
- this is builtin to Codename One with theToolbar
classListView
- Codename One doesn’t recommend lists and instead uses box layout so we will use aContainer
ImageButton
& Shadow - this is used to provide the floating action button, this is builtin to Codename OneTextView
- used to show content when the view is empty. We will have a special case for it in theContainer
Notice that there is a toolbar.xml
file included but it doesn’t include anything important just theme stuff and nothing of value to us.
All of these elements except for the text view are useless to us:
- The
Toolbar
is built-in to Codename One - We should avoid lists and instead use
BoxLayoutContainer
- We have a built-in
FloatingActionButton
so we don’t need that.
So we will delete everything except for the text. We will also verify that the layout is BoxLayout.Y_AXIS
(it should already be with that layout).
One important thing we need to do is select the root form and make sure its UIID property is Form
otherwise the converter will try to assign the default UIID’s from Android which won’t work well.
Figure 5. After removing all the redundant stuff…
We save and open the Java source file, then edit the constructor to include the FloatingActionButton
as well as the proper title. So we change this:
public ActivityMain(com.codename1.ui.util.Resources resourceObjectInstance) {
initGuiBuilderComponents(resourceObjectInstance);
}
To this:
public ActivityMain(com.codename1.ui.util.Resources resourceObjectInstance) {
super("app_name");
initGuiBuilderComponents(resourceObjectInstance);
FloatingActionButton fab = FloatingActionButton.createFAB(FontImage.MATERIAL_ADD);
fab.bindFabToContainer(getContentPane());
getToolbar().addSearchCommand(e -> Log.p("Todo"));
getToolbar().addCommandToOverflowMenu("Backup Notes", null, e -> Log.p("Todo"));
getToolbar().addCommandToOverflowMenu("Restore Notes", null, e -> Log.p("Todo"));
getToolbar().addCommandToOverflowMenu("Rate App", null, e -> Log.p("Todo"));
}
It should be mostly self-explanatory and should construct the main UI.
Step 5: Run
To see what we have we can just edit the main class ( SwiftnotesCN1
) and replace this code:
Form hi = new Form("Hi World");
hi.addComponent(new Label("Hi World"));
hi.show();
With this:
new ActivityMain(theme).show();
The end result still needs styling which we will do in the next step.
Figure 6. Before styling the result
Next Time
In the final part of this series, we will finish the app and make it look very close to its native counterpart.
Opinions expressed by DZone contributors are their own.
Comments