Kotlin Multiplatform For Android and the Web - Part 3 The Web App

This article series contains parts of my thesis about Kotlin Multiplatform. The thesis is based on my Fuller Stack project which consists of an Android App, a Kotlin React.JS app and a Ktor server. The full thesis pdf can be found here.

Part 1 Introduction and the Server
Part 2 The shared module
Part 3 The Web App (This part)
Part 4 The Android App
Part 5 The Summary

The React.JS app is fully written in Kotlin thanks to Kotlin.JS and the React.JS Kotlin wrapper.

The app

Sign in screen - the only screen an unauthenticated user sees. Clicking the button navigates the user to the Auth0 sign in page
The Auth0 signing screen - the user can sign in using a Google account or email and password
Home screen - the main screen of the app. It features the notes list and a note editor which can be opened and closed
Settings screen - this screen allows the user to sign out or change their date formatting preferences.

Kotlin/JS

Besides the Kotlin/JS standard library, Kotlin leverages the existing JavaScript ecosystem and allows for the use of its libraries. A lot of popular libraries have a corresponding "Kotlin wrapper" which allows for a type safe and easier use from Kotlin. JetBrains (with the help of the community) also provides a set of wrappers. These wrappers i.a. include React.JS with helper libraries and Redux, both of which are used in this project.

Calling JavaScript from Kotlin

Given the alert function in JavaScript which shows a pop-up with a message, one way of calling this function in Kotlin is inlining JavaScript:

fun alert(message: String?) {
    return js("alert(message)")
}

One thing to note here is that the js function returns a dynamic type. Kotlin is a statically typed language but JavaScript is not and does not have any concept of types. This difference requires Kotlin to support cases where the type is unknown or just unexpected and this is what the dynamic type is used for. This dynamic type basically disables Kotlins type checker, which in hand removes compile-time safety. For example: JavaScript offers a function for calculating the minimum of the numbers. In Kotlin it could look like:

fun min(a: Int, b: Int): dynamic {
    return js("Math.max(a, b)")
}

In order to use the result of the min function this result needs to be casted from a dynamic type to a number type. This cast can only happen at run-time, which prevents the compiler from failing if there is a problem with  types.

Another problem with inlining JavaScript is the fact that the execution is also not compile-time safe. In the alert example above, changing the message parameter to text is still a valid Kotlin code which compiles even if the string inside the js function remains the same. At run-time, however, the application would not work as expected and executing the alert function would result in an error: "Uncaught ReferenceError: message is not defined". This is because Kotlin executes the JavaScript just as it is defined in the js function but the message variable does not exist anymore because it was replaced with text in Kotlin.

The safer option is to define a contract using the external keyword:

external fun alert(message: String?)

This contract assumes that the alert function is declared externally in the JavaScript world. This more or less suffers from the same dynamic problem as inlining JavaScript, but it leverages the Kotlin type system more. Changing the parameter names does not break the application at run-time.

Using JavaScript libraries

As it was mentioned before, Kotlin leverages the JavaScript ecosystem and a lot of Kotlin wrappers are provided for popular JavaScript libraries. These wrappers allow Kotlin to call those libraries out of the box. Unfortunately, the less popular libraries do not have such wrappers. In such case there is a possibility to write these wrappers by hand using the external keyword mentioned in the previous section. Doing this by hand can be tedious and error prone, fortunately there is an experimental solution for this problem called Dukat. This tool uses existing TypeScript declaration files in order to generate matching Kotlin external definitions.

Given a TypeScript declaration file like follows:

export function doSomething(): number;

Generating a Kotlin file with Dukat gives a result similar to this:

import kotlin.js.*
import org.khronos.webgl.*
import org.w3c.dom.*
import org.w3c.dom.events.*
import org.w3c.dom.parsing.*
import org.w3c.dom.svg.*
import org.w3c.dom.url.*
import org.w3c.fetch.*
import org.w3c.files.*
import org.w3c.notifications.*
import org.w3c.performance.*
import org.w3c.workers.*
import org.w3c.xhr.*

external fun doSomething(): Number

Dukat adds a lot of additional imports which are not always needed. Fortunately, the generated Kotlin files can be modified to contain only what is needed.

In this project this tool has been used to generate Kotlin files for two libraries, one is for authentication and the other is for a database.

Kotlin React.JS

This section will show some JavaScript React.JS examples and how they can be achieved in Kotlin.

The first example: An input for typing with a button
class TextInputExample extends React.Component {
  render() {
    return (
      <div>
        <input
          type="text"
          onChange={(event) => {
            const newVal = event.target.value;
            console.log(newVal);
          }}
        />
        <button onClick={() => console.log("Button clicked")}>
            A button
        </button>
      </div>
    );
  }
}

ReactDOM.render(<TextInputExample />, document.getElementById("root"));

React.JS uses JSX which is an extenstion for JavaScript and allows for writing code in an HTML-like syntax. In the example above TextInputExample is a class react component and it is used just as other HTML elements in React.JS (line 20).

class TextInputExample : RComponent<RProps, RState>() {
    override fun RBuilder.render() {
        input(InputType.text) {
            attrs.onChangeFunction = { event ->
                val newVal = (event.target as HTMLInputElement).value
                console.log(newVal)
            }
        }
        button {
            + "A button"
            attrs {
                onClickFunction = { console.log("Button clicked") }
            }
        }
    }
}

fun main() {
    window.onload = {
        render(document.getElementById("root")) {
            child(TextInputExample::class)
        }
    }
}

The Kotlin wrapper for React.JS does not use JSX but it uses a custom DSL for it. Instead of using HTML-like tags, Kotlin uses predefined functions which correspond to a given HTML tag. Since using React components differs from the JSX usage, an additional child function (line 21) needs to be used in order to place the React component.

The second example does not use a class component but a functional component which has better performance and involves less boilerplate code. Additionally, the component uses props and state. Props could be thought of arguments which are passed in from the parent component. When a prop value changes, the component is re-rendered in order to reflect the new value. While props are passed in from the parent, the state is self contained in the component and optionally passed to its children components. Both props and state changes cause the component to re-render.

The second example: A text label which value is based on a passed in prop and a checkbox which value is based on the components state
function CheckBoxExample(props) {
  const [isChecked, setIsChecked] = useState(false);

  return (
    <label>
      {props.name}
      <input
        type="checkbox"
        checked={isChecked}
        onChange={() => {
          setIsChecked(!isChecked);
        }}
      />
    </label>
  );
}

ReactDOM.render(
  <CheckBoxExample name="React.JS" />,
  document.getElementById("root")
);
interface ExampleProps : RProps {
    var name: String
}

val checkboxExample = functionalComponent<ExampleProps> { props ->
    val (isChecked, setIsChecked) = useState(false)

    div {
        label {
            + props.name
            input(InputType.checkBox) {
                attrs.checked = isChecked
                attrs.onChangeFunction = {
                    setIsChecked(!isChecked)
                }
            }

        }
    }
}

fun main() {
    window.onload = {
        render(document.getElementById("root")) {
            child(checkboxExample) {
                attrs.name = "React.JS"
            }
        }
    }
}

Because Kotlin is a static language, the props contract is defined in the form of an interface (line 1). Both JSX and Kotlin allow for easy extensions in the form of extracting existing logic into standalone components. But I personally think that modifying and creating functions in Kotlin is easier and safer than in JSX thanks to types and a less verbose syntax.

Modules

The main web app module consist of the React module along with its dependency modules.

  • Authentication - provides user authentication functionality using the Auth0 library;
  • Persistence - provides database access thanks to the Dexie.js library;
  • Shared - provides shared functionality.

Both the libraries for authentication and persistence were generated using Dukat.

Architecture

Redux

Redux is not an integral part of the whole project, so it will be briefly explained with a simple checkbox example.

The state of the app resides in a single state container branch (i.e a branch could relate to a screen or a feature) in this case App state. This state is only changed by the reducer, which is invoked when an action is dispatched. Because React.JS components do not ask for state but only react to state changes in combination with Redux this creates a unidirectional data flow:

View layer

The view layer is responsible for showing the user interface and reacting to user input. In React.JS the view is composed from components which could be thought of as small building bricks which are joined together in order to create something bigger like the notes list:

The whole notes list consists of an action row component and a number of note items. The Action row is composed from a button component and two icon components.

Some components hold their own state and some are derived from Redux. Usually things that relate to outside sources like the users notes are held in the Redux state.

Presentation layer

The presentation layer mainly revolves around Redux, since it is an integral part of almost every main application part like the notes list or the note editor. The Redux usage was inspired by Redux-toolkit and its convention of using "slices".

Slices can be thought of containers for related Redux classes and functions. The main building blocks of Redux are actions, reducers and states. Slices are the classes which hold these main building blocks. Referring back to the checkbox example, here's the Kotlin Redux code which could be used for this:

object CheckboxSlice {
    data class State(
        val checkboxValue: Boolean = false
    )

    data class SetCheckboxValue(val newValue: Boolean) : RAction

    fun reducer(state: State = State(), action: RAction): State {
        return when (action) {
            is SetCheckboxValue -> {
                state.copy(checkboxValue = action.newValue)
            }
            else -> state
        }
    }
}

This slice class contains the classes for the state, actions and a reducer function which calculates the new state.

For asynchronous actions like reaching out to the database, Redux thunk is used. The basic idea is such "Thunks"  provide a callback function which can be invoked after an asynchronous task ends. This callback takes in an action which is sent to the reducer in order to modify the state.

Summary

With all of this in mind the web app architecture for the View and Presentation layer can be summed like this:

The React.JS components dispatch actions usually as a result of a user input. These actions are sent to a reducer function which changes the state. Because React.JS components are reactive, any relevant state changes cause the components to re-render based on the new state.

Technologies used

Most of these libraries make use of Kotlin wrappers, but I have omitted the ones which come from the official JetBrains repository.

Libraries related to UI:

  • React.JS - allows for composing the user interface using small self contained components;
  • Styled components - provides easier styling for React.JS components;
  • Material UI - provides predefined React.JS components which adhere to material designs;
  • Muirwik - the Kotlin wrapper for Material UI.

Libraries related to logic:

  • React router - provides declarative navigation to React.JS apps;
  • Redux - a state container for JavaScript apps;
  • Redux thunk - an extension for Redux which allows for asynchronous actions;
  • Auth0 - allows for an easy integration of authentication in React.JS;
  • Kotlin Coroutines - provides asynchronous programming features to the Kotlin language. It is used in the React presentation layer;
  • Klock - library used for operating with notes timestamps and formatting them to dates on the notes list;
  • Kotlin Serialization - allows for an easy JSON serialization for API requests and responses;
  • Kodein - allows for injecting dependencies and retrieving them from the react module;
  • Dexie - the JavaScript library for the database. The Kotlin files were generated using Dukat.