Why multi-platform is hard and what you can do about it
Building multi-platform products is harder than most people expect. Even with teams that have a strong grasp of iOS, Android, web frameworks etc, the experience of multi-platform builds is often challenging and frustrating.
We look at some of the reasons why multi-platform is hard and consider some strategies to deal with it. (Spoiler — it’s not necessarily a technical problem.)
This article started as a presentation — you can find it here.
Why is this harder than we thought?
The reality is that building across multiple platforms isn’t just about copying.
It can be quite hard for teams to understand why building effectively the same app on multiple platforms is hard. If you are unlucky, the related frustration will sometimes lead to a few Stakeholder Anti-patterns creeping into your projects.
Here are some of the hard lessons learned from several multi-platform projects.
- Feature parity is hard to maintain > “iOS ahead of Android”
- Teams adopt a (platform) silo mentality > “iOS is not my problem”
- Quality is highly variable between platforms > “Why are customers happy on Android but not on iOS”
- It is hard to understand each platform implementation > “different mental models needed to understand each implementation”
- It is hard to understand platform-specific challenges > “Why is one platform under performing?”
- Communication becomes harder with as teams grow > “Nobody told us that team x was doing it this way”
How do we handle these challenges?
To answer this question, let’s consider some technical approaches and then some practical measures you can implement.
We can think of the approaches to multi-platform projects in terms of how the technical implementations relate to each other.
The same code running on multiple platforms. Examples include React Native, Flutter, Kotlin multi-platform.
The same framework applied to each platform. Examples include RIBs, VIPER. This could also apply to a shared design system (e.g. Material Design).
Applying common principles or architecture whilst embracing separate platforms. Examples are reactive or event-driven principles, clean architecture or code generation.
Sharing the same development culture, values and practices, between teams whilst accepting separate platform implementations. Examples are teams using TDD or a rapid iteration culture but not aligning on implementation.
This classification might be helpful to understand our options, but there is something missing…
It is not just about technology
We must not forget that humans play a central part in the success of multi-platform projects too.
Indeed, it turns out that your technology approach has consequences for the humans involved.
The more you embrace the differences between platforms, the more emphasis you must place on (cultural) alignment between the platform teams.
We are NOT making a blanket recommendation to use cross-platform technologies.
We ARE saying that when you favour platform-specific approaches, you must put more effort into keeping the humans aligned with each other.
Let’s get into some practical measures we can take to more elegantly grapple with multi-platform projects.
Considerable consistency with multi-platform implementations can be achieved through using shared code.
Cross-platform implementations are the most obvious way to share code. In some circumstances this can make sense. WARNING — this is not a silver bullet, please approach cross-platform options with caution.
Kotlin Multi-platform looks to be a very promising approach for sharing a lot of the ‘plumbing’ whilst leaving UIs to be platform-specific. Creating a C++ library is also an option, although this might be a difficult undertaking, as experience by the team at Dropbox.
(We have tried other solutions like j2ObjC, but these haven’t been good enough to pursue further.)
Flutter looks to be a better bet on cross-platform than previous efforts and one that we are actively investigating.
This could also be called ‘shared specification’. The idea is to rely on platform-specific generated code whilst sharing the description of what the code should do.
Before we get too excited, code generation can help in specific areas, but isn’t a general strategy for multi-platform development. Practical examples are API client code generation based on Open API specs or using gRPC.
We referred to this approach as Sibling implementations. The idea is to create parallel versions with platform-specific implementations of the same architecture.
Be aware that you will likely have to accept some compromises with this approach. Our experience of Viper, for example, is that implementations tend to be unnecessarily verbose and non-idiomatic in favour of consistency.
Thinking about problems in a consistent way helps everyone share a common mental model and understanding.
- Use common architectural styles.
Examples could be: Reactive or Event-Driven. Dependency Injection or Service Locators. Try to have a common architectural style.
- Use similar (platform-specific) libraries.
Retrofit and Alamo Fire are similar approaches to calling APIs. Dagger and Clense for dependency injection.
Common test inputs
For test-driven teams, by using common definition of tests, the platform-specific implementations share a common benchmark. Some ways to implement this are:
- Gherkin to describe tests in a non-implementation specific way.
- Cucumberish for iOS implementations of tests defined with Gherkin.
- Cucumber-Android for Android implementations of tests defined with Gherkin.
- Appium with Cucumber**
** Don’t go overboard with blackbox UI testing — that way leads to madness…
Invest in a Design System
A common design language and well-known components help consistency between multiple platforms. A good design system brings the following benefits:
- Defined building blocks for common user interactions.
- Less on-the-fly decisions to be made during implementation, which often end-up being skewed to a favourite platform.
- Less chance for inconsistent use of UI or user interactions.
- Allows the design system team to focus on smaller details and purposefully handle platform differences.
Technology strategies alone won’t make multi-platform development easier. You must deal with the humans as well.
These strategies are more subtle in their implementation, but without them you reduce your chance of success.
A team culture that is focused on the product more than the technology helps shift the emphasis away from technology silos.
An obvious strategy is to group teams around features vs platforms. The second component to this is to make the whole team responsible for the feature, irrespective of the platform.
Definition of done should be multi-platform too
In support of a feature-driven culture, start describing ‘done’ in a way that applies to all platform implementations.
In other words, a feature team didn’t get to ‘done’ if something is missing on the Android version of the feature.
By making ‘done’ a team responsibility, we try to get people out of the “not my platform, not my problem” mentality.
Demo as a team
It is healthy for teams to demo the features they build. It is also good for developers to do the demos.
Another twist on this is to have iOS devs demo the Android implementation and visa versa. Whilst some might be reluctant to do this, the benefits are:
- It create a sense of ownership for “the other platform”.
- There is a natural opportunity to compare the implementation of a feature on the other platform by those who know it intimately well.
- It helps breakdown the platform silo mentality.
- It help create a sense of team delivery.
Creating products that work on multiple platforms is more challenging than it first appears.
There are some technical approaches and strategies that can help. However the key to successful multi-platform development lies with humans.
Use technical strategies to drive consistency between the platforms. Use feature-driven thinking and culture to avoid platform-based silos.