Orhan Tozan
03/26/2020, 3:53 PM// 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
}
zak.taccardi
03/26/2020, 3:54 PMzak.taccardi
03/26/2020, 3:54 PMinterface HomeViewModel {
fun send(intention: Intention)
}
sealed class Intention {
object ButtonClick : Intention()
}
Orhan Tozan
03/26/2020, 3:56 PMOrhan Tozan
03/26/2020, 3:56 PMzak.taccardi
03/26/2020, 3:56 PMSendChannel<T>
too, except you don’t want to expose the ability to close that channelzak.taccardi
03/26/2020, 3:57 PMsealed class
representing all forms of input so you can use an exhaustive when
to process itzak.taccardi
03/26/2020, 3:57 PMzak.taccardi
03/26/2020, 3:57 PMzak.taccardi
03/26/2020, 3:58 PMHomeViewModel
just needs to process those input events in order and to build a Flow<State>
for your UI to consumezak.taccardi
03/26/2020, 3:59 PMactor
coroutine to process input events and build state and it is awesome
https://kotlinlang.org/docs/reference/coroutines/shared-mutable-state-and-concurrency.html#actorsOrhan Tozan
03/26/2020, 4:00 PMzak.taccardi
03/26/2020, 4:01 PMViewModel
can expose any number, but I generally just expose 1 flow of statezak.taccardi
03/26/2020, 4:03 PMRepository
classes and reduce them into a single state for your UIzak.taccardi
03/26/2020, 4:06 PMViewModel
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
Orhan Tozan
03/26/2020, 4:08 PMsend
method, the view is allowed to send a dialogbuttonclickEvent
when the dialog is not shown. However, I thought of the following:
interface ViewModel {
val dialogState: Flow<DialogState>
sealed class DialogState {
object NotShown : DialogState()
class Shown : DialogState() {
val buttonClicks: Flow<..>
// or
val buttonClicks: SendChannel<..>
}
}
}
Orhan Tozan
03/26/2020, 4:09 PMzak.taccardi
03/26/2020, 4:10 PMState
is always a data class for mezak.taccardi
03/26/2020, 4:10 PMzak.taccardi
03/26/2020, 4:11 PMsealed class
can work with simple stateszak.taccardi
03/26/2020, 4:11 PMdialogbuttonclickEvent
matter if the view isn’t shown?zak.taccardi
03/26/2020, 4:12 PMViewModel
should process it. and it could decide to drop itOrhan Tozan
03/26/2020, 4:12 PMzak.taccardi
03/26/2020, 4:12 PMwasyl
03/26/2020, 4:13 PMOrhan Tozan
03/26/2020, 4:14 PMview.send(dialogbuttonclickEvent)
, like you suggested @zak.taccardi initiallyzak.taccardi
03/26/2020, 4:15 PMOrhan Tozan
03/26/2020, 4:16 PMviewmodel.sent(dialogbuttonclickEvent)
while viewmodel.dialogState == NotShown
zak.taccardi
03/26/2020, 4:16 PMdialogButtonClickEvent
, do not update the current State
under that conditionOrhan Tozan
03/26/2020, 4:17 PMzak.taccardi
03/26/2020, 4:18 PMOrhan Tozan
03/26/2020, 4:19 PMOrhan Tozan
03/26/2020, 4:19 PMinterface 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 Shownzak.taccardi
03/26/2020, 4:20 PMclass Shown : DialogState() {
val buttonClicks: Flow<..>
How does a Flow<T>
live on a State
which is effectively a data class
?Orhan Tozan
03/26/2020, 4:20 PMOrhan Tozan
03/26/2020, 4:20 PMzak.taccardi
03/26/2020, 4:21 PMzak.taccardi
03/26/2020, 4:22 PMState
+ Intention
= (new) State
. And the View
or the ViewModel
itself can send Intention
objects to update that State
Orhan Tozan
03/26/2020, 4:23 PMOrhan Tozan
03/26/2020, 4:24 PMOrhan Tozan
03/26/2020, 4:25 PMzak.taccardi
03/26/2020, 4:25 PMzak.taccardi
03/26/2020, 4:25 PMzak.taccardi
03/26/2020, 4:26 PMOrhan Tozan
03/26/2020, 4:26 PMOrhan Tozan
03/26/2020, 4:27 PMzak.taccardi
03/26/2020, 4:27 PMFlow<Data>
thoughOrhan Tozan
03/26/2020, 4:28 PMzak.taccardi
03/26/2020, 4:28 PMdata class State(val showDialog: Boolean)
Orhan Tozan
03/26/2020, 4:28 PMOrhan Tozan
03/26/2020, 4:29 PMzak.taccardi
03/26/2020, 4:30 PMzak.taccardi
03/26/2020, 4:30 PMState
anymoreOrhan Tozan
03/26/2020, 4:31 PMzak.taccardi
03/26/2020, 4:31 PMwasyl
03/26/2020, 4:31 PMOrhan Tozan
03/26/2020, 4:32 PMzak.taccardi
03/26/2020, 4:32 PMViewModel
are valid. you mean the View
will only be able to consider intentions appropriate for that State
zak.taccardi
03/26/2020, 4:32 PMwasyl
03/26/2020, 4:33 PMall intentions for the ViewModel are validI’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
zak.taccardi
03/26/2020, 4:34 PMIntention
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 intentionwasyl
03/26/2020, 4:35 PMzak.taccardi
03/26/2020, 4:35 PM@Compose
worldwasyl
03/26/2020, 4:35 PM@Compose
is an implementation detail over UI. It shouldn’t make any difference for the view model implementation 😄Orhan Tozan
03/26/2020, 4:36 PM@Compose
is used or not is a view implementation detail, like @wasyl states alsoOrhan Tozan
03/26/2020, 4:36 PMzak.taccardi
03/26/2020, 4:37 PM(<http://Intention.Xyz|Intention.Xyz>) -> Unit
passed in with your State
to send eventszak.taccardi
03/26/2020, 4:37 PMSendChannel<Intention.Xyz>
is worth itzak.taccardi
03/26/2020, 4:38 PMsuspend
from the UI would work well because it could be cancelledzak.taccardi
03/26/2020, 4:38 PMwasyl
03/26/2020, 4:39 PMwasyl
03/26/2020, 4:40 PMzak.taccardi
03/26/2020, 4:40 PMzak.taccardi
03/26/2020, 4:41 PMif (currentWhatever == newWhatever)
then update UIzak.taccardi
03/26/2020, 4:42 PMFlow<EntireViewModelState>
and I love it to be honestzak.taccardi
03/26/2020, 4:43 PMFlow<T>
where possible as a UI implementation detailzak.taccardi
03/26/2020, 4:43 PMOrhan Tozan
03/26/2020, 4:44 PMOrhan Tozan
03/26/2020, 4:46 PM(currentWhatever == newWhatever)
on everything.zak.taccardi
03/26/2020, 4:46 PMState
+ Intention
= (new) State
. Whether its a repository or a view model. I kind of see ViewModel
as just a repository for a specific UI componentOrhan Tozan
03/26/2020, 4:47 PMState
+ (current) Intention
= (new) State
+ (new) Intention
.zak.taccardi
03/26/2020, 4:47 PMzak.taccardi
03/26/2020, 4:48 PMIntention
is just an instance that describes how you update a State
zak.taccardi
03/26/2020, 4:48 PMState
+ Intention
= State
is the reducer function that just gets called repeatedly every time an Intention
is sentzak.taccardi
03/26/2020, 4:49 PMIntention
instances aren’t stored, they are processed then GC’dzak.taccardi
03/26/2020, 4:49 PMOrhan Tozan
03/26/2020, 4:50 PMzak.taccardi
03/26/2020, 4:50 PMFlow<Boolean>
effectivelyOrhan Tozan
03/26/2020, 4:50 PMconst toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
Orhan Tozan
03/26/2020, 4:50 PMOrhan Tozan
03/26/2020, 4:51 PMzak.taccardi
03/26/2020, 4:52 PMzak.taccardi
03/26/2020, 4:55 PMOrhan Tozan
03/26/2020, 4:56 PMval clickCounter: Flow<Int> = buttonClicks.map {
clickCounter + 1
}
That's the concept, I just don't know the best way to do it with coroutineszak.taccardi
03/26/2020, 4:58 PMval states: Flow<State>
val sendInput: (Input) -> Unit
But if Input
is a sealed class
, there can be a pretty complex number of implementationszak.taccardi
03/26/2020, 4:59 PMval clickCounter: Flow<Int> = buttonClicks.map {
clickCounter + 1
}
zak.taccardi
03/26/2020, 4:59 PMzak.taccardi
03/26/2020, 4:59 PMzak.taccardi
03/26/2020, 5:00 PMOrhan Tozan
03/26/2020, 5:01 PMOrhan Tozan
03/26/2020, 5:01 PMOrhan Tozan
03/26/2020, 5:02 PMpackage com.jakewharton.presentation
interface Presenter<ModelT : Any, EventT : Any> {
val models: ReceiveChannel<ModelT>
val events: SendChannel<EventT>
suspend fun start()
}
Orhan Tozan
03/26/2020, 5:02 PMdewildte
03/26/2020, 5:56 PMzak.taccardi
03/26/2020, 5:56 PMChannel<T>
. What do you mean?dewildte
03/26/2020, 5:57 PMdewildte
03/26/2020, 5:59 PMdewildte
03/26/2020, 6:00 PMdewildte
03/26/2020, 6:03 PMzak.taccardi
03/26/2020, 6:05 PMfilterIsInstance()
will drop items from your busdewildte
03/26/2020, 6:05 PMdewildte
03/26/2020, 6:05 PMdewildte
03/26/2020, 6:06 PMdewildte
03/26/2020, 6:07 PMdewildte
03/26/2020, 6:07 PMOrhan Tozan
03/27/2020, 5:51 PMOrhan Tozan
03/27/2020, 5:52 PMOrhan Tozan
03/27/2020, 5:52 PM