https://kotlinlang.org logo
#coroutines
Title
# coroutines
o

Orhan Tozan

03/26/2020, 3:53 PM
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

zak.taccardi

03/26/2020, 3:54 PM
4
😅 2
Copy code
interface HomeViewModel {
  fun send(intention: Intention)
}
Copy code
sealed class Intention {
  object ButtonClick : Intention()
}
o

Orhan Tozan

03/26/2020, 3:56 PM
So you suggest a single generic event functions that needs to be called for all events by the view
Could you elaborate?
z

zak.taccardi

03/26/2020, 3:56 PM
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

Orhan Tozan

03/26/2020, 4:00 PM
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

zak.taccardi

03/26/2020, 4:01 PM
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

Orhan Tozan

03/26/2020, 4:08 PM
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

zak.taccardi

03/26/2020, 4:10 PM
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

Orhan Tozan

03/26/2020, 4:12 PM
because its impossible for a dialogbuttonclickEvent to happen if the dialog is not shown
1
z

zak.taccardi

03/26/2020, 4:12 PM
then how can it be sent in the first place?
w

wasyl

03/26/2020, 4:13 PM
It can’t, but you want this to be immediately clear from the code
o

Orhan Tozan

03/26/2020, 4:14 PM
because the view called
view.send(dialogbuttonclickEvent)
, like you suggested @zak.taccardi initially
z

zak.taccardi

03/26/2020, 4:15 PM
I fail to understand how its an issue
o

Orhan Tozan

03/26/2020, 4:16 PM
the view called
viewmodel.sent(dialogbuttonclickEvent)
while
viewmodel.dialogState == NotShown
z

zak.taccardi

03/26/2020, 4:16 PM
then when you process
dialogButtonClickEvent
, do not update the current
State
under that condition
o

Orhan Tozan

03/26/2020, 4:17 PM
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

zak.taccardi

03/26/2020, 4:18 PM
how are you preventing invalid input then?
o

Orhan Tozan

03/26/2020, 4:19 PM
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

zak.taccardi

03/26/2020, 4:20 PM
Copy code
class Shown : DialogState() {
            val buttonClicks: Flow<..>
How does a
Flow<T>
live on a
State
which is effectively a
data class
?
o

Orhan Tozan

03/26/2020, 4:20 PM
This was a quick proof of concept, maybe state isn't the correct term here
StateMachine perhaps?
z

zak.taccardi

03/26/2020, 4:21 PM
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

Orhan Tozan

03/26/2020, 4:23 PM
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

zak.taccardi

03/26/2020, 4:25 PM
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

Orhan Tozan

03/26/2020, 4:26 PM
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

zak.taccardi

03/26/2020, 4:27 PM
repository exposes
Flow<Data>
though
o

Orhan Tozan

03/26/2020, 4:28 PM
Furthermore, that's a viewmodel implementation detail, I think the view should only be concered what it can do in the current state
z

zak.taccardi

03/26/2020, 4:28 PM
well the following is pretty simple
Copy code
data class State(val showDialog: Boolean)
o

Orhan Tozan

03/26/2020, 4:28 PM
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

zak.taccardi

03/26/2020, 4:30 PM
sure, but its still added complexity
and then you aren’t sending dumb data classes as
State
anymore
o

Orhan Tozan

03/26/2020, 4:31 PM
Added complexitiy is debatable imo
z

zak.taccardi

03/26/2020, 4:31 PM
if it works for you then great. it is certainly an interesting concept
w

wasyl

03/26/2020, 4:31 PM
It’s moved complexity at best, since now view model won’t have to consider invalid intentions
1
o

Orhan Tozan

03/26/2020, 4:32 PM
^that's true
z

zak.taccardi

03/26/2020, 4:32 PM
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

wasyl

03/26/2020, 4:33 PM
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

zak.taccardi

03/26/2020, 4:34 PM
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

wasyl

03/26/2020, 4:35 PM
The point is that sending click intention while the dialog is closed (if I got the discussion right) shouldn’t be valid 😉
z

zak.taccardi

03/26/2020, 4:35 PM
I think your idea @Orhan Tozan makes a lot of sense in a
@Compose
world
w

wasyl

03/26/2020, 4:35 PM
Chiming in again,
@Compose
is an implementation detail over UI. It shouldn’t make any difference for the view model implementation 😄
👍 1
o

Orhan Tozan

03/26/2020, 4:36 PM
I think whether
@Compose
is used or not is a view implementation detail, like @wasyl states also
👍 1
Interesting discussion fellas, thanks
z

zak.taccardi

03/26/2020, 4:37 PM
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

wasyl

03/26/2020, 4:39 PM
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

zak.taccardi

03/26/2020, 4:40 PM
(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

Orhan Tozan

03/26/2020, 4:44 PM
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

zak.taccardi

03/26/2020, 4:46 PM
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

Orhan Tozan

03/26/2020, 4:47 PM
Current intention can change, hence this would be better: (current)
State
+ (current)
Intention
= (new)
State
+ (new)
Intention
.
z

zak.taccardi

03/26/2020, 4:47 PM
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

Orhan Tozan

03/26/2020, 4:50 PM
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

zak.taccardi

03/26/2020, 4:50 PM
like, whether or not to show a dialog is a
Flow<Boolean>
effectively
o

Orhan Tozan

03/26/2020, 4:50 PM
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

zak.taccardi

03/26/2020, 4:52 PM
I think its a very cool concept btw
union types would be nice for this
o

Orhan Tozan

03/26/2020, 4:56 PM
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

zak.taccardi

03/26/2020, 4:58 PM
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

Orhan Tozan

03/26/2020, 5:01 PM
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

dewildte

03/26/2020, 5:56 PM
Why not just use a global static Event Bus?
🤮 1
z

zak.taccardi

03/26/2020, 5:56 PM
An event bus is a
Channel<T>
. What do you mean?
d

dewildte

03/26/2020, 5:57 PM
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

zak.taccardi

03/26/2020, 6:05 PM
filterIsInstance()
will drop items from your bus
d

dewildte

03/26/2020, 6:05 PM
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

Orhan Tozan

03/27/2020, 5:51 PM
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
2 Views