Sharing code between different apps running on a range of platforms and device types is a challenge, but our ability to do just that is helping us deliver at pace.
Our App Development team are building a range of apps of various flavours. Some will have a global reach, others will have a smaller audience. Each have unique goals and they will be running on different combinations of devices and platforms. It all sounds like a tough challenge for a small team.
I've worked in much bigger companies and larger teams, but never in one as productive. A large part of that is down to being an experienced team who have continued to improve our working practices over many years together. Then there's the lean culture that we've created at Audiogum which allows everyone to focus on what is important. The final part is our architecture, which allows us to reuse a large proportion of our code across these varied projects.
A Cross-Platform Technology
The first piece of the puzzle is to use a technology that allows us to write all of our app code in a single language. Xamarin does just that and unlike many of the alternative cross-platform solutions out there, it results in a truly native app on each target platform.
Building on Mono (the original cross-platform version of .Net), Xamarin wraps the platform APIs, allowing us to write code that can target iOS, Android and Windows in C#.
We build our user interfaces using the native APIs on each platform, taking advantage of our past experience and our platform helper libraries which provide reusable UI controls, layouts, animations and utility classes to help us easily re-apply our established practices on each new project.
A Single Core for Multiple Platforms
Next, let's look at how we code-share within a single application across multiple platforms.
At a high-level, our apps can be thought of as having a Hexagonal Architecture. They share a common core which is designed to allow components to be plugged-in to create different configurations.
Here are three examples of a single codebase running in different configurations. The core is identical, but what changes is either the UI (or test runner) driving the app and any components that need to interact with specific platform APIs. For example, when we build a voice-enabled app, we require different audio detection code for each platform. The core provides a common interface for voice interaction and the plugged-in implementation varies per target platform.
These platform specifics are very thin, the minimum that is required to interact with the native APIs. Everything else belongs in the core.
Defining the Application
A shared core allows us to reuse logic in a single app on multiple platforms. To understand how we share code between different apps, we need to look at the core in more detail and how it interacts with the UI.
It is made up of three different components, Application, Domain and Infrastructure.
Application essentially defines the app. We use the MVVM pattern, so it contains ViewModels which a user interface can bind to. Like a lot of the Xamarin community, we use the MVVMCross framework to support this.
The Application component is unique to each different application. It dictates the pages that exist in the application, the navigation flows through the app and serves data to the UI. Each ViewModel also provides commands that delegate to various domain models in response to UI actions in order to implement our use cases.
Mix and Match Domain Models
Each app we build may require a different combination of features. Many will require our standard sign in and register functionality, some will for example, use our personalisation or voice technology or need to interact with a range of smart devices. For some there will be bespoke features. By splitting each Domain Model into distinct, reusable libraries using NuGet, we can easily pull together a combination of dependencies that fulfill a particular set of app requirements.
Eliminating dependencies between models helps keep our code clean and easy to follow. Just as the combination of features is unique to each different application, so is any interaction between them. It therefore falls on the Application to orchestrate this either by using Application Services to co-ordinate complex operations or by responding to Domain Events.
You'll notice there is no database layer. Each domain library is responsible for the persistence of its own data, building on helpers in Infrastructure which use Akavache to do so. Everything depends on our Infrastructure library, which contains some core types and utility classes for interacting with our APIs, parsing Json using Json.Net, managing analytics and logging among other things.
A Constant Stream of Libraries and Apps
We are big fans of Continuous Delivery to ensure we can respond to early and regular feedback. A change to our library code instantly triggers an automated test run and publishes an internal NuGet update to make the new version instantly available. The same goes for the apps themselves, as we push changes to them, new versions are tested and made available internally.
We use Jenkins to manage our build jobs and Bitrise CLI to define the individual pipelines. Fastlane helps us automate iOS delivery to TestFlight for internal and Beta distribution. The result is a constant production line of new library and app versions being created throughout the day allowing anyone in the company to download the latest version to see our progress and offer feedback.
Solid Foundations For Each New Application
With an established set of tooling, architecture and reusable code in place, starting a new application build involves pulling all the existing pieces together and starting work on the custom aspects. It means that at the start of a new project, we can get straight into tackling the unique challenges of each new app.
While it might sound like a tough challenge, having an established practice for code reuse and a set of pre-tested building blocks of key functionality on-hand gives us a vitally important kick-start for every new application build. The result is that we focus on the most important things early, have a faster time to market and an overall reduced risk for our clients.