Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polymorphic Serialization of ScreenParams. #26

Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
use json polymorphic serialization to serialize ScreenParams. remove …
…paramsAsString
luca992 committed Sep 14, 2023
commit ca7885ace1e5e5d50ae5cdd96e51dd0a9f9ba712
Original file line number Diff line number Diff line change
@@ -12,10 +12,9 @@ import eu.baroncelli.dkmpsample.composables.navigation.templates.TwoPane
import eu.baroncelli.dkmpsample.shared.viewmodel.Navigation
import eu.baroncelli.dkmpsample.shared.viewmodel.NavigationState
import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenIdentifier
import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Level1Navigation
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Screen

import eu.baroncelli.dkmpsample.shared.viewmodel.screens.ScreenParams

@Composable
fun Navigation.Router() {
Original file line number Diff line number Diff line change
@@ -9,13 +9,13 @@ import eu.baroncelli.dkmpsample.composables.screens.countrieslist.CountriesListT
import eu.baroncelli.dkmpsample.composables.screens.countrydetail.CountryDetailScreen
import eu.baroncelli.dkmpsample.shared.viewmodel.Navigation
import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenIdentifier
import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.CountryDetailParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Screen
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Screen.CountriesList
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Screen.CountryDetail
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.ScreenParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist.CountriesListState
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist.selectFavorite
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrydetail.CountryDetailParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrydetail.CountryDetailState


Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import com.russhwolf.settings.long
import com.russhwolf.settings.string
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Level1Navigation

class MySettings(s: Settings) {
class MySettings(private val s: Settings) {


// here we define all our local settings properties,
@@ -14,5 +14,5 @@ class MySettings(s: Settings) {
var listCacheTimestamp by s.long(defaultValue = 0)
var savedLevel1URI by s.string(defaultValue = Level1Navigation.AllCountries.screenIdentifier.URI)


fun clear() = s.clear()
}
Original file line number Diff line number Diff line change
@@ -52,7 +52,16 @@ class Navigation(val stateManager: StateManager) {
fun getStartScreenIdentifier(): ScreenIdentifier {
var startScreenIdentifier = navigationSettings.homeScreen.screenIdentifier
if (navigationSettings.saveLastLevel1Screen) {
startScreenIdentifier = ScreenIdentifier.getByURI(savedLevel1URI) ?: startScreenIdentifier
startScreenIdentifier = try {
ScreenIdentifier.getByURI(savedLevel1URI) ?: startScreenIdentifier
} catch (e: ScreenParamsDeserializationException) {
stateManager.dataRepository.localSettings.clear()
throw ScreenParamsDeserializationException(
"Failed to deserialize params for screen: ${startScreenIdentifier.screen.asString}. " +
"Local settings have been reset. Please restart the app.",
e
)
}
}
return startScreenIdentifier
}
Original file line number Diff line number Diff line change
@@ -3,29 +3,47 @@ package eu.baroncelli.dkmpsample.shared.viewmodel
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Level1Navigation
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Screen
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.ScreenInitSettings
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.ScreenParams
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

typealias URI = String

class ScreenParamsDeserializationException(message: String? = null, cause: Throwable? = null) :
Exception(message, cause)

class ScreenIdentifier private constructor(
val screen: Screen,
var params: ScreenParams? = null,
var paramsAsString: String? = null,
) {

val URI: URI
get() = returnURI()


companion object Factory {

internal val json = Json

fun get(screen: Screen, params: ScreenParams?): ScreenIdentifier {
return ScreenIdentifier(screen, params, null)
return ScreenIdentifier(screen, params)
}

fun getByURI(URI: String): ScreenIdentifier? {
val parts = URI.split(":")
Screen.values().forEach {
if (it.asString == parts[0]) {
return ScreenIdentifier(it, null, parts[1])
val splitAt = URI.indexOf(':')
val part0 = URI.substring(0, splitAt)
val part1 = URI.substring(splitAt + 1)
Screen.entries.forEach {
if (it.asString == part0) {
val jsonString = if (part1 == "null") null else part1
val params: ScreenParams? = try {
jsonString?.let { json.decodeFromString(jsonString) }
} catch (t: Throwable) {
throw ScreenParamsDeserializationException(
"Failed to deserialize params for screen: ${it.asString}",
t
)
}
return get(it, params)
}
}
return null
@@ -34,45 +52,39 @@ class ScreenIdentifier private constructor(
}

private fun returnURI(): String {
if (paramsAsString != null) {
return screen.asString + ":" + paramsAsString
val paramsString = if (params != null) {
json.encodeToString(params)
} else {
"null"
}
val toString = params.toString() // returns `ClassParams(A=1&B=2)`
val startIndex = toString.indexOf("(")
val paramsString = toString.substring(startIndex + 1, toString.length - 1)
return screen.asString + ":" + paramsString
}

// unlike the "params" property, this reified function returns the specific type and not the generic "ScreenParams" interface type
inline fun <reified T : ScreenParams> params(): T {
if (params == null && paramsAsString != null) {
val jsonValues = paramsStrToJson(paramsAsString!!)
params = Json.decodeFromString<T>("""{$jsonValues}""")
}
return params as T
}

fun paramsStrToJson(paramsAsString: String): String {
// converts `A=1&B=1` into `"A":"1","B":"2"`
val elements = paramsAsString.split("&")
var jsonValues = ""
elements.forEach {
if (jsonValues != "") {
jsonValues += ","
return try {
params as T
} catch (t: Throwable) {
if (screen.navigationLevel == 1) {
val defaultParams = Level1Navigation.entries.first {
it.screenIdentifier.screen.asString == screen.asString
}.screenIdentifier.params
debugLogger.log(
"Warning: Failed to cast params: $params as ${T::class} returning default L1 params for screen: " + "$defaultParams"
)
defaultParams as T
} else {
throw t
}
val parts = it.split("=")
jsonValues += "\"${parts[0]}\":\"${parts[1]}\""
}
return jsonValues
}


fun getScreenInitSettings(stateManager: StateManager): ScreenInitSettings {
return screen.initSettings(stateManager, this)
}

fun level1VerticalBackstackEnabled(): Boolean {
Level1Navigation.values().forEach {
Level1Navigation.entries.forEach {
if (it.screenIdentifier.URI == this.URI && it.rememberVerticalStack) {
return true
}
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ import kotlin.reflect.KClass


interface ScreenState
interface ScreenParams

class StateManager(repo: Repository) {

Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package eu.baroncelli.dkmpsample.shared.viewmodel.screens

import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenIdentifier
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.Screen.CountriesList
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist.CountriesListParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist.CountriesListType.ALL
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist.CountriesListType.FAVORITES

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package eu.baroncelli.dkmpsample.shared.viewmodel.screens


import eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist.CountriesListType
import kotlinx.serialization.Serializable

/***
* [ScreenParams] is an interface which defines the parameters to the passed to the screen if any are needed.
* Each class which implements it should be a data class and should always be set as [Serializable]
*
* Note: we are defining all implementations here instead of inside a screen's
* eu.baroncelli.dkmpsample.shared.viewmodel.screens.`screen-name` subpackage, because all classes implementing
* a sealed interface must be in the same package. The reason we are using a sealed interface is that it
* allows the list of ScreenParams subclasses that can be serialized in a polymorphic way to be
* determined at compile time vs having to explicitly registered them at runtime.
* See: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism
*/
@Serializable
sealed interface ScreenParams

@Serializable
data class CountriesListParams(val listType: CountriesListType) : ScreenParams

@Serializable
data class CountryDetailParams(val countryName: String) : ScreenParams
Original file line number Diff line number Diff line change
@@ -2,21 +2,17 @@ package eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrieslist

import eu.baroncelli.dkmpsample.shared.datalayer.functions.getCountriesListData
import eu.baroncelli.dkmpsample.shared.datalayer.functions.getFavoriteCountriesMap
import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenParams
import eu.baroncelli.dkmpsample.shared.viewmodel.StateManager
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.CallOnInitValues
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.CountriesListParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.ScreenInitSettings
import kotlinx.serialization.Serializable

// INIZIALIZATION settings for this screen
// this is what should be implemented:
// - a data class implementing the ScreenParams interface, which defines the parameters to the passed to the screen
// - Navigation extension function taking the ScreenParams class as an argument, return the ScreenInitSettings for this screen
// to understand the initialization behaviour, read the comments in the ScreenInitSettings.kt file

@Serializable // Note: ScreenParams should always be set as Serializable
data class CountriesListParams(val listType: CountriesListType) : ScreenParams

fun StateManager.initCountriesList(params: CountriesListParams) = ScreenInitSettings(
title = "Countries: " + params.listType.name,
initState = { CountriesListState(isLoading = true) },
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrydetail

import eu.baroncelli.dkmpsample.shared.datalayer.functions.getCountryInfo
import eu.baroncelli.dkmpsample.shared.viewmodel.ScreenParams
import eu.baroncelli.dkmpsample.shared.viewmodel.StateManager
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.CountryDetailParams
import eu.baroncelli.dkmpsample.shared.viewmodel.screens.ScreenInitSettings
import kotlinx.serialization.Serializable


// INIZIALIZATION settings for this screen
// to understand the initialization behaviour, read the comments in the ScreenInitSettings.kt file

@Serializable // Note: ScreenParams should always be set as Serializable
data class CountryDetailParams(val countryName: String) : ScreenParams

fun StateManager.initCountryDetail(params: CountryDetailParams) = ScreenInitSettings(
title = params.countryName,
initState = { CountryDetailState(isLoading = true) },