Because of that, there is no need for a bridge between shared code and native platforms which gives us the best possible performance. Unlike other popular solutions, UI has to be implemented natively which gives us the opportunity to create pixel-perfect layouts and doesn’t affect performance either. For this reason, multiplatform projects created in Kotlin requires a slightly different approach comparing to other cross-platform solutions in case of software development process or even team structure. This review of Kotlin Multiplatform Guidelines is going to focus on architectural aspects of multiplatform projects developed in Kotlin.
Choosing the right architecture is the most important step in the planning phase of software development. In Kotlin Multiplatform project it is even more important. The biggest advantage of multiplatform solutions over the native ones is that we can share the same code between different platforms. We have to keep in mind that in Kotlin Multiplatform, we are able to share only the business logic layer and that the UI code will be implemented natively. Because of that, architecture should be chosen very wisely and it has to be the best possible option for all the platforms that will be sharing the common code.
After a deep research, in our case, we’ve decided to use MVP, because:
We were able to easily separate the common code from the platform-specific one
We were able to easily place the whole presentation and data layer in common modules
We were able to easily connect the presentation layer to the native views
Using MVP in Kotlin Multiplatform projects has many advantages:
We can easily adapt it to different platforms like Android, iOS or even frontend
We can share the whole presenter and model layers across every platform
We can easily test the common code
We can easily define the common View interfaces and implement them painlessly on the native side
We can easily integrate the common code with the native one - we just need to add one presenter per view and implement the proper interfaces
Of course, there are some disadvantages:
Presenter needs to keep a reference to the view, so we have to remember to attach it and detach depending on the view lifecycle to avoid memory leaks!
Requires some boilerplate code
MVP is not a first option architecture on some platforms
Kotlin Multiplatform gives us a possibility to share the code between different platforms. Using Model View Presenter architecture allows us to easily share the whole Presenter and Model layers between different clients, but what about the backend? Yes, we can easily share the code even with the backend! How to share different code with different platforms without any redundant code? We can use project modularization.
Let’s assume that we are creating a project that consists of Android, iOS and the backend which will be written in Spring. We already know that we have to write the whole clients UI natively, so we have to create two separate modules which will be responsible for handling clients views. We also know that we can easily share the whole Presenter and Model layers between clients, so we have to add one more module for that. During the planning, we discovered that we can also share some common code between backend and clients common module (e.g. data models) - we need an extra module for that. :tada: we have our final project structure:
Android module - Android app UI
iOS module - iOS app UI
Backend module - backend specific code
Clients-shared module - the code that will be shared across clients (Presenter + Model layers)
Common module - the code that will be shared between backend and clients-shared module (e.g. data models)
Project structure presented above allows us to clearly separate the code between different platforms. It’s crucial to think with which platform we can share the code and create separate module if our current project structure doesn’t fit our requirements (e.g. we don’t need to share data models with the clients).
There is no possibility to share the whole business logic between different platforms. There are some features that have to implemented natively because they behave differently or they use different components on each platform (e.g. taking photos or key-value storage). We can deal with it in two ways:
Create an interface in common module, implement it in platform-specific modules and pass it through presenter constructor to the Presentation layer
Define a common interface for those features using expect/actual mechanism
The first option is quite easy to implement and use. However, this approach is not ideal in cases when you have a library on one of the platforms that implement the functionality you need, and you'd like to use the API of this library directly without extra wrappers. Also, it requires common declarations to be expressed as interfaces, which doesn't cover all possible cases.
With expect/actual mechanism, we can implement platform-specific features in a common module. To do that we need to place the expected class definition in common package and after that, the compiler ensures that every expected declaration has actual declarations in all platform-specific packages, and reports an error if any actual declarations are missing.
Platform-specific package has access to classes from native platform. Because of that, we can easily implement almost every feature that we need. The list of classes which are available in platform-specific packages can be found in Kotlin Native repository.
Using external dependencies speeds up the development process. Our app needs a Dependency Injection - we can use one of many existing libraries. We need an HTTP client - we can do the same
There are thousands of platform-specific libraries which we can use to handle different kinds of features, but it’s not so easy to find proper library when it comes to Kotlin Multiplatform. Why? Because technology is quite fresh and there aren’t many existing libraries. Moreover, the external dependency that you need, has to support every platform from your project. When adding an external dependency to the project be sure that:
It supports every platform that you need. The library should contain a common artifact and one artifact per each platform that you need
Library supports all the platform versions/devices that you need
External dependency can be easily integrated