I'm getting more and more convinced that Xamarin.Forms is the wrong choice for consumer apps - even seemingly simple ones. It's just too buggy and slow.— RenĂ© Wiersma (@Rene_Wiersma) October 24, 2018
My choice would be native development, even though it pains my heart to write all the code twice.
As (I think I have) mentioned previously, my team is responsible for the company's Mobile Applications (and supporting cloud/on-premise services they connect to).
We need to support both iOS and Android as equals - in almost all cases we'll ship new features for both iOS and Android simultaneously, so the problem of developing for both of these different platforms keeps coming up; we've been doing this for the last 4-5 years.
We're not using any cross platform tools, so how do we do it? Simple, we write the app twice - once for each platform. For iOS we use Xcode and Swift (older code being ObjC) and for Android we use Android Studio and Kotlin (older code being Java).
Note: a key overall goal here from day one has been to try keep the code structurally similar, so that when you're fixing a bug on one platform, it's easy to find the corresponding piece of code to fix on the other platform. There is a tension here though against using the different abilities of each platform.
Original plan: One developer creates a major feature for a platform, another creates that same major feature for the other platform in parallel
Note: When I say "whole feature" I'm talking about a significant chunk of work - at least a month's worth.We thought at the time that this would work, and perhaps the following might happen:
- Each developer could work (and up-skill) on their preferred platform
- Devs could work for a longer time without context-switching which would be good for productivity
- We thought this would be OK as each dev would code-review the other's code, and they would both be involved in design reviews.
It was awful. I felt it took more than 2xtime/cost and generated technical debt like crazy..
- Both devs would arrive at the same problems, and both would come up with designs to solve them. Usually these designs were different. This caused a whole lot of discussion and back-and-forth arriving at a consensus. The thing was - in almost all cases either design would have worked fine, and this discussion wasn't ultimately productive.
- The devs didn't code review each-other as strictly as perhaps they should have, and when they did, their code had diverged enough that they simply reviewed it for correctness and didn't want to spend additional time going back over it and bringing it into line with the other platform.
- And besides - if we need to align two diverged code-bases, whose code is the right code to align with?
Next plan: One dev creates a minor feature for a platform, then another dev does it for the other platform on a delayed basis; the feature is mostly complete by the time the second dev starts
We sort of fell into this due to devs coming free from other tasks at different times, but after the previous bad experience, we decided to make a deliberate go at this.Note: our "minor feature" here is about 2-3 weeks or more. We simply could not use the delayed approach at the "major feature" level as it would have added so much time to the final delivery date that we wouldn't have been able to ship both platforms together on time.
This was only a little better. I felt it took about 2x time/cost
- We thought the delay would prevent the "two people designing the same thing at the same time" problem and be more efficient. This did turn out to be true, BUT
- The second developer burned a significant amount of time having to go back and learn how the first developer had done everything
- Because the second developer didn't have the original context, when they had to make a different choice due to a platform difference, they didn't fully understand the code/design and would be more likely to make bad choices
- The second developer ended up writing structurally quite different code just due to personal preference, which is a nasty trap for maintenance in future
- This is terrible for testing. It makes sense for a tester to pick up the feature on both platforms at the same time, so they can check for consistent behaviour, etc.
Take 3: One developer creates a small sub feature for a platform, then another developer does it for the other platform after the first is pretty much done
The idea here was to try and minimise the bad effects caused by the large delay of the previous approach. We aimed for sub features being around 3 days or less worth of work, but sometimes up to a week.This was better again, but still not great. I felt it had about 1.8x time/cost
- I wasn't sure what would happen with designs, but it turns out even a small delay stops two people clashing trying to design the same thing in different ways, which was good.
- The smaller delay meant that the second developer didn't have as large a chunk of work to catch up on
- When the second developer needed the original context, it was relatively fresh in the mind of the first dev, so better discussions could be had
- Testing waited til the second dev finished, but that was usually only a couple of days
- BUT, the second dev still doesn't fully understand the problem in the same way the first dev did, so they still burn time coming up to speed constantly
- ALSO, the second dev ends up using a different style/structure which hurts maintenance down the line. This is a big deal
Take 4: One developer creates a small sub feature for a platform, then that same developer creates it again for the other platform.
The idea was to try address the "second dev missing context and coming up to speed" problem.Now we're getting somewhere. I feel this is about 1.5x cost overall
- This means all the devs must learn to work on both platforms. If you're the kind of place that wants specialist iOS-only and Android-only developers you may not like this. Personally, I think pigeonholing people like that is terrible in any environment (Don't get me started on frontend vs backend developers), but hey, my team is small - perhaps when you have 50 devs you need to silo them like that, who am I to say?
- This *is* effective; if it's the same dev doing both platforms, then they inherently understand the problem equally both times, and nobody else has to second-guess their decisions. Seems obvious, doesn't it.
- BUT, when the dev would do the feature for the second platform, they would sometimes structure their code differently out of trying to better fit the other language/platform, or simply because it was a different week and they felt differently. We still had the maintenance pain down the line. Ouch.
Fifth time lucky: One developer creates a small sub feature for a platform, then that same developer ports the code in as literal a way as possible.
Goal: Stop the code diverging as much due to the devs being creative when writing the feature for the second.Process: Write your small sub-feature for a platform, then open
File.swift
on one monitor and File.kt
on the other, and go through function-by-function and line-by-line; Copy paste and then fix the errors and tweak the code.The code inside each function doesn't have to match perfectly, but structural similarity is key here.
This takes discipline, but it is the best way we have discovered so far. It feels like it gets the cost/time down to about 1.3x
- To quote a team member: "Porting code is boring", so that's a downside
- Boring though it may be, porting code like this is really fast and often easy and safe. Things don't get missed nearly as much.
- MAJOR Upside: If your code is in the same-named files, with the same-named methods in the same order, then when you make a fix on one platform it's very very easy to make that exact change safely on the other platform
- Kotlin and Swift are much more similar than ObjC and Java, so now that we've moved to the newer languages this process is a lot easier than it used to be.
Conclusion
So here you have the recipe:- Break your tasks down into as small as possible sub-features
- Have ONE dev do the sub-feature on one platform
- Have the SAME dev do a "boring" port of that code over to the other platform. Don't be clever about it!
- Enjoy :-)
Note: We have definitely looked at various cross-platform tools, but found them all lacking:
- Web-based (either PWA or cordova-type): Seriously evaluated and for what we wanted, we couldn't get good enough performance and nice enough UI
- Xamarin: Seriously evaluated and found it to be really buggy, and also quite an additional burden to learn not only how the UI works (UITableViewController etc) but the quirks and oddities of how those things project into C#.
- Flutter: Played around - maybe promising in future, but I think it's too early to depend on yet
- React Native: Only read about it; Personally I can't get past the JavaScript. Ugh. Some people say that even for a single-platform app react native is more productive than Swift or Kotlin, and if this is true (while still maintaining the level of quality and performance we want) then it could be very compelling. It's on the list to investigate soon