Skip to content

Commit

Permalink
Updates docs
Browse files Browse the repository at this point in the history
  • Loading branch information
franciscoengenheiro committed Mar 15, 2024
1 parent 95a3300 commit 7f8a36d
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ captures
.externalNativeBuild
.cxx
local.properties
xcuserdata
xcuserdata
242 changes: 223 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,262 @@

## KMP - Kotlin Multiplatform

> The [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) (KMP) technology facilitates the sharing of
> application code across several platforms,
> leveraging the multiplatform capabilities of the Kotlin language to enable the development of a unified codebase
> compiled for various platforms.
Module: [kmp](./kmp)

### Testing the Architecture
### Architecture Overview

The KMP architecture is composed of **three** main categories:

- **Common**: Code shared across all platforms (i.e., `CommonMain`, `CommonTest`);
- **Intermediary**: Code that can be shared on a particular set of platforms.
See [Intermediate Source Sets](#intermediate-source-sets);
- **Specific**: Platform-specific code (i.e., `<Platform>Main`, `<Platform>Test`).

The main goal is to maximize code reuse, meaning to aggregate as much code as possible in the **Common** category.
However, sometimes it's necessary to create specific code for a **target** platform in the
following situations:

1. A certain functionality cannot be implemented commonly because:

- It requires access to specific target APIs;
- The libraries available for common code _(i.e., Standard Kotlin Library, Kotlinx)_ do not cover the desired
functionalities,
and there's no external KMP-compatible library available to be used as a dependency (or it is discouraged to use);

2. A certain target does not directly support KMP _(e.g., Node.js)_, and thus an [adapter](#adapter) is needed for the
code to be callable from the target.

To create specific code for a target, the `expect/actual` mechanism is used, which allows defining the code to be
implemented and its target implementation, respectively.

Example:

In the [KMP template](https://github.com/Kotlin/multiplatform-library-template) provided by Kotlin,
```kotlin
// commonMain
expect fun platformName(): String

// jvmMain
actual fun platformName(): String = "JVM"

// jsMain
actual fun platformName(): String = "JS"
```

| ![KMP Architecture](./docs/imgs/kmp-architecture.png) |
|:-----------------------------------------------------:|
| KMP Architecture Overview |

### Testing the Application

In the [KMP template](https://github.com/Kotlin/multiplatform-library-template) provided by _Kotlin_,
the example with the `fibonacci` sequence was removed
and replaced by a class to practice the `expect/actual` pattern more explicitly.
and replaced by a few examples to practice the `expect/actual` mechanism more thoroughly.

This [addition](./kmp/src/commonMain/kotlin) follows the same principles:

This addition follows the same principles:
- test common functionality in [CommonTest](./kmp/src/commonTest/kotlin);
- test platform-specific functionality in each platform's test source set (`<Platform>Test`)

- test common functionality in [`commonTest`](./kmp/src/commonTest/kotlin);
- test platform-specific functionality in each platform's test source set (`<target.`Platform.jvm`>Test`)
> To run the tests for all supported targets, use the command:
>
> `./gradlew kmp:allTests`
> To run the tests for all supported targets, use the command `./gradlew kmp:allTests`.
> [!IMPORTANT]
> There's currently a Native target's dependency issue,
> which does not list `linuxX64Main` as a target for testing.
### Intermediate Source Sets

[Intermediate Source Sets](https://kotlinlang.org/docs/multiplatform-discover-project.html#intermediate-source-sets)
enable sharing code
across more than one platform,
yet not encompassing all, as that would be the role of the common source set.
This allows for a more fine-grained control over the code sharing, maximizing code reuse.

| ![Intermediate Source Set](./docs/imgs/inter-source-set.png) |
|:------------------------------------------------------------:|
| Intermediate Source Set Example |

The dependencies between source sets can be configured within the corresponding `build.gradle.kts` file. Additionally,
their hierarchy can be adjusted,
The dependencies between source sets can be configured within the corresponding `build.gradle.kts` file,
as mentioned [here](https://kotlinlang.org/docs/multiplatform-hierarchy.html#manual-configuration).

> [!IMPORTANT]
> The `iOS` source set was removed from the template,
> because there is no `macOS` machine available for testing, as required by Apple.
### Adapter

The **adapter** is a module that allows the code to be called from a target that does not directly support _KMP_ and
_Kotlin_ in general.

For demonstration purposes,
a pure [JS application](./js-app/src/main/js/server.mjs) was created
to call the [adapter](./kmp/src/jsMain/kotlin/Adapter.js.kt)
defined in the `JsMain` module of the `kmp` module,
essentially acting as a consumer.

#### Build and Run

```bash
# from root
./gradlew kmp:jsNodeDevelopment
```

```bash
# from root
node js-app/src/main/js/server.mjs
# take a look at the express paths and PORT configured in `server.mjs`
# open an HTTP client and access http://localhost:PORT
```

For more information about _Kotlin_ and _JavaScript_ interop, see [Kotlin-Js Interop](#kotlin-js-interop) section.

### Relevant Design Choices

As mentioned in the issue [KT-61573](https://youtrack.jetbrains.com/issue/KT-61573), the `expect/actual` pattern
As mentioned in the [issue](https://youtrack.jetbrains.com/issue/KT-61573), the `expect/actual` pattern
should only be used for `functions` and `interfaces`.
An alternative for this pattern is to use `expect fun` + `interface` in the common module.

| ![KT-61573](./docs/imgs/kt-61573.png) |
|:-------------------------------------:|
| KT-61573 |

### Integration with JavaScript
## Kotlin-Js Interop

> The Kotlin-Js Interop is a feature of Kotlin/JS that allows the use of JavaScript libraries in Kotlin code and vice
> versa.
Module: [kotlin-js-interop](./kotlin-js-interop)

### Javascript to Kotlin

#### Demonstrations

<table>
<tr>
<td> <strong> Javascript </strong> </td> <td> <strong> Kotlin </strong> </td>
</tr>
<tr>
<td>

[func-export.js](./kotlin-js-interop/src/main/js/func-export.mjs)

</td>
<td>

[jsFuncImport.kt](./kotlin-js-interop/src/main/kotlin/kjs/jsFuncImport.kt)

</tr>
<tr>
<td>

[file-export.js](./kotlin-js-interop/src/main/js/file-export.mjs)

</td>
<td>

[jsFileImport.kt](/kotlin-js-interop/src/main/kotlin/kjs/jsFileImport.kt)

</td>
</tr>
</table>

#### NPM Dependencies

To use [NPM](https://www.npmjs.com/) dependencies in _Kotlin/JS_,
the dependencies must be added to the `dependencies` block of the `build.gradle.kts` file.

```kotlin
dependencies {
// Install npm dependencies
implementation(npm("randomstring", "1.3.0"))
}
```

And then define the exported function according to the JavaScript library's API.

<table>
<tr>
<td> <strong> Javascript </strong> </td> <td> <strong> Kotlin </strong> </td>
</tr>
<tr>
<td>

```javascript
var randomstring = require("randomstring");

randomstring.generate();
// >> "XwPp9xazJ0ku5CZnlmgAx2Dld8SHkAeT"

randomstring.generate(7);
// >> "xqm5wXX"
```

</td>
<td>

```kotlin
@JsModule("randomstring")
@JsNonModule
external object RandomStringFromNpm {
fun generate(
length: Int = definedExternally,
): String
}
```

</td>
</table>

> [!TIP]
> To delegate default parameter value to the imported JavaScript function, use `definedExternally`.
#### Build and Run

```bash
# from root
./gradlew kotlin-js-interop:nodeDevelopmentRun
```

### Kotlin to Javascript

#### Demonstration

<table>
<tr>
<td> <strong> Kotlin </strong> </td> <td> <strong> Javascript </strong> </td>
</tr>
<tr>
<td>

[Person.kt](./kotlin-js-interop/src/main/kotlin/kjs/Person.kt)

</td>
<td>

[importing.js](./kotlin-js-interop/src/main/js/importing.mjs)

</td>
</table>

#### Run

```bash
# from root
node kotlin-js-interop/src/main/js/importing.mjs
```

References:
#### References

- [JS to Kotlin Interop](https://kotlinlang.org/docs/js-to-kotlin-interop.html)
- [Dependencies from NPM](https://kotlinlang.org/docs/using-packages-from-npm.html)
- [Accessing External JavaScript Library](https://discuss.kotlinlang.org/t/kotlin-1-3-how-to-access-external-javascript-library-from-jsmain/15778)
- [Kotlinlang: JS to Kotlin Interop](https://kotlinlang.org/docs/js-to-kotlin-interop.html)
- [Kotlinlang: Kotlin to JS Interop](https://kotlinlang.org/docs/js-interop.html)
- [Kotlinlang: Dependencies from NPM](https://kotlinlang.org/docs/using-packages-from-npm.html)
- [Kt.Academy: JS Interop](https://kt.academy/article/ak-js-interop)
- [Dev.to: @JsExport guide for exposing Kotlin to JS](https://dev.to/touchlab/jsexport-guide-for-exposing-kotlin-to-js-20l9)

## Ktor Framework

Expand All @@ -73,7 +277,7 @@ fun main() {

### Define Application Module

In Ktor, the application module is using the `Application` class.
In _Ktor_, the application module is using the `Application` class.

```kotlin
fun Application.module() {
Expand Down Expand Up @@ -189,7 +393,7 @@ val response: HttpResponse = client.post("http://localhost:8080/customer") {
}
```

More [at](https://ktor.io/docs/request.html).
More examples [at](https://ktor.io/docs/request.html).

#### Responses

Expand All @@ -200,4 +404,4 @@ val httpResponse: HttpResponse = client.get("https://ktor.io/")
val stringBody: String = httpResponse.body()
```

More [at](https://ktor.io/docs/response.html#body).
More examples [at](https://ktor.io/docs/response.html).
Binary file added docs/imgs/kmp-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js-app/src/main/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
5 changes: 4 additions & 1 deletion js-app/src/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"type": "git",
"url": "https://github.com/kresil/experiments.git"
},
"private": true
"private": true,
"dependencies": {
"express": "^4.18.3"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {Person} from '../../../../build/js/packages/kresil-experiments-js-app/kotlin/kresil-experiments-js-app.mjs';
import {
Person
} from '../../../../build/js/packages/kresil-experiments-kotlin-js-interop/kotlin/kresil-experiments-kotlin-js-interop.mjs';

// Create an instance of Person
const john = new Person('John');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("unused")

package kjs

import kotlin.js.json
Expand Down
2 changes: 1 addition & 1 deletion kotlin-js-interop/src/main/kotlin/kjs/jsFileImport.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@file:JsModule("../../../../../js-app/src/main/js/file-export.mjs")
@file:JsModule("../../../../../kotlin-js-interop/src/main/js/file-export.mjs")

package kjs

Expand Down
2 changes: 1 addition & 1 deletion kotlin-js-interop/src/main/kotlin/kjs/jsFuncImport.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kjs

@JsModule("../../../../../js-app/src/main/js/func-export.mjs")
@JsModule("../../../../../kotlin-js-interop/src/main/js/func-export.mjs")
external fun defaultGreet(name: String): String

0 comments on commit 7f8a36d

Please sign in to comment.