Building an App with Kotlin Multiplatform: Structuring our app

Over the past few months I’ve been seeing more and more talk of Kotlin multiplatform online. With this rise in conversations on the topic, I naturally became more and more curious about the technology. I recently started planning out one of my next side-projects, Minimise – the app to help us think more about the purchases we are making and rediscover things that we already own. I haven’t actually started building this app yet, so it seems like the perfect opportunity to get stuck in with kotlin multiplatform.

Over this series of articles we’ll be documenting each step of this app build – from structuring our multiplatform project, use of multiplatform libraries and the implementation and testing for the logic of each feature. In this post we’re going to begin by laying out the project, thinking about it’s structure and taking a general look at kotlin multiplatform.

Sharing code across multiple platforms has long been sought after by both developers and businesses. When building an app, writing the code once and running that single piece of code on multiple platforms is the dream, right? Whilst there are currently solutions to “write once, run everywhere”, we can often run into limitations with these solutions. Kotlin Multiplatform focuses on the sharing of business & connectivity logic, allowing us to natively code our User Interface layers in the corresponding frameworks own language. This approach helps us to avoid these limitations + difficulties which cross-platform technologies currently face, whilst being able to share core parts of our projects across different clients.

For an MVP of the Minimise application we’re going to build a couple of features. These features will give our users enough functionality to be useful, whilst also giving us enough scope to experiment with kotlin multiplatform

  • Authentication – we’ll need to allow our users to sign-up and sign-in to the application. For this we’ll be using the Firebase Authentication REST API – we could probably tweak the Firebase Auth SDK to work with multiplatform, but using the REST API gives us a good chance to play with ktor for making HTTP requests.
  • Inventory – this will be the main screen of the application, allowing the user to view items that they own, are considering to purchase and items they did not end up purchasing. The data for these will come from Firebase Firestore, but again we’ll use the Firestore REST API for reading the data for display.
  • Manage Inventory – because the user can add inventory items, we need to also allow them to manage them. This will involve the user being able to add an inventory item, as well as being able to edit the details for it down the line. During the process of adding an inventory item the user will be asked a series of questions to help them reflect on their purchases before they are made. Editing inventory items will allow the user to change the details for an item, such as changing the name of updating the status to purchases. These features could probably go into separate modules, but for simplicity we’ll use a single module for now and cross that bridge when it comes to building it.

Providing Shared Code to clients

When you’ve written some code that supports multi-platform, you’re going to want to allow clients access to it. The code you’ve written will be packaged and exported for use by each corresponding platform, so for example:

  • For Android, the library in question will be imported as a gradle dependency. Because we’re building our library module with kotlin, no extra work is required here
  • For iOS, the library in question will be imported as an xcode framework. Because our library module is in the format of a kotlin project, we have to add a short gradle task which will export our library module as an xcode framework.

Ideally for our projects the multi-platform code would be within its own project and each library/framework would be exported and provided as a dependency. For example, a gradle dependency from maven central for android and cocoa pods for iOS. For Minimise we’re going to keep the entire project inside of a single repository as this will keep things things simple and easier to work with.

Project Modularisation

When it comes to structuring the project, I want to build Minimise in a way that reflect feature modularisation – not only from the native-UI level but also when it comes to the multi-platform code. Building in this way not only gives us benefits from having a clear separation of concerns, but also might reflect how these areas may be worked on at our organisations. For example, the squad working on the shared multi-platform code for feature A will not need to touch or know about the shared multi-platform code for feature B. With this in mind, I’ve ended up with something like the below:

  • Starting on the left hand side of the diagram we have our Android and iOS native code. These are the features each separated into their corresponding feature modules – this allows us to keep our native feature code separated from one another. Each feature will live inside of its own module and then access the required shared feature code that we have written without our multi-platform project.
  • At the bottom of the diagram we have our native base application classes. In the case of Android this would be our Application class and in iOS, our ApplicationDelegate. These will be needed to initialise our shared framework and anything else that may be needed to control a global state. For example, if end up using a Dependency Injection solution that handles the injection for the code within our multi-platform modules, then this will need to be initialised in some way.
  • In the center of the diagram we have our shared feature logic modules. You’ll notice here that we have a shared module for each of our features – whilst you can chuck everything into a single shared code module this would mean that every single feature will know about one another. Also, if you are distributing shared code as libraries to each project then as changes are made to features you can distribute them independently.
  • These Shared Feature modules also depend on a shared common module. This module will provide anything that is common between each of the feature modules, such as the dispatchers used for coroutines or base classes used throughout the shared features code. This helps us to promote reuse throughout our shared code libraries.
  • We then have the Minimise Core shared library, this is what is going to be used by our Applications to access any global logic / state for our framework. In the case of Minimise it mostly be used to initialise our framework and setup the dependency injection. In most cases you might not need something like this, it really depends on how your project is configured.
  • Next we have each of our feature API modules. We could have placed this inside of a single module (acting as an API SDK which would probably be better here) but for clarity I’ve gone with a single module for each one (partially because I plan on writing a Firebase Auth multi-platform library, so half of this will be separated anyway). This remote logic is outside of our shared feature modules so that they are kept more concise and have a clear line of responsibilities between them.
  • Finally we have a shared remote module that will allow us to share common code between each of the API modules. For example, any http client logic that is configured or anything else that is reused throughout each of the API modules.

As you can see the above gives us a clearly modularised project, along us to work on feature development and maintain existing code with a reduced overlapping of work. We’ll also be able to test each of these pieces with more ease due to the separation that is outlined here.

There’s only one issue. Currently, Kotlin Multiplatform does not allow you to use multiple Kotlin frameworks within an xcode project. There’s good news as this feature is coming in 1.3.70, but currently we are at 1.3.61 so we’re going to have to wait until that is out of Early Access Preview. Because of this, we can’t quite have our Shared Feature code separated into multiple modules, so for now we’ll move forward with something like this:

For now we’ll place all of our feature code within a Shared Common module which will be used by each of our features. Whilst this isn’t ideal, it unblocks the original idea – once 1.3.70 is out of EAP we can make some quick changes to get the desired configuration in place. Whilst we could get the 1.3.70 EAP working in our project, it adds complexity to the project and also for people wanting to follow along – hopefully in the near future we’ll be able to optimise this!

Project Structure

Alongside the modularisation of the project, I also wanted to take a moment to think about how each of these modules is going to be placed within our project. As previously mentioned, you would usually provide shared modules as libraries for dependencies to import via their platform method (cocoapod, gradle dependency etc) – to keep things simple here, and because it’s only me working on this project, I’m going to have everything within the one project/repository.

Whilst we could have each of our modules/directories placed in the root of this project, we’re going to have a few different modules which could make it a bit hard to navigate around. For this reason we’re going to split things up a bit into directories that represent where each thing belongs. At the root level of our project we’re going to have something like this:

Within this we can see four directories, each of these are:

  • buildSrc – this will declare the dependencies used throughout our shared projects (the android module will also use dependencies from here)
  • platform_android – this will contain all modules that are related to the native android project code
  • platform_iOS – this will contain all the modules that are related to the native iOS project code
  • shared – this will contain all of the modules that are shared across the different platforms. E.g shared_authentication from the previous section diagram

As you can see from above, this helps to group the different modules of our projects so that they are cognitively more navigable. To take a quick look at what each of these might look like, let’s take the android and iOS platform directories. We can see that within each of these we have the corresponding feature modules for that platform:

Finally, the shared directory contains each of the shared modules from within our project. There is also going to be a subdirectory within here for the remote layer of our application, again containing each of the modules that contain the feature specific remote implementations.

With the above organisation in mind, our single repository will be slightly simpler to navigate around. I’ll be working on this project alone so as previously mentioned, having this in a single repository makes more sense for me. If you’re doing something similar, maybe the above will help!

From the above points we’ve begin to think about how our kotlin multiplatform project is going to be structured and organised. We’ve also taken a quick look at how the project is going to look from a modularisation level – I think this is an important thing to think about, even more so when we are starting to deal with applications that are split out into feature teams.

With the above in mind I’m ready to start diving into building Minimise using Kotlin multi-platform. Stay tuned for the next blog post on this topic where we’ll dive into building our first API module using ktor.

[twitter-follow screen_name=’hitherejoe’ show_count=’yes’]

Leave a Reply

Your email address will not be published. Required fields are marked *