Skip to content

Commit

Permalink
use json polymorphic serialization to serialize ScreenParams. remove …
Browse files Browse the repository at this point in the history
…paramsAsString
  • Loading branch information
luca992 committed Sep 13, 2023
1 parent b84b006 commit c5fd320
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import eu.baroncelli.dkmpsample.composables.navigation.templates.TwoPane
import 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.ScreenParams


@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import eu.baroncelli.dkmpsample.composables.screens.countrydetail.CountryDetailScreen
import eu.baroncelli.dkmpsample.composables.screens.countrieslist.CountriesListScreen
import eu.baroncelli.dkmpsample.composables.screens.countrieslist.CountriesListTwoPaneDefaultDetail
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.*
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


Expand Down Expand Up @@ -50,7 +51,6 @@ fun Navigation.ScreenPicker(
}



@Composable
fun Navigation.TwoPaneDefaultDetail(
screenIdentifier: ScreenIdentifier
Expand All @@ -61,7 +61,7 @@ fun Navigation.TwoPaneDefaultDetail(
CountriesList ->
CountriesListTwoPaneDefaultDetail()

else -> Box{}
else -> Box {}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Up @@ -46,7 +46,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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,88 @@ 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 kotlinx.serialization.decodeFromString
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,
val screen: Screen,
var params: ScreenParams? = null,
var paramsAsString: String? = null,
) {

val URI : URI
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
}

}

private fun returnURI() : String {
if (paramsAsString != null) {
return screen.asString + ":" + paramsAsString
private fun returnURI(): String {
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
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 += ","
inline fun <reified T : ScreenParams> params(): T {
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 getScreenInitSettings(stateManager: StateManager): ScreenInitSettings {
return screen.initSettings(stateManager, this)
}

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


interface ScreenState
interface ScreenParams

class StateManager(repo: Repository) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package eu.baroncelli.dkmpsample.shared.viewmodel.screens

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


// CONFIGURATION SETTINGS
Expand All @@ -18,6 +18,6 @@ object navigationSettings {
// LEVEL 1 NAVIGATION OF THE APP

enum class Level1Navigation(val screenIdentifier: ScreenIdentifier, val rememberVerticalStack: Boolean = false) {
AllCountries( ScreenIdentifier.get(CountriesList, CountriesListParams(listType = ALL)), true),
FavoriteCountries( ScreenIdentifier.get(CountriesList, CountriesListParams(listType = FAVORITES)), true),
AllCountries(ScreenIdentifier.get(CountriesList, CountriesListParams(listType = ALL)), true),
FavoriteCountries(ScreenIdentifier.get(CountriesList, CountriesListParams(listType = FAVORITES)), true),
}
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
Expand Up @@ -2,23 +2,18 @@ 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.debugLogger
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 (
fun StateManager.initCountriesList(params: CountriesListParams) = ScreenInitSettings(
title = "Countries: " + params.listType.name,
initState = { CountriesListState(isLoading = true) },
callOnInit = {
Expand All @@ -38,7 +33,7 @@ fun StateManager.initCountriesList(params: CountriesListParams) = ScreenInitSett
}
},
callOnInitAtEachNavigation = CallOnInitValues.CALL_BEFORE_SHOWING_SCREEN
// enabling in this way favourites can refresh at each navigation
// CALL_BEFORE_SHOWING_SCREEN is used, as favourites come from the local storage and not from the network
// (for more information about "callOnInitAtEachNavigation" values, look at "ScreenInitSettings" class definition)
// enabling in this way favourites can refresh at each navigation
// CALL_BEFORE_SHOWING_SCREEN is used, as favourites come from the local storage and not from the network
// (for more information about "callOnInitAtEachNavigation" values, look at "ScreenInitSettings" class definition)
)
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
package eu.baroncelli.dkmpsample.shared.viewmodel.screens.countrydetail

import eu.baroncelli.dkmpsample.shared.datalayer.functions.getCountryInfo
import eu.baroncelli.dkmpsample.shared.viewmodel.Navigation
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 (
fun StateManager.initCountryDetail(params: CountryDetailParams) = ScreenInitSettings(
title = params.countryName,
initState = { CountryDetailState(isLoading = true) },
callOnInit = {
Expand Down

0 comments on commit c5fd320

Please sign in to comment.