replace enums with multiple extends sealed classes - kotlin

Currently I have three enum classes that represents states in my state machine and one to display operations
interface State
enum class OperationState : State {
InProgress,
Finished,
Error
}
enum class FirstState : State {
//some states
}
enum class NextState: State {
//some states
}
enum class LastState: State {
//some states
}
In my service I have:
when (state) {
is FirstState -> {
//do something
changeFirstStateOperationState(state)
}
is NextState -> {
//do something
changeNextStateOperationState(state)
}
is LastState -> {
//do something
changeLastStateOperationState(state)
}
}
private fun changeFirstStateOperationState(state: FirstState){
when(state){
FirstState.A -> OperationState.Error
listOf(FirstState.B, FirstState.C) -> OperationState.InProgress
FirstState.D -> OperationState.Finished
}
}
I would like change my current implmenetation to sealed classes.
I tried something like:
sealed class State {
sealed class OperationState : State() {
sealed class FirstState : OperationState() {
object A: FirstState()
object B: FirstState()
object C: FirstState()
object D: FirstState()
}
sealed class NextState:OperationState(){ ... }
sealed class LastState:OperationState(){ ... }
}
}
but it doesn't work...
Is there any way, using the sealed class, to know what is current OperationStatus without mapping State to it?

This works for me
sealed class State {
sealed class OperationState : State() {
sealed class FirstState : OperationState() {
object A: FirstState()
object B: FirstState()
object C: FirstState()
object D: FirstState()
}
sealed class NextState:OperationState(){ }
sealed class LastState:OperationState(){ }
}
}
fun main() {
checkState(State.OperationState.FirstState.A)
}
fun checkState(state: State) {
when(state) {
is State.OperationState.FirstState.A -> "it's A"
is State.OperationState.FirstState -> "it's another FirstState"
is State.OperationState.NextState -> "it's a NextState"
is State.OperationState.LastState -> "LastState"
}.run(::println)
}
Just note that you have to specify the entire type name (State.OperationState...) unless you do an import (probably recommended if you're nesting this much), and also you have to use is in the when matcher, because you're comparing types. You don't need it for the first one (FirstState.A) since you're comparing to a specific singleton object, so it's up to you how you want it to look

Related

Kotlin - make multiple sealed classes have common set of "base" subclasses, but each could still add it's specific ones

This question has a wider scope than Extract common objects from sealed class in kotlin and Android - How to make sealed class extend other sealed class? so it's not a duplicate of these
I have multiple sealed classes that represent results of various API calls. Each of these calls has a common set of expected results (success, network error, unexpected error), but each could introduce it's own result types (like 'user not found' or 'wrong ID').
To avoid copying same subclasses to each of sealed class, I want to create a "base" type that includes all common result types, while each sealed class could add it's specific subclasses:
interface BaseApiCallResult {
data class Success(val data: String) : BaseApiCallResult
data class UnexpectedError(val error: Throwable) : BaseApiCallResult
data class NetworkError(val error: ApolloException) : BaseApiCallResult
}
sealed class ApiCallResult1 : BaseApiCallResult {
data class WrongID(val id: Int) : ApiCallResult1()
}
sealed class ApiCallResult2 : BaseApiCallResult {
data class UserDoesNotExist(val userid: Long) : ApiCallResult2()
}
sealed class ApiCallResult3 : BaseApiCallResult {
data class NameAlreadyTaken(val name: String) : ApiCallResult3()
}
the problem is that subclasses in "base" cannot be treated as "child" classes:
fun apiCall1(): ApiCallResult1 {
// won't compile, since BaseApiCallResult.UnexpectedError is not ApiCallResult1
return BaseApiCallResult.UnexpectedError(Exception(""))
}
fun useApi() {
when(val result = apiCall1()) {
is ApiCallResult1.WrongID -> { }
// compile error: Incompatible types
is BaseApiCallResult.Success -> { }
is BaseApiCallResult.UnexpectedError -> { }
is BaseApiCallResult.NetworkError -> { }
}
}
solution from Android - How to make sealed class extend other sealed class? might be applied here, but for big number of sealed classes (I expect I might need several dozen of such classes) it becomes rather hacky
interface BaseApiCallResult {
data class Success(val data: String) : Everything
data class UnexpectedError(val error: Throwable) : Everything
data class NetworkError(val error: ApolloException) : Everything
}
sealed interface ApiCallResult1 : BaseApiCallResult {
data class WrongID(val id: Int) : ApiCallResult1()
}
sealed interface ApiCallResult2 : BaseApiCallResult {
data class UserDoesNotExist(val userid: Long) : ApiCallResult2
}
sealed interface ApiCallResult3 : BaseApiCallResult {
data class NameAlreadyTaken(val name: String) : ApiCallResult3
}
// adding each new sealed interface here seems like a hack
interface Everything : BaseApiCallResult, ApiCallResult1, ApiCallResult2, ApiCallResult3
Additionally, with above solution, every when {...} complains about Everything case not being handled. I could resign from using Everything, but then I have to list all interfaces in each "base" subclass, which is simply terrible:
// just imagine how would it look if there were 30 ApiCallResult classes
interface BaseApiCallResult {
data class Success(val data: String) : BaseApiCallResult, ApiCallResult1, ApiCallResult2, ApiCallResult3
data class UnexpectedError(val error: Throwable) : BaseApiCallResult, ApiCallResult1, ApiCallResult2, ApiCallResult3
data class NetworkError(val error: ApolloException) : BaseApiCallResult, ApiCallResult1, ApiCallResult2, ApiCallResult3
}
Is there a better way to handle this kind of situation ?
You have to separate ApiResult from ApiMethodResult they should not to be relatives.
Kotlin already has type Result and you can use it:
sealed interface ApiCall1Result {
class WrongID : ApiCall1Result
class UserInfo(val userId: Int) : ApiCall1Result
}
fun api1() : Result<ApiCallResult>
fun useApi1() {
val result = api1()
if(result.isFailure) {
handle failure
} else {
val apiResult = result.getOrThrow()
when(apiResult) {
is WrongID -> {}
is UserInfo -> {}
}
}
}
Or you can implement it by your self:
interface ApiResult<in T> {
class Success<T : Any>(val data: T) : ApiResult<T>
class Fail(val error: Throwable) : ApiResult<Any>
}
sealed class ApiCallResult1 {
class WrongID(val id: Int) : ApiCallResult1()
class UserInfo(val id: Int, val name: String) : ApiCallResult1()
}
fun apiCall1(): ApiResult<ApiCallResult1> {
return ApiResult.Fail(Throwable())
}
fun useApi() {
when (val result = apiCall1()) {
is ApiResult.Fail -> {}
is ApiResult.Success -> when (result.data) {
is ApiCallResult1.WrongID -> {}
is ApiCallResult1.UserInfo -> {}
}
}
}
You could create a generic type for the sealed interface, and this type gets wrapped by one additional child class:
interface ApiCallResult<out O> {
data class Success(val data: String) : ApiCallResult<Nothing>
data class UnexpectedError(val error: Throwable) : ApiCallResult<Nothing>
data class NetworkError(val error: ApolloException) : ApiCallResult<Nothing>
data class Other<out O>(val value: O): ApiCallResult<O>
}
Then you can define your other callback types using a specific class as the O type:
data class UserDoesNotExist(val userid: Long)
fun handleApiCallResult2(result: ApiCallResult<UserDoesNotExist>) {
when (result) {
is ApiCallResult.Success -> {}
is ApiCallResult.UnexpectedError -> {}
is ApiCallResult.NetworkError -> {}
is ApiCallResult.Other -> {
// do something with result.value
}
}
}
When you have more than one other case, you can create a sealed interface to be the parent of those other cases, but you'll unfortunately need a nested when to handle them.
When you have no other cases, you can use ApiCallResult<Nothing> as your response type, but you'll unfortunately need to leave a do-nothing {} branch for the Other case. Or you could set up a separate sealed interface like in your long-winded final solution in your question, which is manageable because it would never grow to more than two sealed types.

tinder state machine - working with lists

Im trying to implement state machine with some complicated business logic
I'm holding a list of objects that I need to iterate and send each object to the state machine
sealed class State {
object PendingForAction: State()
object ActionFailed : State()
object ActionCompleted : State()
object PendingNextAction : State()
}
sealed class Event() {
object OnPendingForAction : Event()
object OnActionFailed : Event()
object OnActionCompleted : Event()
}
sealed class SideEffect() {
object UpdateStatusPendingForAction : SideEffect()
object UpdateStatusActionFailed : SideEffect()
object UpdateStatusCompleted : SideEffect()
}
#Component
class StateMachine #Autowired constructor(
) {
#Autowired
lateinit var repository: SomeRepository
val stateMachine = StateMachine.create<State, Event, SideEffect> {
initialState(State.PendingForAction)
state<State.PendingForAction> {
on<Event.OnActionFailed> {
transitionTo(State.ActionFailed, SideEffect.UpdateStatusActionFailed)
}
on<Event.ActionCompleted> {
transitionTo(State.PendingNextAction, SideEffect.UpdateStatusCompleted)
}
// so on with next actions
}
onTransition() {
val validTransition = it as? StateMachine.Transition.Valid ?: return#onTransition
when (validTransition.sideEffect) {
SideEffect.UpdateStatusPendingForAction -> {
//here i want to do complicated code that use the data object from the list
//but i can't access it
}
// and so on ...
}
}
}
}
now trigger somehow the state machine with my data:
val data = listOf(obj,obj,obj...)
// each object is in a different state and I want it to be generic enough to know the
state and perform the transition automatically
data.forEach{
stateMachine.transition(it)
}
please note that transition function only accept sideEffect but i want it to accept the object and it's state

Generics in Objects

I have a question about sealed class, generics and object.
Let's say I would like to model something like 3 finite cases with a sealed class something like this:
sealed class ChangeState<S> {
fun reduceState(state: S): S
}
data class SetState<S>(val newState: S) : ChangeState<S>() {
override fun reduce(state: S): S = newState
}
object NoStateChange : ChangeState<Nothing>() { // What do I specify here for ChangeState? Nothing?
override fun reduce(state: Nothing): Nothing {
throw Exception("This should never be called")
}
}
The goal is to provide a convenient way to define NoStateChange in a generic way that it can be used as following:
fun foo(i : Int) : ChangeState<Int> {
return if (i==0)
NoStateChange // Won't compile because return type is ChangeState<Nothing> but expected ChangeState<Int>
else
SetState(i)
}
Is there a way to do that with object and Generics somehow?
As pointed out by #Tenfour04 the issue is that out is needed but reduceState() would require in as well. However, reduceState() can be refactored out of the class hierarchy and moved to an extension function like that:
sealed class ChangeState<out S>
data class SetState<S>(val newState: S) : ChangeState<S>()
object NoStateChange : ChangeState<Nothing>()
fun <S> ChangeState<S>.reduce(state: S): S {
return when (val change = this) {
is SetState -> change.newState
is NoStateChange -> state
}
}

Get kotlin sealed class name heirarchy

I wrote a dirty function that gives me the name structure of a sealed class heirarchy.
It sucks because its using reflection - so i'm asking if there's a better, non reflective way.
fun KClass<*>.sealedClassName(): String {
var className = simpleName
var sealedParent = superclasses.firstOrNull { it.isSealed }
while (sealedParent != null) {
className = "${sealedParent.simpleName}.$className"
sealedParent = sealedParent.superclasses.firstOrNull { it.isSealed }
}
return className ?: qualifiedName ?: jvmName
}
interface AnInterface
open class AnClass
sealed class Grandparent : AnClass(), AnInterface {
sealed class Parent : Grandparent(), AnInterface {
object Child : Parent()
}
object AuntieObject : Grandparent()
data class UncleData(val x: Int) : Grandparent()
}
#Test
fun sealedClassName() {
check(Grandparent.Parent::class, "Grandparent.Parent")
check(Grandparent.AuntieObject::class, "Grandparent.AuntieObject")
check(Grandparent.UncleData::class, "Grandparent.UncleData")
check(Grandparent.Parent.Child::class, "Grandparent.Parent.Child")
check(Unit::class, "Unit")
}

Kotlin secondary constructor with generic type

In java
I can achieve two constructors like
public TargetTitleEntryController() { }
public <T extends Controller & TargetTitleEntryControllerListener> TargetTitleEntryController(T targetController) {
setTargetController(targetController);
}
I want to convert it to Kotlin
class TargetTitleEntryController ()
with the secondary constructor. I don't know how to declare with generic type like Java counterpart.
There is no intersection types in Kotlin (sad)
But there is Generic constraints (hope)
But Generic constraints not applicable in the secondary constructor (sad)
But you can simulate secondary constructor in a companion object using Invoke operator overloading (workaround):
class TargetTitleEntryController {
// ...
companion object {
operator fun <T> invoke(targetController: T): TargetTitleEntryController
where T : Controller,
T : TargetTitleEntryControllerListener {
return TargetTitleEntryController().apply {
setTargetController(targetController)
}
}
}
}
Here is an example where you specify a Type T which implements two interfaces (CharSequence, Runnable):
class Person<T>(val name: String) where T : CharSequence, T : Runnable {
constructor(name: String, parent: T) : this(name) {
}
}
So actually something like this should work:
class TargetTitleEntryController<T> () where T : Controller, T : TargetTitleEntryControllerListener {
constructor(targetController: T) : this() {
}
}
You can do it like this :)
class TargetTitleEntryController <T>() : Controller() where T: Controller, T: TargetTitleEntryControllerListener<T> {
constructor(target: T) : this() {
targetController = target
}
}
you can implement it in your parent controller like this:
class TargetDisplayController : Controller(), TargetTitleEntryControllerListener<TargetDisplayController> {
var targetTitleEntryController = TargetTitleEntryController(this)
override fun onTitlePicked(String option) {
}
override fun onAttach(view: View) {
// push controller here
}
}