- Kevin Galligan
The dream. Two native apps, zero tax, with maximum shared logic.
I’ve been running an Android consulting business for roughly 6 years. Much of that work has been, obviously, porting iOS apps. UX is critical in general, but more so on mobile. Efficiency is critical if you want to stay in business.
Some people occasionally see me as a “thought leader”, which means they feel the need to argue with me about why “___ is finally going to solve cross platform (so shut down your business)”. There are a number of reasons why this is crap, but the short version: until Apple and Google get their UI teams together to “solve” cross platform, you’re not really going to “solve” it. HTML5 was supposed to kill native every year since 2010 or so. React is interesting, but it’s basically Facebook’s version of Appcelerator.
Here’s the secret. They all have the same problem. Apple and Google spend lots of time and resources making fantastic, platform-specific, UI’s. Anything that tries to bridge them is a sad compromise. If your user base is locked in, that’s not a problem. Think “enterprise HR compliance form”. If you’re building a consumer app, you’re making a huge leap of faith building with a “cross platform” framework. Anything in between, well, Godspeed as they say.
Most significant user-facing apps have been built as two entirely separate apps, with separate code. That’s critical for the UI. However, for the logic, this is a waste. One of the fundamental rules of coding is DRY: Don’t Repeat Yourself.
If there is a sad part of my story, it’s that I run a business who’s core beliefs create their own cognitive dissonance:
- Apps should be native, which means 2 sets of code
- Don’t repeat yourself
The holy grail, then, is being able to share logic with native UI’s. Some very viable ways of doing this have been around for a long time (~2011):
- Xamarin (Robovm, Intel’s thing, etc)
Efficiency, Tax and the Product cost
You can centralize your “business logic” with any of these. That’s been possible for years. The problem is efficiency.
- You and your team need to know a 3rd environment.
- The tools for that environment will always be worse. That’s just a reality of the world we’re in. Apple and Google are investing heavily in their platforms. Other tools cannot keep up.
- The community for that environment will be smaller. Much, much smaller. That means:
a) Far less community support
b) Far less mature libraries
You can share logic, but it’ll cost you. You’re going to be figuring lots of things out by yourself, with worse tools. We’ll call that the “pioneer tax”.
Quantifying that tax is difficult, but lets say its at least, in a wildly optimistic best case, 30%. In the real world its probably 50%+. Imagine trying to code a modern app with C++. Network requests, database ops, logic. Stack overflow help? Only 50% tax would be a miracle.
Once you pay that tax, you’re good, right? Well, here’s the thing you learn building “the second platform”. The first platform is much more expensive to build for. When building any product, you learn a bunch of things, try a bunch of things you throw away, etc (See agile). It is “waste”, but it is valuable waste. The whole thing is “product cost”. We’ll call the “waste” “product refinement cost”.
If you’ve ever built a new software product, you’ll know that the “waste” can easily be 100% of the non-waste. Plus/minus, for sure, but we’ll just say half of your time is learning lessons. Guess what? All of your “product cost”, the stuff that stays and the stuff that gets tossed, gets the “pioneer tax”. The code in your final product had to pay that tax, as did all the code you threw away. Everything was more expensive. It also took longer to get to market, was frustrating for your devs (who hopefully didn’t quit), etc. It all cost more.
The dream. Two native apps, zero tax, with maximum shared logic.
If you’re not really technical, you should probably stop here and grab one of your devs to vet the rest.
Doppl is not magic.
In fact, its really a productization of several existing tools.
I’d heard about it a while back, but the concept sounded pretty horrible. I think this is the reaction any normal developer would have to such a concept. However, we worked on a project this year which triggered an obsession with the current state of “cross platform”.
That project is Research Stack, which is the Android version of Apple’s Research Kit. Research Kit is a medical research framework, built by Apple, which is a wonderful thing for humanity (as long as you have an iPhone). We were duplicating the same framework on Android. Lots of similar code. Lots of similar logic. Very little of it was specific to the platform. Lots of duplicate effort. I know (roughly) what Apple spent to build their proprietary framework. I know exactly how much (less) we got to work on the port. Had that not been a platform specific effort, it would’ve been a better result for everybody.
My cognitive dissonance kind of hit a breaking point. While our team did a great job building out Research Stack v1, I became fixated on finding the best option for building “cross platform” apps.
But I digress. That’s a long story. The short version: j2objc is great tech, but very hard to set up, and it needs the libraries we all love from the Android community.
J2objc out of the box includes some of the Android stack, but its pretty explicitly limited in scope. We’ve built some libraries on top to add some of that functionality.
The most obvious missing bit is the Context, and associated structures (Sqlite, SharedPreferences, local file system). Doppl has an IOSContext, with access to some of the features of Context. What to include is kind of a judgement call, to support popular libraries and what would make sense in an iOS world. Sqlite? Yes. ContentProvider? No. We may add/remove with feedback.
If you’re in the camp that thinks you shouldn’t have Context in shared code, you only need it if the library you’re using needs it.
Next up, threading. There is, of course, the “Thread” class in j2objc. However, if you want to support a lot of the cool stuff out there, and to maximize code share, we added a partially simulated form of Looper, MessageQueue, and Handler. This allows support for RxJava/Android, various message queues, and Eventbus.
In the testing realm, many projects wind up using Robolectric, or the standard Android unit test framework, to get access to Context, as well as main/background threads. We added some simple test runners to support that. Also working on some tools to run tests visually in xcode, which helps quite a bit when figuring things out. Trust me.
Other supported libs as of today: gson, dagger, retrofit (1.9), GreenDao, Cupboard, Squeaky (my rewrite of OrmLite). We have our message queue, MagicThreads, which has functionality very similar to the Android Priority Jobqueue. The jobqueue is also “ported”, but hasn’t really been tested yet. Also available are commons lang and io, although I think we can all agree nobody should be putting those into apps, if only because of method count, so I’ve been yanking them out 😉
The list of libraries is an expression of what we use at touchlab, but anything that’s not UI and doesn’t get too into the weeds of Android-isms should probably be pretty portable. j2objc can run some java right out of the box, but in reality, you will almost always want/need some massaging for frameworks of significant complexity.
You *can* have source jars automatically transpiled by j2objc, but in all but the simplest cases, I think this is a mistake. Its better to fork and create an explicit build, which leads to the tools.
There is a j2objc-gradle project. It was our starting point. Community work on that has basically stopped, just FYI. Our gradle plugin is a pretty severe fork of j2objc-gradle, and a true v1 will probably be a ground-up rewrite. Why? j2objc-gradle uses native compile from gradle to actually build object code, in gradle, and run unit tests. It doesn’t provide an easy way to supply alternate dependencies. It requires Cocoapods, but in a funky way (IMHO) which makes everything sort of difficult to manage.
Our gradle plugin yanked everything out except, basically, the transpile task. We added a doppl-specific dependency scope, which allows you to provide doppl specific dependencies. It won’t compile objective-c, try to convert source jars, or manage podspecs. I think all of that stuff is best handled elsewhere. YMMV.
What’s the end result? You do all of your Android in Android Studio. Separate your UI and logic (you do that anyway, right?). Put that logic in a separate java module. Make sure you use compatible dependencies. You can call all that, including your tests, in xcode. Since you use Android Studio, with Android libraries, your Android app is 100% “Android”. Assuming clean architecture, you pay zero tax (close to zero, anyway). On the Xcode side, you call the same logic methods, from either Objective C or Swift. If you decide you have some logic you’d rather code on both sides, OK. Just code it in Xcode directly. You’re using the best tools for the job.
The Xcode part is all Objective-C. It is not a simulated environment. You can wire your UI’s using the best tools for the job, and your (hopefully thin) layer of UI glue code can be Objc or Swift (or whatever).
Maximum shared code, best libraries, most efficient tools, with minimal platform risk. That’s the technical part of the dream.
Also, in case you missed it, its a really good reason to release on Android first. We’re sort of Android focused, so that’s a nice side effect.
There’s a catch, of course. Memory. iOS code uses reference counting, not garbage collection. Were that not an issue, j2objc would be magic. Details are beyond the scope of this post. The short version. Yes, its an issue. You need to learn how to deal with it. Its (generally) not a big deal. (And Rx has *a lot* of memory cycles out of the box).
Performance? Its actually pretty good. There may be cases where the way you want to code in Java, and the way that gets converted to Objective-C, don’t work well. All I can say to that is in most apps, very little of the code actually pushes performance boundaries, if any. That’s true for most code, in most domains. Not just mobile. Code first, optimize later. The point is, its very easy to optimize, because you’re using the native platform tools.
Our original plan was to have this public preview ready by Droidcon NYC, but then business things happened, so we weren’t quite there. I did give a talk about it, and it explains some of the tech in better detail, but lots has changed in the past month. For example, there is no longer a dependency on Cocoapods, gradle isn’t stuck at 2.8, and RxJava unit tests are 100% (which is an interesting story on its own, for another day. See above about memory).
This isn’t quite a “release”, but you can get the stripped down version of the Droicon NYC app, and build it for Android and iOS. If you happen to use the same dependencies, you can try building other stuff with it. If you want to have access to the actual lib and tool repos, reach out. I’d like this to be more solid before its public so we don’t get a fork and PR mess.
We have a few smaller projects going on internally, but we would REALLY like to work on something significant using our shiny new tool. Please get in touch if you’re interested.
As for public “launch”, we’re looking for some feedback and trying to figure out a sensible road map. Have to keep the lights on in the meantime.
- Improve library implementations. Memory cycles, tests, versions, docs.
- Better packaging. More frameworks, less bridging headers.
- Improved memory situation. Maybe tools, maybe just training. Maybe both.
- Wrappers for platform specific libraries, where applicable (common ui components, analytics, etc)