KStateMachine / kstatemachine

KStateMachine is a Kotlin DSL library for creating state machines and statecharts.
https://kstatemachine.github.io/kstatemachine/
Boost Software License 1.0
339 stars 19 forks source link

Question: How should download progress be handled? #46

Closed dave08 closed 1 year ago

dave08 commented 1 year ago

If I have a state data class called data class Downloading(val progress: Int) : DefaultState() how can I update the progress so that when I listen to state changes in the the view, I can update it accordingly?

nsk90 commented 1 year ago

Hi, seems that your question is not related to state machine it self, so do it the same way like Downloading is a simple data class not related with a state. Looks that progress field should be var or some observable val? Add some notification about changes of its value (observer pattern) and listen to it in view layer.

You can look at this sample https://github.com/nsk90/android-kstatemachine-sample/blob/main/app/src/main/java/ru/nsk/kstatemachinesample/ui/main/MainViewModel.kt See how ammoLeft field is updated. Just imagine that it is a progress) The only difference that in this sample this field is stored in another class, but it does not matter, you can store it in your Downloading state.

dave08 commented 1 year ago

The thing is that if I addState this Downloading class with one value and it gets copied with a different one, then it's not the same instance registered with all it's transactions... And I get new state with processEvent... So where could I put the changing state, in an onTriggered block? Can state be replaced or muted there?

You're suggesting the progress itself shouldn't be a transition in the state machine at all? Then if I'm not yet at 100 when i get another (invalid) transition what would happen? In your example you cancel the job, but you're not getting any new states from the events coming in... So it's possible just to do it that way.

nsk90 commented 1 year ago

It is not possible to combine immutable data class and State in one class. State cannot be copied or replaced with another one, but you can manage State properties as you want.

You can implement it like this:

class Downloading(val progress: SomeObservable<Int>) : DefaultState() // this way you can observe Int value

or:

class Downloading(var progress: Int) : DefaultState() {
    // some code for subscription and notification
}

or:

data class Downloading(val progress) // immutable data class if you need it.

class DownloadingState(val progress: SomeObservable<Downloading>) : DefaultState() // state with observable Downloading property

I suppose that when your state machine is in DownloadingState your app starts some operation, timer or a thread that updates progress value. That operation is started in onEntry() callback and cancelled in onExit() if it happens earlier than operation completes itself (for example as a result of CancelOperationEvent). When operation processing is complete app sends SomeOperationCompleteEvent to machine, and it should switch to another state.

There is no way to attach progress to transition as transition just happens, it has no lifetime during which progress could be updated. Only states have a "lifetime" which starts when onEntry() is called and finishes when onExit() is called. This lifetime is a scope where your app can do some operation and notify about its progress.

I am not sure what you mean saying "invalid" transition? When machine receives some event it is matched with defined transitions, if matching transition is found, it is triggered and machine switches to target state, if not then event is ignored by default. If you want to block transitions from Downloading state until progress is 100 you can do it in transition guard function.

looks that you have to provide additional information or code sample how you want to update progress value?

dave08 commented 1 year ago

Say I have:

// Coming from another service doing this
sealed class DownloadState : Event {
   object WaitingForDownload : DownloadState()
   data class Downloading(val progress: Int) : DownloadState()
   data class MoveTo(val location: Path) : DownloadState()
}

// The state machine needs to listen to these events and transform them to states (??? progress doesn't work here ???) for the compose UI
val stateFlow = MutableStateFlow<DownloadState>(...)

// Just manage transitions from state to state according to events recieved from other service
val sm = createStateMachine {
...
}

// In the viewModel -- deliver state changes to the UI from the state machine
sm.onStateChanged {
// ... update UI according to state --- but progress isn't a state...?
}

I'd like to separate the concern of updating the UI from managing the transitions, whereas in your code in the sample -- the UI should be updated directly in the state machine, or a new abstraction layer needs to be created just to provide updates to a separate flow for progress.... I was wondering if I could use the state machine to update the progress somewhere, and provide that to update the UI --- but only when in the downloading state.

By the way, if you're on Kotlin Slack it might be easier to have this discussion... and the library might draw more users/contributors with updates posted on their #feed channel, and maybe even it's own channel there... thanks for taking the time to answer in any way!

nsk90 commented 1 year ago
sealed class DownloadState : Event {
   object WaitingForDownload : DownloadState()
   data class Downloading(val progress: Int) : DownloadState()
   data class MoveTo(val location: Path) : DownloadState()
}

I dont understand this construction, this is mistape or you want States to be Events at the same time?

usually there are two separate sealed classes for Events and States.

nsk90 commented 1 year ago

It is not possible, additional abstraction layer is really necessary. Or you have to provide 100 states with values from 0 to 100, and switching between them. State machine itself notifies only about state changes and transitions, it does not know about states properties changes.

As I see slack is not working in Russia any more.

nsk90 commented 1 year ago

you can use state machine to update the value of your progress (by sending events to it while it is in Downloading state, and having targetless transition in that state which updates the value) but you cannot use state machine notifications to tell to UI layer that the value was updated. additional notification (observable) is required.

nsk90 commented 1 year ago

I recommend to combine notifications from state machine and from progress value updates in ViewModel into a single flow.

dave08 commented 1 year ago
sealed class DownloadState : Event {
   object WaitingForDownload : DownloadState()
   data class Downloading(val progress: Int) : DownloadState()
   data class MoveTo(val location: Path) : DownloadState()
}

I dont understand this construction, this is mistape or you want States to be Events at the same time?

usually there are two separate sealed classes for Events and States.

Those are really events... Coming from an older system, i'm refactoring old code. I do have a separate sealed class for states

dave08 commented 1 year ago

It is not possible, additional abstraction layer is really necessary. Or you have to provide 100 states with values from 0 to 100, and switching between them. State machine itself notifies only about state changes and transitions, it does not know about states properties changes.

I realized that, I guess what I really wanted was a better way to listen to those transitions from state A back to state A outside the state machine, without having to do too muchtype checking and type casting...

As I see slack is not working in Russia any more.

Boy, I hope this war will be finished soon! It reached even Slack!