Sharing Code for iOS & Android (6:49)
In this video, you'll learn how to create native applications for iOS and Android that share functionality, and provide a bespoke user interface for each platform.
We will be using Fire on my Mac and the Swift language, but the same concepts work with any of the other Elements languages, as well as in Water, our IDE for Windows.
First, let's create a new iOS application project. Choose the "TableView" template, to show a list of data. Make sure the "Use Elements Runtime Library" option is checked, and click "OK".
Next, right-click in the solution view and create a second project. This time choose the "Android List View" template, which uses the RecyclerView
class to display a list.
At this stage, we have two separate applications, one for iOS and one for Android. They don't do much now, just show an empty list. There's nothing shared about these two yet, let's change that.
Right-click the solution view again and add a third project, a "Shared Project". Drag the Shared Project onto the other two projects, so they will reference this shared one. This new project will hold the code shared between the two apps.
Now, iOS and Android are very different platforms, not just in look and feel, but also in what APIs are available.
The iOS app compiles as Cocoa code against Objective-C APIs provided by Apple, while the Android app compiles to Java byte code, and uses Java APIs provided by Google and Oracle. Traditionally, this would make it very difficult to share code, but Elements makes it easy, in two main ways.
First, we can use the same language for both apps. That means a lot of code can be written that is not platform specific but will compile for both.
Second, there's the Elements RTL, a library of common base functionality that bridges the differences between the APIs across platforms.
For example, normally you would need to use NSURLSession
on iOS to download data from the web, while on Android you would use HttpUrlConnection
. These are very different APIs, so you would basically write the same logic twice. Elements RTL encapsulates this, and more.
Let's see this in action.
For our app, we want to download a Json file with weather information for a wide range of cities across the globe, and show that in a list.
Right-click the shared project and choose "Add New File", and select a "Class". Let's call it DataManager
, as this class will encapsulate all access to the data from the web.
We'll keep this class entirely platform-agnostic, so it can be used on iOS, Android and, in theory, other platforms in the future.
Some boilerplate to start with; this class will be a singleton, meaning only one instance will exist at any given time. So let's add a property to do that.
static var instance: DataManager {
if _instance == nil {
_instance = DataManager()
}
return _instance!
}
static private var _instance: DataManager?
Let's add a method that will download the weather data, asynchronously, and call a callback when done.
func downloadData(callback: ()->()) {
}
For this, we can use the Http
and JsonDocument
classes from Elements RTL.
func downloadData(callback: ()->()) {
Http.ExecuteRequestAsJson(HttpRequest(Url.UrlWithString("https://foo.bar"))) { response in
if let content = response.Content {
self.weatherData = (content.Root["Weather"] as? JsonArray)?.ToList()
} else if let error = response.Exception {
// 🤷🏼‍♀️
}
}
}
let url = "https://data.curacaoweatherapp.com/data/curacao/weather/international.json"
And finally, let’s expose the data for the rest of our app to use:
private var weatherData: JsonNode?
var locations: [String]? {
if let weatherData = DataManager.instance.weatherData {
return weatherData["Locations"]?.Keys.toSwiftArray()
}
return nil
}
func dataForLocation(code: String) -> JsonNode? {
weatherData?["Locations"]?[code]
}
That was pretty easy, right? Keep in mind that in a real-life application, this class would probably be a lot more complex and contain much more business logic and plumbing. Writing once and sharing across platforms can save both time and errors.
Now let's look at using this class from each app. This requires writing iOS and Android specific code.
First up is iOS. Here's our Table View Controller. In the viewDidLoad
method, we simply add this quick call to downloadData()
, and trigger a reload of the table view in the callback. Easy peasy.
DataManager.instance.downloadData {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Then we implement the usual couple methods in the UITableViewController
to return the actual data...
We get the number of rows available from our DataManager...
if let locations = DataManager.instance.locations {
return locations.count
}
And we create a UITableViewCell, set its label...
if let locations = DataManager.instance.locations {
if let location = DataManager.instance.dataForLocation(code: locations[indexPath.row]) {
cell.textLabel?.text = "\(location["LocationName"]): \(location["Temperature"])ÂşC"
}
}
And done. We could now run this app. But let's complete the same steps for Android first.
Here's our "Main Activity" for the Android app, which is already set up with a RecyclerViewer
control. This class is similar to the TableView on iOS.
In the onCreate
method of our activity we initiate the download of data from our server by calling downloadData()
, and in the callback we put the data into the list view.
DataManager.instance.downloadData {
self.runOnUiThread() {
self.adapter = MyAdapter(dataset: DataManager.instance.locations)
self.recyclerView.setAdapter(self.adapter)
}
}
Finally, we need to make the cells show the proper data, which we do in the ListAdapter
. First, let’s change the data type from DataItem
to String
, because our locations
list is a simple string array of airport codes.
Then we use code similar to iOS to set the proper display label for each cell, here in onBindViewHolder
.
if let location = DataManager.instance.dataForLocation(code: dataset[position]) {
holder.textView.Text = "\(location["LocationName"]): \(location["Temperature"])ÂşC"
}
Lastly, let's look at the list_item
layout file, and add a padding here, for a clearer view.
android:padding="10dp"
With that, we’re almost done, except for one last step, and that is declaring that our app needs permission to access the internet. Open the manifest file, and add a “uses permission” entry:
<uses-permission android:name="android.permission.INTERNET"/>
So here we are, both apps done and ready to run, each user interface created using native UI controls with all the backend data access shared.
Let's give them a spin. In the device picker, let's select the iPhone Xs Simulator as target for our iOS app, and the Pixel 3 Emulator as target for the Android app.
Also in the top right, we have the Active Project picker. Let's select our iOS app, and hit "Run: (or ⌘R). You see that our project builds, and then the iOS Simulator boots up, our app launches, downloads data and — there we go, looks like we're in for some good weather!
Back to Fire, let's switch the Active Project over to the Android app, and hit "Run" again. It builds, and now the Android emulator boots up, and here's our app in all its glory!
This has been Shared Projects in Fire.
Stay tuned for more videos.