An iOS Perspective on Transitioning to Android

For a long-time Apple enthusiast and iOS developer who had never even used an Android device, the thought of learning Android development made me cringe. But like any developer, it’s important to constantly be honing your skills and learning new things, so when the opportunity presented itself to jump onto an Android project, I took it.

At this point, I have about 6 months of Android experience under my belt and wanted to share my experience, highlight some of the things I learned, and take an in-depth look at the tools, languages, frameworks, and paradigms of both platforms. Hopefully, this blog will answer some of the questions those in a similar position may have.

Language: Swift vs. Kotlin

It was fairly easy to jump into Kotlin from Swift. It has many syntactic similarities with Swift making it a relatively painless transition. Kotlin is an Object-Oriented Language, and it doesn’t hinder your ability to apply the same OOP concepts and patterns you would use with other OO languages. At first glance, the bells and whistles of Kotlin stood out. Over time, it was easier to see some of the deficiencies.

Generics and Abstractions

Normal generics systems provide ways to “genericize” the concrete. Swift applies the concept of generics to abstractions via protocols with associated types (like a generic interface). This is extremely powerful and allows developers to do some really cool things, such as constrained extensions and conditional conformances to protocols. Swift is considered a protocol-oriented language. This means that the basis for extending functionality is (generally) not by inheriting it, but rather by conforming to a protocol to which that functionality is attached. Kotlin, on the other hand, utilizes abstract classes and interfaces like many other OO languages. This is fine, but I’m on the fence about abstract classes in general because they blur the line between the abstract and the concrete, and often result in overuse of subclassing (which is tight coupling).

Enums

Swift enums with associated values are extremely powerful. They allow you to use types that are unrelated by inheritance or protocol conformance as a single object. They also provide a concise way of communicating all of the possible scenarios of an enum.

enum State<T> {
    case pending
    case loading
    case loaded(T)
    case error(Error)
}

func didUpdateFooState(_ resolver: State<Foo>) {
    switch resolver {
    case pending:
        // Show empty data
    case loading:
        // Show loading indicator
    case loaded(let foo):
        // Update UI for the new value of foo
    case error(Error):
        // Present error to user
    }
}

In Kotlin, the enums resemble Java enums. They may have fields, but you use must the same constructor for each case so they are limited in value as they do not represent fundamentally different types, so they cannot be used for the same use cases as Swift enums. However, Kotlin has Sealed Classes which can be used where one would use Swift enums with associated values. Looking at the same example, we can use a when expression to switch over the possible cases of the sealed class Noun.

class Person(val name: String)
class Place(val name: String)
class Thing(val description: String)

// Declare a sealed class
sealed class Noun {
    class PERSON(val person: Person): Noun()
    class PLACE(val place: Place): Noun()
    class THING(val thing: Thing): Noun()
}

// Switch over the possible cases
fun description(noun: Noun) = when (noun) {
    is Noun.PERSON -> noun.person.name
    is Noun.PLACE -> noun.place.name
    is Noun.THING -> noun.thing.description
}

val bob = Person("bob")
val noun = Noun.PERSON(bob)
val desc = description(noun)
println(desc) // bob

Although they generally accomplish the same thing, I believe Swift provides a much more concise syntax.

Annotations

From the Kotlin Annotation Docs:

Annotations are means of attaching metadata to code

When paired with reflection, annotations make magic possible. Kotlin has an extensible annotation system. Swift does not. You can define your own annotations on classes, types, fields, and functions and access them via reflection.

// Declare a function annotation
@Target(AnnotationTarget.FUNCTION)
annotation class RequireRoles(val roles: Array<String>)

// Decorate a function with the RequireRoles annotation 
interface Api {
@RequireRoles(roles = ["admin"])
    fun doSomethingOnlyAnAdminShouldBeAbleToDo()
}

// Find the roles on the function via reflection 
val method = Api::doSomethingOnlyAnAdminShouldBeAbleToDo
val annotation = method.findAnnotation<RequireRoles>() 
val roles = annotation?.roles

// Do something with roles

This reduces boilerplate for routine tasks such as API definitions and JSON serialization which we’ll cover later. This is one of the single biggest drawback of Swift, in my opinion. Big plus, Kotlin.

Statements and Expression

In Kotlin, statements are actually expressions meaning they return a value. You can omit the return keyword and the last value of your statement is implicitly returned from the expression and it may be assigned. The when keyword offers nice syntax as an alternative to the switch statement:

fun passOrFail(grade: Double) = when {
    grade >= 70.0 -> "Pass"
    else -> "Fail"
}

As you can see in the code above, the function omits the return type and implicitly returns a String because the when expression returns it. This is wonderful syntactic sugar for Kotlin, and it can usually save you a few lines here and there.

Null Safety

Both Swift and Kotlin feature null safety via optional values.
Swift:

var foo: Foo?

// Force unwrap
foo!.value

// Optional chaining
foo?.value

// Nil coalescing operator
let value = foo?.value ?? 0

// Safe unwrapping
if let value = foo?.value {
    // value is now a non optional Int within this scope
}

// Guarding
guard let value = foo?.value else {
    return
}

// value is now a non optional Int

Kotlin:

var foo: Foo? = null

// Force unwrap
foo!!.value

// Safe calls
foo?.value

// Elvis Operator
val value = foo?.value ?: 0

// Safe unwrapping
if (foo != null) {
    // access foo.value as a nonoptional
}

foo?.let {
    // access it.value as a nonoptional
}

// Exiting on null
val value = foo?.value ?: return

// value is now a non optional Int

I like features from both Swift and Kotlin, and both are fantastic languages that I enjoy working with. In my opinion, Swift is the more structurally sound language. It is spectacular with most of the big picture things (protocols, generics, enums) and does a better job of following convention, but Kotlin has a lot of syntactic sugar and an incredible annotation system.

Memory Management: ARC vs. Garbage Collection

Automatic Reference Counting (ARC) is Swift’s mechanism for memory management. An instance of a Swift class may hold either strong or weak references to other classes. The instance will keep track of how many objects hold a strong reference to it. When its reference count hits 0, it is no longer needed and it will be deallocated. It is up to the developer to avoid memory leaks such as retain cycles, which occur when two objects hold strong references to each other and thus neither of them deallocate.

On the other hand, Kotlin, which runs on the JVM, uses Garbage Collection (GC) for automatic memory management. GC is done in three phases: mark, sweep, compact. The garbage collector will traverse a tree of objects from some root and set a “marked” flag for every object it has visited, indicating the object is accessible and thus referenced. It will then sweep through all the objects in the heap, deallocating any that have not been “marked” and resetting all the other objects’ “marked” flag. Finally, the garbage collector will compact the memory in the heap by moving all remaining objects to adjacent memory addresses to prevent memory fragmentation.

The developer doesn’t have to worry about memory leaks as much, but now the developer must write code to optimize garbage collection because when the garbage collector is running, your application is paused to prevent new allocations. This can be especially problematic for allocating new objects inside of a loop because it may be a while before they are deallocated and it slows down the garbage collection process. I have noticed apps will often freeze as a result of this process. Although garbage collection makes it more difficult to make horrific memory leaks, it hinders the ability for the developer to write the most efficient code possible.

Frameworks and Paradigms

Life Cycle

The most difficult thing to adjust to throughout this process was the application life cycle. This section probably warrants its own post entirely and a ton of research to fully understand (which still I do not at this point), but I’ll try to give a brief overview of the differences and why this was difficult.

In iOS, your application is a single cohesive unit. Different screens (view controllers) within your application (shouldn’t but) may talk to each other directly.

In Android, the Activity is the driving force of Android applications. An activity generally represents a module within an application (like a view controller you would present modally in iOS) and your application is broken up into a collection of activities. An activity’s layout can hold a placeholder for a Fragment where you can use the activity’s fragmentManager to navigate between them (like a navigation controller).

Activities are started with an Intent object which defines an activity and parameters the system should start. You do not start the activity yourself; the system does and the system may destroy and restart your Activity at any time, such as when the system configuration changes (such as screen orientation or keyboard availability) or it may place an activity in a stopped state (when another activity resumes) where it may be destroyed at any time. This makes it particularly painful from a developer perspective because you must prepare by implementing the lifecycle hooks for saving and restoring the state of your activity.

The only communication with the activity you are starting from the current activity is an intent for starting it and returned data in the form of an intent. The intent uses a key-value extra property for passing data so it can require a lot of boilerplate to start an activity and handle the result intent. I actually like this a lot and not because it makes life easier, which it doesn’t. Think about the intent as a configuration for the activity. Let’s say we have a ProfileActivity. The only configuration this activity needs is the id of the user. The activity should use its own repository objects to either fetch the user from a cache or server. This promotes loose coupling between activities so long as you don’t find yourself passing entire object(s) through the intent.

Interacting with RESTful APIs

In Android land, the go-to HTTP client for defining API interactions is Retrofit. All you have to do is define an interface and decorate the functions with Retrofit’s annotations. Then you create a Retrofit object and create a service from that interface. That’s it. You do not have to create a request object because annotations make it possibly for this to be done automagically.

interface FooService {

    @GET("/foo/{fooId}")
    fun fetchFooById(@Path("fooId") fooId: String): Observable<Foo>
}

// Create a service from the interface
Retrofit.Builder()
    .client(httpClient)
    .baseUrl(BASE_URL)
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(FooService::class.java)

There are many different ways this is done in iOS, and all of them require more boilerplate than Android due to the lack of annotations. The approach I generally take is creating a protocol and then injecting an HTTP client into a concrete implementation:

protocol FooService {
    func fetchFooById(fooId: String) -> Observable<Foo>
}

class HttpFooService: FooService {

    private let http: HttpClient

    init(http: HttpClient) {
        self.http = http
    }

    func fetchFooById(fooId: String) -> Observable<Foo> {
        // Make some request object for the path
        // Set the body and headers
        // Basically everything that annotations handle
        return http.executeAndDecode(request)
    }
}

No magic whatsoever, but it gets the job done.

JSON Serialization

With compiler synthesization of the Codable protocol, Swift has taken a step toward streamlining the hassle of deserializing objects. It’s great if your keys exactly match the JSON. If not, it’s a hassle. I’m not at all thrilled with the CodingKey enum approach for defining custom keys.

struct Team: Codable {
    let id: String?
    let nickname: String?

    enum CodingKeys: String, CodingKey {
        case id
        case nickname = "nick_name"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try? container.decode(String.self, forKey: .id)
        self.nickname = try? container.decode(String.self, forKey: .nickname)
    }
}

In Android, where we have Kotlin and annotations, we can use the @SerializedName annotation to provide overrides that Gson (the serialization/deserialization library we use with Retrofit) uses when deserializing JSON from network requests.

data class Team(
    val id: String?,
    @SerializedName("nick_name") val nickname: String?)

It’s pretty easy to see which way of doing it is better.

UI: XIB/Auto Layout/Size Classes vs. XML Layouts

About an hour into Android development, I already had the UI ready to go for the feature I was implementing. Android uses human readable (and writable) XML files calls layouts to represent the UI of a fragment, activity, view, etc… You may specify different layouts with the same name for different device orientations and DPI configurations. This is a stark contrast from XIBs, Auto Layout, and Size Classes for iOS. The underlying representation of a XIB is just an XML file, but it is definitely not something you would want to write yourself, as it is riddled with outlets and UUIDs.

It is a pain to implement a reusable XIB-based view in iOS. You must put a placeholder view in your XIB and then load the other XIB in as a content view. It’s ugly, but plain and simple, and the alternative, which involves creating view controllers for each reusable component, often seems like overkill. It is much easier to create reusable components in Android by making layouts for views, fragments, activities, and items.
Layouts may be embedded in other layouts like so:

// Using a reusable layout
<include layout="@layout/titlebar"/>

// Displaying a placeholder layout for RecyclerView items
<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:listitem="@layout/some_reusable_item_layout"/>

The XML will render any included layouts so you know exactly what your view will look like on different screen sizes. Using data-binding, the elements of your layout are automatically synthesized for use in your fragment. This is a huge advantage over manually setting up an IBOutlet for every element of your view. There are also frameworks like Butter Knife that provide an IBOutlet-like syntax for binding to views using the @BindView annotation.

Dependency Injection

I’m not going to go too much into detail here, but I want to say that Dagger is great. It’s a DI framework by Google, and it uses the @Inject annotation to handle dependency injection. It’s far better than any solution on iOS. I’ve tried Swinject, and I still usually fallback to manual constructor injection. Use Dagger, don’t use singletons. That’s all for this section.

IDE: Android Studio vs. Xcode 10

It has been a breath of fresh air to work with Android Studio. It is far superior to Xcode in almost every conceivable way. Android Studio has a proper autocomplete tool and helpful refactor tips to help you write better code. Android Studio is extensible. Android Studio is configurable. It’s easier to configure different build/run/test configurations. Android Studio is much more stable. There is a lot of magic that goes on in the Android development environment. Most of this magic is code generation to make developers’ lives easier. Occasionally, something will go wrong, and you’ll be left scratching your head. A simple “Invalidate caches and restart” will usually fix the problem. With Xcode there isn’t any magic going on behind the scenes, and it still struggles. Do better, Apple.

Intangibles

It is difficult to quantify some of the differences between the two platforms. It is blatantly obvious (to me at least) that iOS is the more stable platform. This could be for a few reasons. Android’s philosophy on extensibility and compatibility result in a lot of headaches that just aren’t present in the iOS ecosystem. Android’s software adoption rate lags far behind that of iOS. This results in the use of compatibility libraries to implement allow applications to work on devices with older SDK versions. Also, Android can run on any device type so developers have to account for that. This can result in inconsistencies on non-standard devices.

In my experience, it seems technical debt accrues faster on Android, and the Android app has generally lagged behind the iOS app or would require more developers to keep up. It seems like the iOS frameworks are more straightforward, especially in regards to the life cycle, and make it a tad harder for newbies to make disastrous mistakes when coding.

Android is great

I’ve definitely enjoyed the challenges of learning Android. It has made me a better, more versatile programmer. It has been really cool seeing the way certain things are handled on a competing platform and now, with knowledge of both platforms, the deficiencies in both become more obvious.

I’m still far from the level of comfort I feel when I’m working on something in iOS (and I’m not sure if I’ll ever reach this point), but it’s been a great experience. I really hope this has helped anyone who is looking to make or currently making the transition to Android from iOS or vice versa.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *