97

Architecture for Multiplatform native development in Kotlin

 6 years ago
source link: https://blog.kotlin-academy.com/architecture-for-multiplatform-development-in-kotlin-cc770f4abdfd?gi=8b518a3a1a51
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Effective architecture for Multiplatform native development in Kotlin

My mentor was often saying “If you are using Ctrl-C Ctrl-V in a single project, you are doing something wrong”. This sentence got deeply into me and started my story in search of better and better code reusability. Today I am really proud. For last weeks I have been working on a big idea that introduces a new level of reusability in native development. Right now I am ready to present results, idea and the architecture behind it all. Enjoy :)

0*qZfgAQ8us1w9z3Z9.

1*HxsEmnZGeKgLuGPFgNaLzA.png

Let’s start with a demo of the example application.

This is Kt. Academy native Android application: (Google Play)

1*FLfFp81mxH1UsjIrtt_4_w.gif

This is the Kt. Academy website written in React: (link)

1*UzwbChSUMAi8yFpP8bqFWA.gif

This is the desktop application written by Edvin Syse in TornadoFX:

1*cqvbdgANqLym_uWOyf6tww.gif

This is the Android Watch application:

1*DOG76eoQ01xhll19_W9jRw.gif

Soon there will be also Firefox Plugin, Chrome Plugin and iOS clients (if you don’t believe it, then check out this article or this repository).

Even though these applications were written in different frameworks and for different platforms, they all have the same behavior. Separate implementations of this behavior would be a huge waste of time for creation, maintenance, and unit testing. Fortunately, it is implemented once in the single module shared among them.

Kotlin

Full disclosure: the whole project is written in Kotlin. It is open-source language made by JetBrains. Kotlin is a single language with the same grammar and stdlib that can be currently used in 4 different variants:

  • Kotlin/JVM is compiled to JVM bytecode (or DVM bytecode). It fully interoperates with Java: we can actually change a single file in Java project from Java to Kotlin and everything will work fine. This is why Kotlin is becoming quickly really popular in Android development. It is even treated by Google as a first category language for Android.
  • Kotlin/JS is compiled to JavaScript. It also highly interoperates with JavaScript. It is becoming popular to implement React projects in Kotlin.
  • Kotlin/Native is compiled to native bytecode. It can be used to implement iOS applications. Kotlin/Native is still in its early beta, but JetBrains have already presented different projects written in it (check out examples, iOS application or iOS and Android application in Kotlin/Native).
  • Common Kotlin module is a special kind of module. It is not depending on any platform but it can use platform specific types and functions thanks to special mechanism (these types need to be defined for every platform in platform modules for that common module). Such types are called expected declarations, and their platform dependent declarations are called actual declarations. Here you can find more information about Common and platform modules.

Using these components we can make a truly multiplatform project. We can implement all clients in Kotlin: Android mobile, Android Watch, and desktop in Kotlin/JVM; web, Firefox plugin and Chrome plugin in Kotlin/JS; iOS and Apple Watch in Kotlin/Native. It is nice to have everything in a single project and language. Moreover, using Common and plain Kotlin modules we can also introduce the architecture that will improve this project and make it much easier to implement.

Let’s say that we need to create multiplatform project with backend and clients in web, Android and desktop (we will add iOS when its multiplatform support will be more mature).

To keep code organized, reusable and testable, every part of this project needs to have its own architecture. For Android, the most popular architecture is MVP. In a simplified way, it separates following layers:

  • Presenter — instances that are implementing business logic (how application acts).
  • View — The view is a passive interface that displays data and routes user interactions to the presenter. It includes hard-to-unittest elements that are implementing presentation logic (how application looks like).
  • Model — it is often defined as an interface defining the data to be displayed or otherwise acted upon in the user interface.

While Model in MVP definition is far from being well defined, in Android there, is a common approach to define following two type of elements instead:

  • Data Model — objects that are representing structures of data we are using in the application.
  • Repositories — objects used by the presenters and use-cases to communicate with database, network etc.

We will also extract some concrete business logic rules into smaller classes with single responsibility that we are going to call use-cases. Although in our structure they will be presented together with presenters because they serve similar role and they are always used by them (everywhere except in backend).

Business logic (presenters and use cases) is the most important part of our application and it has to be unit tested! While presenters need to interact with views and repositories, it should interact with them from behind the interfaces. Having that in mind, we can present all elements of this architecture in below diagram:

1*Lmb9twmYOyt__Pwq_hty7Q.png

Important point to notice is that all clients can have the same structure:

1*1dSEKY-k4dp-mylICozTTw.png

backend also has its layers. We use the following architecture:

1*yxu-ERpgLwREjTEcFH_LJQ.png

Notice that all architectures presented above have one common element — Data Model. It is similar or the same model for all of them and its reimplementation is a huge waste of time and energy! This is why it should be extracted into the single module:

1*ZPRxMNTfnN-1C04cFASYJg.png

Except data model, common can also include other elements that are needed for all other modules. Do we need sha1 function to calculate hash to improve API security? We can place it in common module common and use the same function in backend and all clients.

Truth is that often we want to use platform specific types in our data model. In the example project, I defined DateTime class that represents point in time. Under the hood it uses Java Calendar and Java Script Date. To allow it, we need to define expected declarations for DateTime. Then we need to define platform modules with actual declarations:

1*bXE4A5b3Dq5XDU_XsJcCEw.png

Much bigger piece of code that is shared among clients and that is waiting to be extracted is clients presentation logic. All the clients are acting the same, so they should have shared presenters and use-cases! We can extract all of them to common-client module. Together with them, we should move repositories interfaces and views interfaces:

1*i_6vkKat2bBtjB44OM7jjA.png

What about the repositories? They are currently defined in client modules and we have to pass them to presenters via constructors. Instead, we could move them to platform modules and then we will be able to:

  • implement dependency injection in common-client module
  • reuse repositories for all platforms

This platform modules will always include other elements like client types that need to be platform dependent. This is how our architecture would look like after this change:

1*lc9lenXiTHL9mYTKux9GHw.png

This is how the structure would look like for this project.

Android common module

Let’s say that we need to add the Android Watch client to our application. It creates a situation when it is reasonable to extract Android common module that will include shared elements like:

  • Resources (images, texts, colors)
  • Android helper functions like view binding helpers, recycler view classes etc.

This module is a reasonable addition to our multiplatform architecture:

1*udKL2WXIuO4lO48JpIOqXA.png

Massive multiplatform project

The above structure is presenting the current state of the example with 4 different clients implemented. Although the ambition of the example project is to become a massive multiplatform project and this mission is far from being fulfilled. We want to include as many clients as possible! Current plans include iOS application, Firefox plugin, Chrome plugin, Android TV, Android Car and Apple Watch. If you have some more ideas or if you want to try yourself by making one of this clients then feel welcomed (contribute or contact me). We enjoy every contribution. Architecture for this massive multiplatform project will look more like this:

1*jW3HSFNqNZb8i0pRaERtbQ.png

Architecture

Thanks to this architecture, we not only gained enormous code reusability but also everything has its own place. It contains following modules:

  • common — contains elements shared among all other modules. Generally, it is Data Model and helper functions. This module might also contain other elements if they are used both in clients and in backend.
  • common-js and common-jvm — contain all platform specific types for common module.
  • common-client — contains business logic for all the clients. Generally, it is placed in Presenters and Use Cases.
  • common-client-js and common-client-jvm — contain platform specific parts of common-client. In this implementation, I decided to place client repositories there, since we want to inject them into presenters and they are specific to platform (the same Java networking libraries can be used both on Android and desktop).
  • web, desktop and other clients modules — contain views for every client. This views implement view interfaces from common-client and pass events to presenters.
  • backend — contains backend implementation.
  • android and other common modules for clients built for single platform — contain shared elements like resources or helper functions.

With this architecture, we can achieve pinnacle of code reuse. Thanks to that we don’t have to implement business logic for every client separately. Similarly, changes in business logic can be applied in the single place. When we check that some business logic works in one client, we can be sure that it works in all other clients too. In this way, not only manual testing is simplified, but also unit testing now can be implemented only once, for the common-client module.

Enough theory, let’s see some code and discus in practice key elements of this different modules. It is not going to be deep analysis because this article is already long enough. We will present deeper analysis of different aspects in next articles. They will be presented one after another in next Mondays on Kt. Academy. But still, this article should give enough explanation to start your own project or contribution into the example project (feel welcomed ;)).

Common module

common module includes following Kotlin files:

1*KSuiTzYUb7NaoEbHrJIKlQ.png

common module source set

Data Model in subpackage data includes classes that:

  • Represent data used in the project
  • Represent API DTO (model of objects passed via API)

For example, this is a News class:

1*FRL94sV9fnKrh4Sb3jF_5A.png

News class in common module

Have you noticed annotation? It is kotlinx.serialization annotation. We need it to deserialize an object in Kotlin/JS. List of News is passed via API packed in NewsData class:

1*CBIdERS_X3cO7GpzVIQprw.png

NewsData class in common module

The reason behind it can be found here.

Another thing to notice is that News includes parameter with DateTime type. It is a class that represents point in time. It is defined in common module using expected declaration:

1*QZZB5OwcU0ixNGvmKvoNTw.png

DateTime expected declaration in common module

Its declaration is minimalistic because we currently don’t need more. We use it just to order the news on the clients. This is why it implements Comparable<DateTime> interface. Method toDateFormatString and extension function parseDate are needed to be used in the serializes. DATE_FORMAT specifies format for API. Its actual declarations are defined common-js and common-jvm:

1*wILpaXY9zl88kCfYuAt9dw.png

DateTimeJs in common-js

1*dEQRiiRuMzP6Dw7PNWo7Bw.png

DateTimeJVM in common-jvm

DateTime is custom type so we need to specify serializes for it to be able to pass News via API. All Kotlin/JVM projects are using Gson to serialize and deserialize objects, so we can define Gson instance in common-jvm. We need to add converter for DateTime class there:

1*gRDCpaoX6hRRIxqq1lanhw.png

Gson instance defines in common-jvm

Note that both backend and common-client-jvm depend on common-jvm, so we are able to use gson in both of them.

Similarly, in common-js we define kotlinx.serialization JSON instance used to serialize and deserialize JSONs and we define it with DateTime serializer:

1*ByIgb7iJb2LyP924SNELiA.png

kotlinx.serialization JSON instance defined in common-js

Now we can pass News via API and it can be correctly deserialized in the clients.

Another thing that we can find in common module are properties with names of parts of endpoints:

1*PXVAktAG-Sktk2MqYOKuWw.png

When they are defined in the single place, then in case of a change is needed, we wouldn’t have to search for all the places where we reference them. We can just change the value of the single property.

Common-client module

common-client module includes presenters and use cases with clients business logic. Let’s briefly describe one of the presenters to understand how it works. We will describe NewsPresenter which controls views displaying news. Its business logic rules are following:

  • After view is created, it loads and displays list of news. During that news loading there is loader being displayed.
  • When user request refreshes, news are loaded. During that refresh there is refresh indicator being displayed.
  • News are refreshed quietly every 60 seconds.
  • News are displayed in the descending occurrence order.
  • When any news loading returns error, it is displayed.

This is how we are representing view that is controlled by this presenter:

1*qgA2Bp_B11sL1feElrObpw.png

NewsView in common-client

BaseView also specifies methods to show and log error:

1*iH1LWc0AVrj8wjosHtY7Xg.png

BaseView in common-client

Here is the presenter implementation:

1*oahjtbEoUkusggvERKxIIQ.png

NewsPresenter in common-client

Presenter lifecycle

NewsPresenter extends BasePresenter class that takes care of cancelling all started jobs during presenter destroy (to prevent data leaks).

1*V2FfLvjYa93-xyE85L3Aug.png

BasePresenter in common-client

It also implements Presenter interface which specifies basic presenter lifecycle methods:

1*mB-f7wUHGxsjufyCq6RJVg.png

Presenter in common-client

Views need to call this methods during view creation and destroy. We will see later how it is ensured for Android.

Kotlin coroutines in presenters

Presenters are using Kotlin coroutines for concurrence. Coroutines are not yet supported in common modules, so we have to specify expected declarations with methods we need:

1*bs6zfurydv9VM9E4gr8Urg.png

This is how actual declarations for them are implemented in common-client-jvm:

1*yv_yHye5WvV6uADdsp-YKw.png

Expected declarations from common-client-jvm for actual declarations in common-client coroutines functions

Lightweight alternative to dependency injection

NewsPresenter uses NewsRepository and PeriodicCaller. Both of them are provided using lightweight alternative to dependency injection. It uses following class:

1*XfNnvBAIJgB6Se1PRWib_g.png

Provider class used as a lightweight alternative to dependency injection. Defined in common module because used both in backend and in common-client.

When we have a class we want to inject, we need to make its companion objects extend Provider and we override create method:

1*z01I6FB9UmGqaT9JZEIqmQ.png

PeriodicCaller class defined in common-client module

After that we can get instance lazily using:

1*PQdzZ6yWAElGazBd-ghj1g.png

We can also easily override this instance for unit testing purposes (check out unit tests).

NewsRepository is an interface defined in common-client:

1*FrGQvjceE4cTO9khVIy4Dw.png

NewsRepository class defined in common-client module

Repositories implementation

News repository implementation needs to use network libraries that currently cannot be defined in common module. All client repositories are specified in platform modules common-client-js and common-client-jvm and we provide them using RepositoriesProvider expected declaration:

1*MtyvyRauBmGxQYW5WdsX6g.png

ReporitoriesProvider in common-client

Actual declaration and implementation of NewsRepository from common-client-jvm:

1*qL6z7Co805dzF2oTwDu-tw.png

ReposirotyProvider from common-client-jvm

1*lQ4uXuQDGO-H2R1ruYSY7A.png

Implementation of NewsRepository defined in common-client-jvm

This is all we need to understand how NewsPresenter works. Its behavior can be also viewed from unit testing perspective (its unit tests can be found here, and soon I’m going to publish the article concentrating only on unit testing common modules). To really understand presenters, we need to see them used in the clients, so let’s see how NewsPresenter is used in Android and web clients.

1*xbef0K0JtDZ6F2vBVUDZsg.jpeg

Clients

Detail description how different clients use presenters will be presented in articles about specific clients (we will publish this articles next weeks, on Mondays). For now, let’s discuss simplified NewsActivity (the below implementation contains only elements connected to the presenter).

1*ml3pYQ_jQzlBfByo4hrFAA.png

Simplified version of Android NewsActivity. You can find full class here.

Activity implements NewsView interface and it overrides all its members. loading is bound to the ProgressView visibility, and refresh is bound to the swipe refresh of the list. It is possible using KotlinAndroidViewBindings library. showList is mapping news into the adapters and displaying them on the list. We don’t need to call presenter lifecycle methods explicitly because they are called in BaseActivity:

1*7hs0BfafwSIYzja1xny3wg.png

BaseActivity in Android

BaseActivity also implements BaseView and overrides its methods, thanks to that we don’t have to define showError and logError in every Activity. More about Android implementation and other tricks that are supporting multiplatform Kotlin project I am going to describe in separate article about Android.

Similar approach was applied in web module, even though React works radically different than Android:

1*8Zj8HLDUlwE0EeHH26Ja6A.png

NewsComponent in web

You can check it out on repository example or you can wait for the article about this part.

Next steps

As I have already mentioned, there are still lots of plans behind this example project:

  • We want to make it massively multiplatform and implement as much different native clients as possible
  • Not everything is finished in the project (check out todo comments in the project)

There are also some improvements that are currently hard to implement, but we want to have them because they might be really helpful for other multiplatform Kotlin projects:

  • Network API should be implemented once for backend and all platforms repositories. For that, we need special networking library that would support that. Although it is possible. I will present the idea in the separate article.
  • Elements like colors or translations could be extracted. It is hard because in most platforms there are different mechanisms to define them. Currently, it might be done using scripts or Gradle plugin. Alternatively if all views would be implemented in Kotlin DSL (in this project only Android is not) then there might be some mechanism introduced that returns strings for tag and specific language.

What is the primacy over other solutions like React Native or Flutter?

The big point here is that we are actually creating native application in native frameworks. If you check out this different applications then you will easily notice that their view and how they act is specific to the platform. In Android, we use CoordinatorLayout from Android Support Library. In web, design is typical to modern websites. In desktop, we see new windows.

We can use all tools and advantages of every platform. In Android, there is Crashlythics used to track errors. In web, there are share buttons for Twitter and Facebook. In the desktop, we are showing commenting in new window.

Applications are native and we do not depend on any bindings or bridges. We can do everything we could do before and our multiplatform architecture is supporting us to make it as efficient as possible.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK