https://kotlinlang.org logo
Title
i

iamthevoid

05/20/2023, 8:17 AM
In my compose project i have several navigation graphs. I would to separate path "family" for each of them. But also i would to create shared mechanism to store path building code. So i create Interface with default methods Compose doesn't matter for question, just explanation. Code in 🧵
I've created interface with shared logic
interface Navigator {

    companion object {
        const val PARAMS_SEPARATOR = "&"
        const val PARAMS_START_SEPARATOR = "?"
        const val PARAMS_KV_SEPARATOR = "="
    }

    fun <T: Enum<T>> T.addParam(key: String, value: Any) : String {
        return name.addParam(key, "$value")
    }

    fun  String.addParam(key: String, value: String) : String {
        return buildString {
            append(this@addParam)
            append(if (contains(PARAMS_START_SEPARATOR)) PARAMS_SEPARATOR else PARAMS_START_SEPARATOR)
            append("$key$PARAMS_KV_SEPARATOR$value")
        }
    }
}
And enums to hold paths, example
enum class Routes: Navigator {
    LIST,
    ADD_SOURCE,
    SOURCE_SCREEN ,
    ;

    companion object {
        const val KEY_SOURCE_ID = "source_id"
    }
}
And now i would use code from navigator someway
There I assume that each of enum members is Navigator and has access to its code
First approach was to use Navigator just in NavGraph
navController.navigate(Routes.SOURCE_SCREEN.addParam(Routes.KEY_SOURCE_ID, it))
But it is impossible because methods defined in scope of navigator as extensions
so i try o modify navigator to use it anywhere
interface Navigator<T: Enum<T>> {

    companion object {
        const val PARAMS_SEPARATOR = "&"
        const val PARAMS_START_SEPARATOR = "?"
        const val PARAMS_KV_SEPARATOR = "="
    }

    fun  addParam(key: String, value: Any) : String {
        return (this as T).name.addParam(key, "$value")
    }

    fun  String.addParam(key: String, value: String) : String {
        return buildString {
            append(this@addParam)
            append(if (contains(PARAMS_START_SEPARATOR)) PARAMS_SEPARATOR else PARAMS_START_SEPARATOR)
            append("$key$PARAMS_KV_SEPARATOR$value")
        }
    }
}
It worked but it is dirty a bit, and i cant build chains like
Routes.ROUTE.addParam(key,value).addParam(key, value)
Because method for string is not modified yet and can't be modified to looks for user the same, as method for enum do So it is bad approach
Than i revert navigator and try another approach
I've add method just to enum member and tried to call it
enum class Routes: Navigator {
    LIST,
    ADD_SOURCE,
    SOURCE_SCREEN {
        fun addSourceId(sourceId: Long) {
            this.addParam(KEY_SOURCE_ID, sourceId)
        }
    },
    ;

    companion object {
        const val KEY_SOURCE_ID = "source_id"
    }
}
Compiler allow it, and i can build chains, good. But, for some reason compiler doesn't allow to call this method on enum member
navController.navigate(Routes.SOURCE_SCREEN.addSourceId(it))
Why cannot I call fun, defined in enum? How to better incapsulate code to build paths for navigation, if i use enums as destinations? How to preserve ability to chain calls to add more than one parameter in path?
As result i've done same
interface PathBuilder<T: Enum<T>> {

    companion object {
        const val PARAMS_SEPARATOR = "&"
        const val PARAMS_START_SEPARATOR = "?"
        const val PARAMS_KV_SEPARATOR = "="
    }

    fun addParam(key: String, value: Any) : String 
    
    fun addParams(param: Pair<String, Any>, vararg params: Pair<String, Any>) : String 

    fun String.addParamInternal(key: String, value: Any) : String {
        return buildString {
            append(this@addParamInternal)
            append(if (contains(PARAMS_START_SEPARATOR)) PARAMS_SEPARATOR else PARAMS_START_SEPARATOR)
            append("$key${PARAMS_KV_SEPARATOR}$value")
        }
    }
}
Inheritor
internal class DefaultPathBuilder<T : Enum<T>>(private val item: T) : PathBuilder<T> {

    override fun addParam(key: String, value: Any): String {
        return item.name.addParamInternal(key, "$value")
    }

    override fun addParams(param: Pair<String, Any>, vararg params: Pair<String, Any>): String {
        var path = item.name.addParamInternal(param.first, "${param.second}")
        for ((key, value) in params) {
            path = path.addParamInternal(key, value)
        }
        return path
    }
}
Interface
interface Navigator<T : Enum<T>> {
    fun pathBuilder(): PathBuilder<T> = DefaultPathBuilder(this as T)
}
Usage
val route = Routes.SOURCE_SCREEN.pathBuilder().addParam(Routes.KEY_SOURCE_ID, it)
navController.navigate(route)
I'll try to live with that from now on )