https://kotlinlang.org logo
d

Davide Giuseppe Farella

08/29/2020, 12:35 PM
Is this the right way to use a ViewModel in compose?
Copy code
@Composable
fun SearchMovie(viewModel: SearchViewModel, query: String) {

    val vm = remember(0) { viewModel }     
    vm.search(query)
    val state by vm.result.collectAsState()

    ...
Also is there a way to “assign” a
CoroutineScope
to a given
@Composable
function? My ViewModel does not inherit from the Android’s one, so I’m free to pass a Scope in the constructor and cancel it, but I’m not sure how to deal with a Composable’s life-cycle
t

Timo Drick

08/29/2020, 12:39 PM
You do not need to remember the viewModel when you get it as a parameter in your composable.
for coroutines you can use the launchInComposition function inside your composable. Inside your ViewModel you should use a different coroutine lifecycle.
d

Davide Giuseppe Farella

08/29/2020, 12:42 PM
I was trying to avoid to have duplicated VM during parent’s recomposition, but I guess it will create a new instance every time, anyway. In the parent I got
SearchMovie(viewModel = koin.get(), ...
But I really have no clue how to deal with that, avoiding to have a singleton application 😄
Can I “draw” inside
launchInComposition
? Like
Copy code
SearchMovie( ... ) {
    launchInComposition(0) {
        Text(text = "hello") 
    }
}
t

Timo Drick

08/29/2020, 12:45 PM
no and you do not need to add this 0 as a parameter
d

Davide Giuseppe Farella

08/29/2020, 12:46 PM
Ok, that’s cool. Anyway I’m struggling a lot about how to manage dependencies 🙂
t

Timo Drick

08/29/2020, 12:46 PM
You should change the state variables in you coroutine. You could use the remember or mutableStateOf functions to define states
d

Davide Giuseppe Farella

08/29/2020, 12:47 PM
viewModel.result
is a
Flow
, so I guess I don’t need more than that
t

Timo Drick

08/29/2020, 12:49 PM
Yes.
Copy code
val state by vm.result.collectAsState()
👍
d

Davide Giuseppe Farella

08/29/2020, 12:50 PM
Oh, another question! Will
lunchInComposition
be executed only once across recompositions, right? Because I just though that every time my component recomposes, it will trigger the
vm.search(query)
in the code I shown above
t

Timo Drick

08/29/2020, 12:53 PM
Yes and it will be scoped in the lifecycle of the composable. So when the composable gets removed from the screen the coroutine will be canceled.
d

Davide Giuseppe Farella

08/29/2020, 12:53 PM
🙏 Thank you a lot!!
t

Timo Drick

08/29/2020, 12:59 PM
Btw. this parameters you can provide in remember(v1, v2) and also in launchInComposition(v1, v2, ...) when provided the init block of this functions is executed again when the parameter changes. So e.g. when you want to start a new query when the searchTerm changed you just provide the searchTerm as parameter
But maybe better to do this in your ViewModel code
d

Davide Giuseppe Farella

08/29/2020, 1:01 PM
Yes, that’s a tricky point, I don’t want my VM to be dependant from UI / Compose logics, also because it’s shared across different platforms
j

Javier

08/29/2020, 1:02 PM
With an actual/expect you can get a common ViewModel which uses Android real ViewModel in Android target
but without duplicate the code
d

Davide Giuseppe Farella

08/29/2020, 1:03 PM
Yes, but I don’t really wanna do that, also because it will be scoped to the Main activity, so I can achieve the same with
single { ... }
in my Koin module 😛
👍 1
My pain point here is to • Create the VM only once - way better if I can pass a
CoroutineScope
connected to the Composable life-cycle • Run the search only when the query is changed
In this scenario I can solve with just a
.distinct()
or similar in the VM, but I wanna find a pattern feasible to most of the situations
t

Timo Drick

08/29/2020, 1:10 PM
You could use a object as your VM or you could create the VM inside of your activity. I don't think it is a good idea to bind it to the composeable lifecycle. But it depends on which states you hold in your VM and what lifecycle you expect inside of your VM. Maybe it is ok to just define with remember {}
In practice i hold often scrollerPositions and stuff like that inside of my VM and when the activity gets recreated i want to keep this positions. That will not work when i create the VM inside of a composable
d

Davide Giuseppe Farella

08/29/2020, 1:13 PM
Nono, I don’t want singleton components. It must be alive only as long as I’m in the
SearchMovie
screen. As now I have 3 VM because the app is tiny, but I’ll have quite a lot, since I wanna enforce the Single Responsibility Principle
Yes, but in your scenario you got a VM related to the UI, so it’s totally fine to have a singleton, but when you have various VM that interact with the business logic, it’s not awesome to have a lot of singletons 🙂
👍 1
t

Timo Drick

08/29/2020, 1:19 PM
i am also currently trying to find a proper architecture for my apps when using compose. So not sure yet what is a good way of modeling business logic and views.
👍 1
d

Davide Giuseppe Farella

08/29/2020, 1:25 PM
I was thinking that actually I could just:
Copy code
MyComposable(viewModel: (CoroutineScope) -> ViewModel) {

  val vm = remember {
    viewModel(CoroutineScope(Job()))
  }
  
  ...
}
t

Timo Drick

08/29/2020, 1:28 PM
do you really need a coroutinescope inside of your VM? But maybe it would be the best to use the scope from the compoasable than. I have to find the right function just a second
Maybe something like that:
Copy code
val scope = rememberCoroutineScope()
val vm = remember {
    viewModel(scope)
}
d

Davide Giuseppe Farella

08/29/2020, 1:30 PM
Wow! Even better!! Thank you!!
t

Timo Drick

08/29/2020, 1:32 PM
Not really used this stuff yet. So i hope it works as expected 😄
d

Davide Giuseppe Farella

08/29/2020, 1:32 PM
It should not make a lot of different from my solution above, but better to use native stuff 🙂
t

Timo Drick

08/29/2020, 1:35 PM
It does make a difference. Because the coroutine will be canceled when the composable is disposed. So e.g. downloads will be canceled
d

Davide Giuseppe Farella

08/29/2020, 1:36 PM
Ouch, you’re right 😄 This totally different way of thinking is confusing me 🙂
t

Timo Drick

08/29/2020, 1:37 PM
It is the same when you using coroutine scope of activity or fragment than this scope is bound to the lifecycle of activity or fragment. And this rememberCoroutineScope() will provide the same for Compose
d

Davide Giuseppe Farella

08/29/2020, 1:39 PM
Yes yes, but thinking at this states and stuff makes me forgot some parts of my usual development pattern 😄
j

Javier

08/29/2020, 1:52 PM
@Davide Giuseppe Farella take a look to Koin PRs, there is one for compose
🙏 1
😮 1
a

Adam Powell

08/29/2020, 2:02 PM
Consider whether your ViewModel actually needs a CoroutineScope or if its methods should be suspend functions instead
d

Davide Giuseppe Farella

08/29/2020, 2:06 PM
They do need 🙂 I got complex concurrency under the hood and I don’t really wanna use something like
Copy code
suspend fun init() = coroutineScope {...
🤔 1
t

Timo Drick

08/29/2020, 2:08 PM
normally it would like this:
suspend fun init() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {...
If you want to do s.th. on io
d

Davide Giuseppe Farella

08/29/2020, 2:22 PM
But in that case you’d need
Copy code
launchInComposition {
  vm.initi()
}
vm.result.collectAsState()
While I skip the first 3 lines and use the regular
init
block as
Copy code
class MyVM(scope: CoroutineScope) {
  
  init {
    scope.launch(Io) {
      ...
    }
  }

}
👍 1
t

Timo Drick

08/29/2020, 2:24 PM
But keep in mind that in compose the composable functions will be executed again when a recomposition happens. So you need to use remember anyways
d

Davide Giuseppe Farella

08/29/2020, 2:25 PM
It is more neat and also in some cases I need to kickstart more than 1 coroutines like
Copy code
scope.launch(Io) {
  while(...) {
    buffer.send(...)
  }
}
scope.launch(Io) {
  while(...) {
    result.data = buffer.receive()
  }
}
Yes, I need to “remember” only the VM 🙂
t

Timo Drick

08/29/2020, 2:25 PM
But yes for your button callbacks it is much easier than
d

Davide Giuseppe Farella

08/29/2020, 3:50 PM
Should this be fine?
remember
Remember the value returned by [calculation] if [v1] and [v2] are equal to the previous composition, otherwise produce and remember a new value by calling [calculation].
In my understanding the lambda is called only when
v1
( in this case ) is changed and won’t keep track of previous values, so • v1 = “a” -> called • v1 = “ab” -> called • v1 = “ab” -> NOT called • v1 = “a” -> called
👍 1
t

Timo Drick

08/29/2020, 3:57 PM
onCommit(query) {
viewModel.search(query)
}
Would be the correct code i think. At the end it is more or less the same but i think it would be much more clean
a

Adam Powell

08/29/2020, 4:01 PM
and the associated
Copy code
onCommit(query) {
  val cancellationToken = viewModel.search(query)
  onDispose {
    cancellationToken.cancel()
  }
}
so that a query doesn't keep running when it's no longer needed. Hence why this all gets cleaner if you use
suspend
functions on the viewmodel for this sort of thing, because then you can do
Copy code
launchInComposition(query) {
  viewmodel.search(query)
}
and rely on coroutines cancellation for all of this 🙂
d

Davide Giuseppe Farella

08/29/2020, 4:04 PM
@Timo Drick Ok, now I have I strong doubt: Let’s call
composition
as
onCreate
and
re-composition
as
onResume
, right? When a state changes we have a
recomposition
, but what if the param received in the function changes? Do we have a fresh
composition
or a
recomposition
? If it’s a
composition
, would it be “fixed” if I pass a
State<String>
instead? @Adam Powell would not the scope received by
rememberCoroutineScope()
be automatically cancelled when disposed?
a

Adam Powell

08/29/2020, 4:05 PM
those concepts do not map to onCreate and onResume
The scope from
rememberCoroutineScope()
will be cancelled when the call to
rememberCoroutineScope
leaves the composition
d

Davide Giuseppe Farella

08/29/2020, 4:06 PM
Yes, I was just trying to draw them in some way in my mind 🙂
a

Adam Powell

08/29/2020, 4:06 PM
but your query has a narrower scope than that; there can be many queries over time, the query can change, etc.
d

Davide Giuseppe Farella

08/29/2020, 4:07 PM
Is there some detailed doc about that? I have so many doubts and I don’t wanna bother you forever 🙂
t

Timo Drick

08/29/2020, 4:07 PM
recomposition is also not so easy to get right, When you use remember and state for variables you than forward to your composable functions than it works as expected.
👍 1
a

Adam Powell

08/29/2020, 4:08 PM
t

Timo Drick

08/29/2020, 4:08 PM
If you want to use data classes outside of the composable than you can use this:
Copy code
var searchTerm by mutableStateOf<String?>(null)
🙏 1
You could use this in your VM and than pass this to your composable functions
d

Davide Giuseppe Farella

08/29/2020, 4:10 PM
Thank you both. I have a strong opinion against remove the scope from my ViewModel, as I want it do be the orchestrator of the concurrency, rather than the UI layer, but I need to clear my mind a lot about that composition stuff 😄
VM is a shared component across the platform
t

Timo Drick

08/29/2020, 4:11 PM
Yes better
Better to use Flow or LiveData or s.th. similar
a

Adam Powell

08/29/2020, 4:12 PM
the reason I'm advocating for removing the scope from the viewmodel is because it's always simpler to have objects that only act when things call them than objects that act continually on their own in the background. When you give an object a
CoroutineScope
to launch things into under the hood, it ends up in the second category
d

Davide Giuseppe Farella

08/29/2020, 4:12 PM
I deleted part of the message because I misread you comment 😄 Anyway I better look into the code-lab, rather go blind 🙂
a

Adam Powell

08/29/2020, 4:13 PM
generally if you have operations that you want to cache or deduplicate, like queries in flight, those work better at a sort of repository layer that outlives individual viewmodels, because it's about your app's backing data, not the UI
so it's often common for a repository object to have a CoroutineScope
d

Davide Giuseppe Farella

08/29/2020, 4:13 PM
Yes, but in that way I gain the advantage to pre-load data, cache it in ram and general more flexibility
a

Adam Powell

08/29/2020, 4:14 PM
in a way that you don't get by moving that to the repository layer and keeping the viewmodel simpler?
most of this distills down to structured concurrency principles rather than anything specific to compose; concurrency should be explicit and opt-in, avoid fire and forget concurrency, which leads to fewer under the hood
scope.launch
calls and more
suspend
functions
1
which then gives you the benefits of standard cancellation signaling and knowing when operations complete without a side channel
d

Davide Giuseppe Farella

08/29/2020, 4:18 PM
I see your point, but I have to run some business logic above the repository, for example • fetch some statistics from a repo ( Data layer ) • model them in order to generate some suggestions parameters ( Domain layer ) • fetch some models from another repo, matching the params ( Data layer in another module ) Also I got a
GetSuggestionsViewModel
, it pre-load a buffer of suggestions and display one by one. I got a
GetSuggestedMovies
use case that return a collection of suggestion, then it’s UI business whether to show a grid or one by one. In my case I show one by one, so the user can swipe RTL/LTR for like/dislike
a

Adam Powell

08/29/2020, 4:19 PM
sure, none of that is incompatible with the above
it's perfectly fine for the viewmodel to act as a cache and policy layer in terms of how it interacts with the repository
d

Davide Giuseppe Farella

08/29/2020, 4:21 PM
But the data layer should be a detail and independant from the business rules, so the only layer in the middle is the domain, and it would not be a great idea to have a scope for use case, as I will end up with a tons of scopes 🙂
On the other side I could deal with it by using some
Flow
and encapsulate my logic in its constructor
Copy code
flow {
  // business stuff here
}
a

Adam Powell

08/29/2020, 4:23 PM
indeed, that can help put the code on this path
👍 1
d

Davide Giuseppe Farella

08/29/2020, 4:23 PM
Thank you! That’s an interesting consideration 🙂
a

Adam Powell

08/29/2020, 4:24 PM
one thing to be careful about when combining flows with compose: creating operator chains inside of a
@Composable
function can get a bit tricky. If you're assembling them outside of composition these considerations don't come into play
👍 1
t

Timo Drick

08/29/2020, 4:24 PM
I just switched to flows for my data loading to support paging and endless scrolling lists. And yes indeed than i do not need a suspend function at the repository component. It was a little bit tricky to combine flows with endless scrolling lists but it works now 😄
a

Adam Powell

08/29/2020, 4:25 PM
but, for example, this code is wrong:
Copy code
@Composble
fun MyComposable(flow: Flow<Foo>) {
  val current by flow.map { mapper(it) }.collectAsState()
what makes it wrong is that it will create a new
.map {}
operator chain every time it recomposes, which means the input
Flow
to
.collectAsState
is different every time, so it has to unsubscribe from the old and resubscribe to the new on every recomposition
🤯 2
the correct (if verbose) way to do the above is:
Copy code
val current by remember(flow) { flow.map { mapper(it) } }.collectAsState()
🤔 1
so it will only recreate the operator chain and resubscribe if the input
flow
changes
d

Davide Giuseppe Farella

08/29/2020, 4:29 PM
This compose is a surely some sort of black magic 😄
I will be fun to lose tons of hours of sleep 🙂
😴 1
a

Adam Powell

08/29/2020, 4:30 PM
it's one of the gotchas for
.collectAsState()
, anyway. I'm playing around with another api that would be more like:
Copy code
val current by produceState(initialValue, flow) {
  flow.map { mapper(it) }
    .collect { setValue(it) }
}
which sort of invites correct usage a bit more
👍 1
t

Timo Drick

08/29/2020, 4:33 PM
Really we should try to avoid possible wrong usage so better to deprecated collectAsState
a

Adam Powell

08/29/2020, 4:35 PM
eh, tradeoffs. 🙂 if you're not assembling operator chains it's quite a bit nicer, but we'll keep an eye on it. It might also be an opportunity for other static analysis guiding towards picking the right tool
d

Davide Giuseppe Farella

08/29/2020, 4:35 PM
Well, I guess
collectAsState
is an awesome API, the point here is that the delegation hide some dangerous mechanism, but on the other side enables some easy way to work with stuff
@Adam Powell is that happening because the delegate keep memory of the “original” field?
a

Adam Powell

08/29/2020, 4:37 PM
.collectAsState()
remembers the input flow, the property delegate just acts on the returned
State<T>
d

Davide Giuseppe Farella

08/29/2020, 4:37 PM
@Timo Drick Look at Coroutines their self, the enable so freaking easy concurrency, but it’s very easy to mess with them 🙂
Ok, so I guess it’s not a matter of delegation, but of Compose itself
a

Adam Powell

08/29/2020, 4:37 PM
it's not unique to
collectAsState
either. If you move a flow assembly to a caller, for example, it's just as wrong:
Copy code
MyComposable(flow.map { mapper(it) })
will exhibit the same pathological problem even if
MyComposable
does everything right
t

Timo Drick

08/29/2020, 4:38 PM
Yea right. I think it will take some time until the developers get familiar with compose 😄
a

Adam Powell

08/29/2020, 4:39 PM
yeah. Learning to think declaratively is a big mindset shift
it pays off big though, just like structured concurrency
and in a way it's easier to teach to brand new developers than it is to teach to experienced developers 🙂
😄 1
d

Davide Giuseppe Farella

08/29/2020, 4:42 PM
Well, it depends, I remember I lost few hours 2 years back, when I fought with
override val x = y
against
override val x get() = y
It’s a very easy thing and I would spot it fairly easily now
3 Views