kotlinkotlin-multiplatformlottiekotlin-js

Kotlin/JS: How to use a Dukat external interface that has functions in it


I'm trying to use Lottie along with Kotlin/JS.

I've been able to create a Lottie external declarations file with Dukat, which gave me an external interface called AnimationConfig (represents the parameters of the animation that will be played by LottiePlayer), an external interface called LottiePlayer (represents all the animation player functions), as well as a variable called Lottie (of type LottiePlayer):

@file:JsModule("lottie-web")
@file:JsNonModule
@file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS")

external interface AnimationConfig {
    var container: Element
    var renderer: String?
        get() = definedExternally
        set(value) = definedExternally
    var loop: dynamic /* Boolean? | Number? */
        get() = definedExternally
        set(value) = definedExternally
    var autoplay: Boolean?
        get() = definedExternally
        set(value) = definedExternally
    var initialSegment: dynamic /* JsTuple<Number, Number> */
        get() = definedExternally
        set(value) = definedExternally
    var name: String?
        get() = definedExternally
        set(value) = definedExternally
    var assetsPath: String?
        get() = definedExternally
        set(value) = definedExternally
    var rendererSettings: Any?
        get() = definedExternally
        set(value) = definedExternally
    val audioFactory: ((assetPath: String) -> `T$1`)?
}

external interface LottiePlayer {
    fun play(name: String = definedExternally)
    fun pause(name: String = definedExternally)
    fun stop(name: String = definedExternally)
    fun setSpeed(speed: Number, name: String = definedExternally)
    fun setDirection(direction: Number /* 1 */, name: String = definedExternally)
    fun setDirection(direction: Number /* 1 */)
    fun setDirection(direction: String /* "-1" */, name: String = definedExternally)
    fun setDirection(direction: String /* "-1" */)
    fun searchAnimations(animationData: Any = definedExternally, standalone: Boolean = definedExternally, renderer: String = definedExternally)
    fun loadAnimation(params: AnimationConfig /* AnimationConfig<T> & `T$5` | AnimationConfig<T> & `T$6` */): AnimationItem
    fun destroy(name: String = definedExternally)
    fun registerAnimation(element: Element, animationData: Any = definedExternally)
    fun setQuality(quality: String)
    fun setQuality(quality: Number)
    fun setLocationHref(href: String)
    fun setIDPrefix(prefix: String)
    fun updateDocumentData(path: Array<Any /* String | Number */>, documentData: TextDocumentData, index: Number)
}

@JsName("default")
external var Lottie: LottiePlayer

Then in the Kotlin/JS side, I'm trying to use these externals to play an animation inside a Div generated with Kotlin compose for web. Here is my main() function:

fun main() {
    renderComposable(rootElementId = "root") {

        Div( attrs = {
            id("svgContainer")
        }) {

            Text("Hola mundo!")

            @Suppress("DEPRECATION")
            DomSideEffect {

                val svgContainer = it

                @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
                val animationConfig = js("{}") as AnimationConfig
                animationConfig.container = svgContainer
                animationConfig.loop = false
                animationConfig.autoplay = true
                animationConfig.assetsPath = "https://labs.nearpod.com/bodymovin/demo/markus/halloween/markus.json"

                Lottie.loadAnimation(animationConfig)
            }
        }
    }
}

The problem with this implementation is that the application throws a runtime error indicating that the variable Lottie is undefined:

Uncaught TypeError: Cannot read properties of undefined (reading 'loadAnimation')

I haven't found a way to use the available variable Lottie in order to get an instance of it and use it.

How should I use either the variable Lottie or the external interface LottiePlayer?

Thanks in advance.


Solution

  • Currently Dukat is incredibly experimental, and essentially never generates the correct bindings, if it works at all. It's best used as an initial step to generate a starting point, and then manually implementing the correct bindings by manually inspecting and understanding the actual JavaScript and TypeScript.

    Inspecting the TypeScript types

    Looking at the index.d.ts for lottie-web I can see the definition for LottiePlayer.

    The relevant part is the loadAnimation() function.

    export type LottiePlayer = {
        // ...
        loadAnimation<T extends RendererType = 'svg'>(params: AnimationConfigWithPath<T> | AnimationConfigWithData<T>): AnimationItem;
        // ...
    }
    

    The function accepts a union type with two different types,

    These both extend a base type, AnimationConfig<T>.

    Comparing with the generated code

    However Dukat has generated a function that only accepts AnimationConfig

    external interface LottiePlayer {
        // ...
        fun loadAnimation(params: AnimationConfig /* AnimationConfig<T> & `T$5` | AnimationConfig<T> & `T$6` */): AnimationItem
        // ...
    }
    

    (Dukat has also generated a comment (/* AnimationConfig<T> & `T$5` | AnimationConfig<T> & `T$6` */) indicating that it knows that the argument is a union type.)

    Bindings for generic types, <T extends RendererType>

    The generated AnimationConfig does not have a type parameter, which might usually be a problem. It's not surprising that Dukat failed because the parameter is bounded by a string union, which isn't something that's easily mappable to Kotlin.

    export type RendererType = 'svg' | 'canvas' | 'html';
    
    export type AnimationConfig<T extends RendererType = 'svg'> = {
        // ...
    }
    

    However it is bounded to be a constant string, 'svg', so the Kotlin binding doesn't need to be aware of it - any instance of T will just return a string. Fortunately the same is true of the subtypes, AnimationConfigWithPath and AnimationConfigWithData.

    Fixing the Kotlin bindings

    Kotlin doesn't have union types, which can be an issue when trying to bind functions that return unions. It's not a problem with functions arguments though, because that's the same as function overloading, which Kotlin already has.

    external interface LottiePlayer {
        // fun loadAnimation(params: AnimationConfig /* AnimationConfig<T> & `T$5` | AnimationConfig<T> & `T$6` */): AnimationItem
        fun loadAnimation(params: AnimationConfigWithPath): AnimationItem
        fun loadAnimation(params: AnimationConfigWithData): AnimationItem
    }
    

    And then you have to define externals for AnimationConfigWithPath and AnimationConfigWithData.

    external interface AnimationConfigWithPath : AnimationConfig {
        var animationData: dynamic
    }
    
    external interface AnimationConfigWithData : AnimationConfig {
        var animationData: dynamic
    }
    

    I haven't tested this code - so if there are corrections please let me know.