In the context of viewmodels/android, which one of...
# coroutines
o
In the context of viewmodels/android, which one of the following three is recommended (in general for events from view => viewmodel)
Copy code
// option 1
interface HomeViewModel {
    fun onLoginButtonClick()
}

// option 2
interface HomeViewModel {
    val loginButtonClicks: SendChannel<ButtonClick>
    object ButtonClick
}

// option 3
interface HomeViewModel {
    val loginButtonClicks: Flow<ButtonClick>
    object ButtonClick
}
z
4
😅 2
Copy code
interface HomeViewModel {
  fun send(intention: Intention)
}
Copy code
sealed class Intention {
  object ButtonClick : Intention()
}
o
So you suggest a single generic event functions that needs to be called for all events by the view
Could you elaborate?
z
you could use a
SendChannel<T>
too, except you don’t want to expose the ability to close that channel
you want a
sealed class
representing all forms of input so you can use an exhaustive
when
to process it
you can add logs for every intention sent
representing input as an object has its advantages like that
internally, your
HomeViewModel
just needs to process those input events in order and to build a
Flow<State>
for your UI to consume
I personally use an
actor
coroutine to process input events and build state and it is awesome https://kotlinlang.org/docs/reference/coroutines/shared-mutable-state-and-concurrency.html#actors
o
I agree with representing input as an object, although I'm trying to see why it should be done as at a single point. Does this also mean a Viewmodel should only expose a single Flow for state?
z
the
ViewModel
can expose any number, but I generally just expose 1 flow of state
`ViewModel`’s job is generally to collect (observe) output of multiple
Repository
classes and reduce them into a single state for your UI
💯 1
though I generally scope my
ViewModel
classes to a user flow rather than a UI component such as an Activity or Fragment. This allows me to inject multiple view models into each other or observe one view model for
FragmentA
in
FragmentB
o
Because there is the following usecase where I think your solution struggles: Given a screen, where a dialog that has a button, could be shown. With your idea, with a single
send
method, the view is allowed to send a
dialogbuttonclickEvent
when the dialog is not shown. However, I thought of the following:
Copy code
interface ViewModel {
    val dialogState: Flow<DialogState>

    sealed class DialogState {
        object NotShown : DialogState()
        class Shown : DialogState() {
            val buttonClicks: Flow<..>
// or
            val buttonClicks: SendChannel<..>
        }
    }
}
This way it's more type-safe, isn't it?
z
State
is always a data class for me
using a sealed class for it generally means you are violating this principle here: https://publicobject.com/2020/01/01/modeling-states-vs-facts/
though a
sealed class
can work with simple states
why does sending a
dialogbuttonclickEvent
matter if the view isn’t shown?
if the user clicked the dialog button, then the
ViewModel
should process it. and it could decide to drop it
o
because its impossible for a dialogbuttonclickEvent to happen if the dialog is not shown
1
z
then how can it be sent in the first place?
w
It can’t, but you want this to be immediately clear from the code
o
because the view called
view.send(dialogbuttonclickEvent)
, like you suggested @zak.taccardi initially
z
I fail to understand how its an issue
o
the view called
viewmodel.sent(dialogbuttonclickEvent)
while
viewmodel.dialogState == NotShown
z
then when you process
dialogButtonClickEvent
, do not update the current
State
under that condition
o
I mean yes, that could be an option, but that's not typesafe nice right? Doesn't come down to the same why state machines exist, since every state can have it's own possible events
1
z
how are you preventing invalid input then?
o
by exposing the possible events with the state itself, like my code example above
Copy code
interface ViewModel {
    val dialogState: Flow<DialogState>
    sealed class DialogState {
        object NotShown : DialogState()
        class Shown : DialogState() {
            val buttonClicks: Flow<..>
// or
            val buttonClicks: SendChannel<..>
        }
    }
}
This way, button dialog click event can only be sent if the dialog state is Shown
z
Copy code
class Shown : DialogState() {
            val buttonClicks: Flow<..>
How does a
Flow<T>
live on a
State
which is effectively a
data class
?
o
This was a quick proof of concept, maybe state isn't the correct term here
StateMachine perhaps?
z
it’s certainly interesting but I have never done something like that
I tend to keep it simple.
State
+
Intention
= (new)
State
. And the
View
or the
ViewModel
itself can send
Intention
objects to update that
State
o
I've never done it before myself, was in the livedata + simple method onXX, now trying to do the coroutines way, but this striked me
But it's better to narrow the possible intentions as narrow as possible, isn't it?
You are now probably narrowing the possible intentions to the current ViewModel, but wouldn't be even better to narrow it down to the current state?
z
it’s a trade-off, right
(in complexity)
eventually view model states become quite complex and you are collecting a lot of data, so switching from one state to another isn’t always feasible because otherwise you will lose that data
o
I don't see much added complexity, its just defining the possible intent in the current state scope, instead of the viewmodel scope
I don't get that, since the data would be saved in the repository anyawy thats being called
z
repository exposes
Flow<Data>
though
o
Furthermore, that's a viewmodel implementation detail, I think the view should only be concered what it can do in the current state
z
well the following is pretty simple
Copy code
data class State(val showDialog: Boolean)
o
I think we are going off-topic talking about how a repository should be implemented
Yes, that's the current state, but the dialog can only be closed when it's shown. You want to communicate that to the view
z
sure, but its still added complexity
and then you aren’t sending dumb data classes as
State
anymore
o
Added complexitiy is debatable imo
z
if it works for you then great. it is certainly an interesting concept
w
It’s moved complexity at best, since now view model won’t have to consider invalid intentions
1
o
^that's true
z
all intentions for the
ViewModel
are valid. you mean the
View
will only be able to consider intentions appropriate for that
State
something like this probably works real well with compose
w
all intentions for the ViewModel are valid
I’d argue it’s view model’s responsibility to define which intentions are valid and which are not. View should be only an implementation detail and it shouldn’t be able to do anything that view model doesn’t expect or allow
z
Intention
would be a
sealed class
on the
ViewModel
. This defines what can be used to update the `ViewModel`’s state. If it’s not able to update the `ViewModel`’s state, then it’s not a valid intention
w
The point is that sending click intention while the dialog is closed (if I got the discussion right) shouldn’t be valid 😉
z
I think your idea @Orhan Tozan makes a lot of sense in a
@Compose
world
w
Chiming in again,
@Compose
is an implementation detail over UI. It shouldn’t make any difference for the view model implementation 😄
👍 1
o
I think whether
@Compose
is used or not is a view implementation detail, like @wasyl states also
👍 1
Interesting discussion fellas, thanks
z
btw you probably just want
(<http://Intention.Xyz|Intention.Xyz>) -> Unit
passed in with your
State
to send events
idk if expose a
SendChannel<Intention.Xyz>
is worth it
idk how sending input via
suspend
from the UI would work well because it could be cancelled
which may be good/bad depending on the scenario
w
I’ll definitely be interested at what solution you arrive at, @Orhan Tozan 🙂 One thing I can share from my experience is that a single flow/observable with entire view state exposed from the view model doesn’t work very well if this state represents entire screen. That’s because if you only change one property of the state, the view doesn’t know that, so you either update entire view anyway which is inefficient, or you start implementing diffing solutions. That said it’d probably better with DataBinding, which has built-in diffing in many adapters, but still binding just single variable will be not very efficient
👍 2
(But maybe there are some clever solutions to this problem, I didn’t really researched it)
z
(ehhh I would stay far away from databinding) - but that is a separate discussion
it’s easy enough to just check
if (currentWhatever == newWhatever)
then update UI
I’ve do a single
Flow<EntireViewModelState>
and I love it to be honest
and you can certainly break that up into smaller
Flow<T>
where possible as a UI implementation detail
it also depends on what threading strategy you use for updating your states
o
I think that a ViewModel is just another state in the application, states inside the viewModel could be seen as substate. The reason why you scope the possible Intentions to the ViewModel instead of the whole Application could be applied to why you scope the intentions to the substate of the ViewModel
👌 1
Because with your reasoning @zak.taccardi, you can define all of the possible intention at a global (application level), and make the viewmodel do if
(currentWhatever == newWhatever)
on everything.
z
I actually represent all state updates with (current)
State
+
Intention
= (new)
State
. Whether its a repository or a view model. I kind of see
ViewModel
as just a repository for a specific UI component
o
Current intention can change, hence this would be better: (current)
State
+ (current)
Intention
= (new)
State
+ (new)
Intention
.
z
huh?
Intention
is just an instance that describes how you update a
State
State
+
Intention
=
State
is the reducer function that just gets called repeatedly every time an
Intention
is sent
Intention
instances aren’t stored, they are processed then GC’d
yeah I’ve never understood the global state thing
o
That's the main reason why Reducer patterns get criticqued for ala Redux, and people are starting to use xState for (state machines): https://xstate.js.org/docs/
z
like, whether or not to show a dialog is a
Flow<Boolean>
effectively
o
Copy code
const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: { on: { TOGGLE: 'active' } },
    active: { on: { TOGGLE: 'inactive' } }
  }
});
Yes, not argueing with how state should be shown
👍 1
State can change, why not change possible intent with it?
👍 1
z
I think its a very cool concept btw
union types would be nice for this
o
Anyhow, using coroutines for sending events would be nice so you can do things like this:
Copy code
val clickCounter: Flow<Int> = buttonClicks.map {
    clickCounter + 1
}
That's the concept, I just don't know the best way to do it with coroutines
z
effectively every UI component should be provided a state that it can be in, and the output it can send.
Copy code
val states: Flow<State>
val sendInput: (Input) -> Unit
But if
Input
is a
sealed class
, there can be a pretty complex number of implementations
Copy code
val clickCounter: Flow<Int> = buttonClicks.map {
    clickCounter + 1
}
^^ that can create race conditions I believe?
or is this flow all subscribed to together to form a single coroutine?
ehh if different threads are used I still think race conditions are possible
o
It's more a concept, I'm not sure the best way to do it in the coroutines. It's just that, just like state is a stream, events can also be seen as a stream.
👍 1
I think that's the next step for more declarativeness
Copy code
package com.jakewharton.presentation


interface Presenter<ModelT : Any, EventT : Any> {
  val models: ReceiveChannel<ModelT>
  val events: SendChannel<EventT>
  suspend fun start()
}
Inspired by @jw
d
Why not just use a global static Event Bus?
🤮 1
z
An event bus is a
Channel<T>
. What do you mean?
d
No its not. An event bus can use a channel but it does not have too. Use a vs is a.
The desired functionality here is to communicate click events. An event bus is fantastic for that purpose.
Usage is like this.
z
filterIsInstance()
will drop items from your bus
d
Yes that's the point.
It filters out junk I don't care about.
You can always get all the events if you like.
The post office works under the same principles.
Anyway that is my two cents.
o
message has been deleted
I managed to do it I think. No imperative .offer() needed here
Nice declarative way of seeing how the event and state is connected