Back in 2017, we had started working on a project which required development for four native mobile apps — two apps (for two different kinds of users), with native Android and iOS versions for each. For confidentiality purposes, this article keeps project-specific details to a minimum.
As a large and long-term project, we decided that the risks of using a cross-platform technology, such as React Native, would outweigh any potential time-saving benefits (something that deserves its own post.) Additionally, React Native was under a more controversial licence (issues surrounding patent grants, but now resolved), which made it difficult to recommend for such a project.
In order to close the effort gap — something that Codevate does well — we had to be as efficient as possible. Taking double the time/effort to clone the two apps on another platform wasn't realistic within the budget of the project. We were confident that we could do much better.
The approach outlined in this post allows us to take on larger projects, with a smaller and more nimble team. This advantage is passed onto our clients, allowing them to enter the marketplace more efficiently and compete with bigger players.
One dual native developer: the efficiency advantage
We first started off implementing the apps on iOS, where the bulk of iteration and feedback was performed. If we hadn’t, any doubling back and feedback would have to be implemented twice on both iOS and Android - which would be quite wasteful while the app is rapidly evolving.
We also realised that having separate iOS and Android teams would be inefficient, as they would be doubling up on a lot of work and problem solving.
As a company, we were in the fortunate position of having a developer that could develop Android and iOS apps to the same high standard. Having a developer with the mental model and full understanding of an app on one platform is invaluable in being able to implement it again on a second platform at a rapid pace.
To capitalise on the efficiency advantage of having the same developer work on both platforms back-to-back, we wanted to keep the app architectures as aligned as possible, to make implementing the second app (in this case, Android) much more straightforward.
Developing a reusable native app architecture
(Not here for the tech jargon? You can skip this section.)
On iOS, we had used the MVVM (model view viewmodel) pattern to great success for separation of concerns and a clean architecture.
To go further, and make screens independent and reusable, we used the coordinator pattern for navigation.
We wanted to bring these patterns across to Android as closely as possible, while still respecting the platform differences and not forcing a square peg into a round hole.
This meant that we had the following code that could be developed in the Android apps with a lower amount effort:
- Models - simple plain objects, mainly representations of API models.
- Viewmodels - by design, these are plain objects with no platform-specific code (which aids in unit testing).
- Services - most of our services were to abstract out network calls and perform some processing on the data (e.g. storing an access token) to prevent the viewmodels from knowing too much - pretty simple logic and design.
- Coordinator/navigation flows - all the work of designing these was already done, easy to copy over. The actual native navigation is abstracted away behind a router, so these are simple objects.
- API call definitions - request/response objects are the same as models, and the routes themselves are easily replicated.
- Dependency injection - rather, the dependency tree was all fully designed.
This left the following not-so reusable parts that needed more effort putting into:
- View layer - building the actual UI is extremely platform dependent. However, these were as thin as possible due to the viewmodels doing a lot of the heavy lifting, and the visual design could largely be copied with minor modifications to make them fit the platform.
- Network layer - deserialisation and network call implementations are specific to the libraries used on each platform.
- Data storage - this is naturally going to be platform dependent, although it can be minimised by using SQLite directly. As the app was online-only, we had very little storage to worry about - mostly only authentication.
- Underlying architecture implementation - the actual implementations of MVVM, Coordinator pattern, dependency injections are all platform-specific.
- Third party libraries - even those that have Android & iOS versions (Facebook, Google, Stripe SDKs, etc.) tended to differ in implementation quite a bit - this is likely due to having separate teams working on them!
There were a lot of design problems solved that could be brought over quickly and efficiently, making it mostly a porting job rather than having to design all these elements again.
If a separate team were to have developed the Android app, then the effort of design and implementation would be doubled - especially if the apps were developed concurrently.
Benefits of back-to-back vs concurrent native app development
If the project timeline allows, back-to-back development is almost always going to be more efficient with time/effort. Taking the lessons learned and problems solved from the first implementation of the app can drastically speed up development of the second.
In our case, we were able to put a feature-freeze in place once the iOS app was signed off. This meant that we weren't trying to hit a moving target, effectively making it a port from iOS to Android.
Another advantage of developing the apps back-to-back, using iOS as a reference point, was that more bugs were noticed when porting the code over - overall making both apps more robust.
Of course, if a time-to-market deadline needs to be met, then concurrent development may be necessary. This will cost more overall though, both for the initial development and in the future - as more man-hours will need to be put in to develop both apps (due to design differences).
If your apps are developed concurrently, there will likely be a large amount of drift in the designs, making future updates and maintenance harder. This could mean that two developers/teams need to be kept on retainer, rather than one.
It's also important to be familiar with The Mythical Man-Month ("9 women can't make a baby in 1 month").
Every developer added to a project will have increasingly diminishing returns, meaning the costs can start racking up fast for less and less benefit. This is one reason why Codevate is a big advocate for smaller team sizes - with efficiency being a core value. This focus on efficiency is a reason many clients choose Codevate for their app development projects.
Why we didn't "code share" between iOS and Android
When starting the project, we felt that Swift (for iOS) and Kotlin (for Android) were mature enough languages to use for this project. It helped that Swift and Kotlin have a lot of similarities - much more so than Objective-c vs Java.
In an ideal world, we would have had code sharing instead of porting common components over to another programming language (and having to maintain them separately). As noted at the start of this post, we felt the risks of using a cross-platform technology weren’t appropriate for the project.
There were some experimental ways of using Kotlin on iOS and Swift on iOS, but they were not really production ready. There has been more progress on this since, but still not something I'd feel comfortable putting in a large long-term project.
At the time, we couldn't find anyone that had attempted to implement the Coordinator pattern on Android, so it was a problem that we had to solve ourselves. The pattern had to be implemented quite differently on Android, due to the differing lifecycles and memory management - which was a bit of a challenge.
Business-oriented technical decisions for the client's benefit
The main reason we decided to run the project in this way was to be most efficient with total man-hours and money spent. This approach allows us to tackle bigger projects with a smaller, nimbler team.
If we’re more efficient, our clients can "punch above their weight" with a limited budget. Being able to compete in a marketplace with bigger players (with much deeper pockets) can be invaluable in making a project feasible.
Being efficient also means that our clients don’t have to resort to other perceived cost-saving measures such as outsourcing overseas, using more junior developers, going for “cheaper” quotes, reducing quality, or cutting corners.
We’ve been approached in the past by prospects who had tried to get their project developed overseas to save on costs. Their projects were dragging on with no end in sight, with development costs spiraling out of control. These prospects were surprised that we quoted within the same ballpark of what they had already spent on a failing project.
To be balanced, it’s worth noting that our approach on this project has a few disadvantages that may make it inappropriate for some projects.
The main goal of this approach is to be most effective with the number of man-hours spent on the project. If there is a strict requirement to get the project into the market as soon as possible, it may be necessary to reduce the efficiency and increase the number of developers on the project (and therefore increasing costs). We mitigated this by having other developers work on separate parts of the system concurrently, such as the back-end server.
While our approach to using the same developers on both platforms may not scale up to apps the size Facebook or Uber (who have hundreds of engineers per platform), some of these companies are also using a unified architecture approach to gain some of the benefits discussed here - showing that this can be scalable.
The “bus factor” can be important to keep track of for risk mitigation. While most efficient, having a single developer with all of the knowledge can be a risk if they leave the company (or worse!)
In order to reduce the bus factor risk whilst retaining our cost-efficient approach, we have the following processes in place:
- Thorough code reviews (quality assurance), so other developers gain knowledge of code they haven't directly worked on. They can also bring cross-functional knowledge to other areas of the project.
- Following best practices and standards, which makes it easier for others to get up to speed.
- Producing enough documentation, so project knowledge is externalised and shared with the wider team.
- Staff retention schemes, to retain project knowledge and familiarity in the longer term.
It’s worth noting that using two separate single-platform iOS and Android developers would still have a similar bus factor risk - as one developer wouldn’t be able to take over the other's work.
Our approach to this project can still work with multiple dual-platform developers, at the cost of some efficiency (see "The Mythical Man Month" above), to reduce risk.
Other ways we're cost-effective with app development
Selecting the right tool for the job is crucial, especially from the start of a project - it’s important to build a solid foundation. This choice can have far-reaching effects into the future of the project, as the benefit is compounded over time. For example, building on the wrong framework will be very expensive to correct the further into development you get.
We use a good blend of tried-and-tested tech and community-backed new open-source software to save on development time, by not having to reinvent the wheel, and instead focusing on the requirements unique to the project.
Shorter development phases/smaller deliverables and regular client touch points help us to keep business and tech elements aligned, and raise any issues as soon as possible. If a pivot in requirements is made, it will be cheaper to make the necessary changes as less work will have to be thrown away.
Long-term developer retention means lessons learned are carried forward, so we can avoid repeating mistakes, and do things faster the next time around.
So, how much more efficient were we?
Although the argument isn't to "always only ever use one developer", as time can be of the essence on some projects. But if you're like Codevate, a small and nimble agency that delivers on big software projects, then Lean methodology has incredible benefits for those clients who aren't in a time-to-market rush.
This doesn't mean don't build an MVP — we still encourage building an MVP. But if you're short on cash (but not time) then back-to-back single dev is the way to go.
For the project discussed in this post, looking back over our project management tools, deliverable documentation, and invoices, we estimate that the time spent on Android development (the second platform) was (very roughly) 50% of the iOS development time.
Yes, you read that right! Combining all the data suggests we saved the client around 50% on their second app with back-to-back development, at a small cost of a longer time-to-market.
This was quite a significant saving, not to mention the boomerang effect of quality assuring the first (iOS) app whilst reproducing the functionality in the Android app (therefore making them both more robust).
It’s worth noting that, as the back-end had started development at the same time as iOS, this may have slowed down development somewhat while the design was finalised. To be even more efficient, we could have developed the back-end first - but the project would have taken longer to get to market. We felt that this was a good compromise.
What do you think about our approach to reusable native app architecture? Feel free to start a conversation below in the comments section — or give your favourite part of the article a like so we know what to expand on in our next articles.