Two Peas in a Pod

Credit unknown — please get in touch if this image is yours. It’s awesome!

UIViewControllers and Activities

In iOS we have the concept of a UIViewController. Generally speaking, a UIViewController is a core component in every iOS app. It manages a set of subviews and handles app lifecycle events that might affect those subviews. It is the Controller in the Model-View-Controller pattern. Broadly speaking, it coordinates between model objects and its subviews, and often intercepts touch events to act on them. UIViewControllers “make it happen” in the iOS world. Most apps will have several, if not dozens, of UIViewControllers running the show. Just ask Andy, Buffer’s iOS extraordinaire.

On my first day at Buffer, as I explored the existing Buffer for Android code, it became apparent that Android has UIViewControllers too! Yay for familiarity! Except they’re not called UIViewControllers, they’re called Activities. And they’re sort of the same, but not quite.

Activities and UIViewControllers perform much of the same functions, but because of the differences in how Android handles its lifecycle events, there are some key differences. Nonetheless, I was happy to have found a familiar friend.

In Android, Activities are also responsible for creating and manipulating their subviews (or reusable UI components called Fragments — a very important part of modern Android development) and handling touch events. However, Activities are often more “atomic” (I use the word in a non-computer-sciencey way) than UIViewControllers because they can be designed to run in isolation from one another.

An Android app is really a collection of Activities that can talk to each other, but don’t necessarily need to.

For example, when you launch Buffer for Android, it’s the MainActivity that runs. When you tap on the blue “Compose” button, we actually create a new Activity object of type ComposerActivity and ask the MainActivity to present it with a slide transition. This is almost identical to how UIViewControllers work with each other. But herein lies a key platform difference between iOS and Android. The ComposerActivity can also be called independently from anywhere else in the OS. The most common example would be sharing from a third party app. For example, if you tap “Share” then “Add to Buffer” from the Twitter app, the OS instantiates and displays a single ComposerActivity, sans MainActivity. It runs all by itself, like magic! This feature of the platform is incredibly powerful and has been a fundamental differentiator since the beginning.

Another note of importance is that Android is unrelenting in destroying and recreating Activities at any time they’re not on screen, so the state must be saved and the Activity reconstructed at any point. This is achieved through a variety of callback methods and definitely carries a bit of a learning curve. And I had to learn it quick, because every time a device is rotated all Activities are destroyed and recreated. This difference between iOS and Android takes some adjusting to, as completely and correctly recreating Activities can be tricky (e.g., scrolling a ListView to it’s pre-rotation y-offset). The OS handles much of the state-recreation, but not all of it.

There are plenty of obvious and less-obvious differences between UIViewControllers and Activities, but suffice it to say I was somewhat relieved at all of the principle similarities. Different languages, different clothes, same fundamentals. It was nice.

Delegates and Adapters

The delegate pattern is used extensively in iOS to offload specific tasks to specific classes (protocols are the Objective-C mechanism used to achieve this). In iOS, these are generally referred to as delegate protocols (or datasource protocol in some cases), and in Android they are generally referred to as adapters. Delegates and adapters follow the same pattern and effectively perform the same tasks.

For example, in iOS a UITableView instance will specify a delegate (and datasource, if we’re being specific) to tell it how many rows it has, to deliver a view for each specific row, to give the height of each row, and more.

The delegate’s job is to give the UITableView instance everything it needs to actually render the list to the screen. It is a basic principle of object-oriented programming to keep things loosely coupled and delegates are one means to that end.

When a UITableView requests from its delegate a view to be used for a row, it gives us the row number it’s requesting, which we then use to get our row-specific data, build the view, and finally return it back to the UITableView that requested it.

So after years of using this method to do that:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

You can imagine my relief when I was exploring our Android ListView adapter code and I saw this method:

public View getView (int position, View convertView, ViewGroup parent)

These methods are effectively identical: A list of some kind is asking for a view for the row identified in indexPath or position, respectively. Therefore, the following lines are code result in equivalent functionality:

[self.table setDelegate:myTableViewDelegate]; this.table.setAdapter(myTableViewAdapter);

First UIViewControllers and Activities, now delegates and adapters too? Another direct translation between platform paradigms, another win for the iOS-turned-Android guy! It was becoming apparent that iOS and Android were not as different as I had first suspected, and I was happy with this revelation.

Layouts and UI

At Buffer, my first task was to redesign the Android app to conform with our new design language. The redesign, including many functionality updates, took about two weeks.

In my experience, this is really where the main differences in the platforms become apparent. iOS and Android took different approaches to this from the start, for a lot of really good reasons.

Android uses XML layout files, similar to XIBs, except that they are human-readable (and even better, writable). I’ve personally found that creating layouts in Android is a fairly rapid process, and while the visual editor provides an excellent approximation for multiple screen sizes, I wouldn’t bother using it to actually create your layout. Anything non-trivial is best created by hand in the XML file.

Android’s layout system is almost entirely relative, which makes building layouts for multiple screen resolutions very smooth. In comparison, iOS’s autolayout system feels very heavy-handed.

However, when talking about animations, Android’s layout system generally falls short of iOS (although this is changing in a big way with Android L). As I understand it, the differences in complexity between layout frameworks are closely related to the animatable nature of each component within a layout.

Apple has always focused on powerful, complex and smooth animations, making it a focal point of their platform. This approach was in large made possible by having total control over their hardware: each iOS device was guaranteed to be shipped with a GPU. In contrast, Google made the choice that, in order to keep costs low, manufacturers of Android devices were not required to have a GPU. Now, I have no proof except the pudding, but in my opinion, the result was that rich animations became a lower priority since the experience couldn’t be consistent across devices, and until the Material Design language was developed, complex, high-granularity animations didn’t become a mature, focused part of the platform.

I love to fine-tune the animations used throughout an app, so this limitation makes it difficult to achieve those animation polishes that make some iOS apps so satisfying. There are still lots of options for animations, but none feel as complete or in-control as they do on iOS. With this in mind, I am eagerly awaiting Android L and plan on taking full advantage of all of the new animation APIs available. Seriously, it’s going to be awesome.

Practical Differences

Project Structure

The project structure in Xcode is loose: create (symbolic) folders and files wherever, and reference them all the same. There are a few naming conventions required in certain situations, such as app icons, @2x/@3x graphics and category classes, but it’s really just a handful at most.

Conversely, the structure for an Android project is very rigid. There are lots of naming conventions to adhere to for resource folders, style folders, source folders, and so on. This is because, on top of the source structure required for proper Java namespacing, a single Android app will have a number of screen densities to support and resources are separated by screen density.

For example, the Nexus 5 is considered an extra-extra-high-dpi device, so it will pull drawable resources from a specifically-named folder called res/drawable-xxhdpi, whereas a Nexus 4 is only an extra-high-dpi device and will pull its resources from a folder called res/drawable-xhdpi. You cannot nest these folders at all, and don’t even think about using uppercase letters in your resource file names. On the bright side, since Android uses Java, it uses proper namespacing and packages so you don’t have to worry about prefixing all your classes (which I always found to be a bit of a curious half-measure).

Our folder structure for resources used in Buffer for Android

Simulators and Emulators

One of the biggest surprises to me was the difference between the iOS simulator and the Android emulator. The iOS simulator is lightning fast compared to the Android emulator. However, this is for good reason and there is method behind Android’s emulator madness.

The iOS simulator is indeed very fast to boot, reset and load new apps, but it fails to provide a perfectly accurate representation of an iOS device. It uses the host computer’s CPU and memory, which have a huge effect on the performance of an app. This means apps run much faster in the simulator than on a device, particularly older devices, masking performance related issues. Device testing in the Apple world is an absolute must.

Android’s emulator, on the other hand, is effectively a virtual machine running a virtualized CPU and limited memory. It is a much closer approximation of a real world device. And when dealing with Android’s inherent fragmentation, the emulator is a natural choice as you can specify any permutation of device specs for development and testing, and be reasonably confident the system is behaving accurately. However, device testing is still an absolute must! You just probably don’t need as many.

Though even on Android I still recommend using a real device for development, which is nearly equal in speed to that of iOS device testing.

Afterthought: Genymotion have made some great advances in the Android emulator space. I highly recommend checking them out!

Language Differences

Somebody (not me) could write a whole book on the differences between Java, Objective-C, or any two other languages. An in-depth comparison is out of scope for this post but it’s still worth going over a few of the more prominent gotchas.

When developing in Java, check for null. Objective-C handles nil objects gracefully — it is perfectly legal to send a message to a nil object — but Java will throw a NullPointerException as soon as you try to call a method on a null object. There are some higher level discussions around the culture of null in Java, but in practice, you will simply be checking for null… a lot.

I also find myself missing Objective-C properties. In Java, there are no automatic getters and setters. You must write (or generate through the IDE) getA() and setA(Object A) and use them explicitly if you wish. I find this leads to a lot of boilerplate code, but then again maybe I was just spoiled in Objective-C when that was all abstracted away.

The final thing that I am still smitten with about Java is method overloading. It’s just so convenient! I highly recommend learning about and taking advantage of the native features of either language.

Memory Memory and Garbage Collection

Memory management is a huge topic, particularly in Objective-C when prior to ARC, memory management was handled explicitly by the developer. When done properly, apps were incredibly memory efficient, however it was somewhat of a difficult concept and led to a lot of bugs in practice. Since ARC, things have become a lot easier in terms of memory management, but it’s still a critically important aspect of developing for mobile.

When I switched over to Android, I made the mistake of assuming a garbage collected runtime would absolve me of any memory management responsibilities (within reason, of course). I was wrong. Even in Android, developers need to be acutely aware of the memory their app is using: large images, too many views or too many drawables may all lead to thrown OutOfMemoryErrors. Bitmaps can be especially tricky because if you place a bitmap in the wrong resource directory, Android will scale it to the correct density, which can result in very large bitmaps and equally large headaches.

I discovered this only recently, when creating a new introduction flow for Buffer for Android. I had incorrectly placed an image that was 1080 x 1920 in the res/drawable-mdpi folder not realizing that it would still scale 3x to display on xxhdpi devices, like the Nexus 5 whose native resolution is 1080 x 1920. When I displayed the image I immediately ran into an OutOfMemoryError, because it created a bitmap that was actually 3240 x 5670 and required over 9x the memory to display as the original graphic! I quickly realized that correctly sizing and organizing bitmaps by density is a very important step to ensuring you’re using as little memory as necessary.

Nesting LinearLayouts was another faux pas I made, except I realized it pretty late in the game.

My first task at Buffer was to redesign the app to conform to our new design language, which went surprisingly smoothly. Except just before we were going to release the redesign to the world, I decided to test it on an older device running 2.3, which we still officially supported.

It crashed and burned, a day before we planned to release the update. This was entirely my fault for not testing on older devices earlier, and a lesson I took to heart.

This dialog became somewhat familiar during my first few weeks at Buffer

I was running into StackOverflowErrors all over the place. It turns out that during the redesign I was simply nesting layouts willy-nilly, ignorant to the exponential effect that had on the heap. Nested LinearLayouts are particularly taxing due to the recursion required to calculate their subviews. I had to rewrite the majority of the layout code to be much more flat, only nesting where absolutely necessary and using RelativeLayouts for everything I could (which are very powerful!). A lot of research and a day or two later, the app was running smooth as butter across all our supported devices. Another lesson taken to heart.