Davide Giuseppe Farella
08/29/2020, 12:35 PM@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-cycleTimo Drick
08/29/2020, 12:39 PMDavide Giuseppe Farella
08/29/2020, 12:42 PMSearchMovie(viewModel = koin.get(), ...
But I really have no clue how to deal with that, avoiding to have a singleton application 😄launchInComposition
?
Like
SearchMovie( ... ) {
launchInComposition(0) {
Text(text = "hello")
}
}
Timo Drick
08/29/2020, 12:45 PMDavide Giuseppe Farella
08/29/2020, 12:46 PMTimo Drick
08/29/2020, 12:46 PMDavide Giuseppe Farella
08/29/2020, 12:47 PMviewModel.result
is a Flow
, so I guess I don’t need more than thatTimo Drick
08/29/2020, 12:49 PMval state by vm.result.collectAsState()
👍Davide Giuseppe Farella
08/29/2020, 12:50 PMlunchInComposition
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 aboveTimo Drick
08/29/2020, 12:53 PMDavide Giuseppe Farella
08/29/2020, 12:53 PMTimo Drick
08/29/2020, 12:59 PMDavide Giuseppe Farella
08/29/2020, 1:01 PMJavier
08/29/2020, 1:02 PMDavide Giuseppe Farella
08/29/2020, 1:03 PMsingle { ... }
in my Koin module 😛CoroutineScope
connected to the Composable life-cycle
• Run the search only when the query is changed.distinct()
or similar in the VM, but I wanna find a pattern feasible to most of the situationsTimo Drick
08/29/2020, 1:10 PMDavide Giuseppe Farella
08/29/2020, 1:13 PMSearchMovie
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 PrincipleTimo Drick
08/29/2020, 1:19 PMDavide Giuseppe Farella
08/29/2020, 1:25 PMMyComposable(viewModel: (CoroutineScope) -> ViewModel) {
val vm = remember {
viewModel(CoroutineScope(Job()))
}
...
}
Timo Drick
08/29/2020, 1:28 PMval scope = rememberCoroutineScope()
val vm = remember {
viewModel(scope)
}
Davide Giuseppe Farella
08/29/2020, 1:30 PMTimo Drick
08/29/2020, 1:32 PMDavide Giuseppe Farella
08/29/2020, 1:32 PMTimo Drick
08/29/2020, 1:35 PMDavide Giuseppe Farella
08/29/2020, 1:36 PMTimo Drick
08/29/2020, 1:37 PMDavide Giuseppe Farella
08/29/2020, 1:39 PMJavier
08/29/2020, 1:52 PMAdam Powell
08/29/2020, 2:02 PMDavide Giuseppe Farella
08/29/2020, 2:06 PMsuspend fun init() = coroutineScope {...
Timo Drick
08/29/2020, 2:08 PMsuspend fun init() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {...
If you want to do s.th. on ioDavide Giuseppe Farella
08/29/2020, 2:22 PMlaunchInComposition {
vm.initi()
}
vm.result.collectAsState()
While I skip the first 3 lines and use the regular init
block as
class MyVM(scope: CoroutineScope) {
init {
scope.launch(Io) {
...
}
}
}
Timo Drick
08/29/2020, 2:24 PMDavide Giuseppe Farella
08/29/2020, 2:25 PMscope.launch(Io) {
while(...) {
buffer.send(...)
}
}
scope.launch(Io) {
while(...) {
result.data = buffer.receive()
}
}
Timo Drick
08/29/2020, 2:25 PMDavide Giuseppe Farella
08/29/2020, 3:50 PMremember
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” -> calledTimo Drick
08/29/2020, 3:57 PMonCommit(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 cleanAdam Powell
08/29/2020, 4:01 PMonCommit(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
launchInComposition(query) {
viewmodel.search(query)
}
and rely on coroutines cancellation for all of this 🙂Davide Giuseppe Farella
08/29/2020, 4:04 PMcomposition
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?Adam Powell
08/29/2020, 4:05 PMrememberCoroutineScope()
will be cancelled when the call to rememberCoroutineScope
leaves the compositionDavide Giuseppe Farella
08/29/2020, 4:06 PMAdam Powell
08/29/2020, 4:06 PMDavide Giuseppe Farella
08/29/2020, 4:07 PMTimo Drick
08/29/2020, 4:07 PMAdam Powell
08/29/2020, 4:08 PMTimo Drick
08/29/2020, 4:08 PMvar searchTerm by mutableStateOf<String?>(null)
Davide Giuseppe Farella
08/29/2020, 4:10 PMTimo Drick
08/29/2020, 4:11 PMAdam Powell
08/29/2020, 4:12 PMCoroutineScope
to launch things into under the hood, it ends up in the second categoryDavide Giuseppe Farella
08/29/2020, 4:12 PMAdam Powell
08/29/2020, 4:13 PMDavide Giuseppe Farella
08/29/2020, 4:13 PMAdam Powell
08/29/2020, 4:14 PMscope.launch
calls and more suspend
functionsDavide Giuseppe Farella
08/29/2020, 4:18 PMGetSuggestionsViewModel
, 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/dislikeAdam Powell
08/29/2020, 4:19 PMDavide Giuseppe Farella
08/29/2020, 4:21 PMFlow
and encapsulate my logic in its constructor
flow {
// business stuff here
}
Adam Powell
08/29/2020, 4:23 PMDavide Giuseppe Farella
08/29/2020, 4:23 PMAdam Powell
08/29/2020, 4:24 PM@Composable
function can get a bit tricky. If you're assembling them outside of composition these considerations don't come into playTimo Drick
08/29/2020, 4:24 PMAdam Powell
08/29/2020, 4:25 PM@Composble
fun MyComposable(flow: Flow<Foo>) {
val current by flow.map { mapper(it) }.collectAsState()
.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 recompositionval current by remember(flow) { flow.map { mapper(it) } }.collectAsState()
flow
changesDavide Giuseppe Farella
08/29/2020, 4:29 PMAdam Powell
08/29/2020, 4:30 PM.collectAsState()
, anyway. I'm playing around with another api that would be more like:
val current by produceState(initialValue, flow) {
flow.map { mapper(it) }
.collect { setValue(it) }
}
which sort of invites correct usage a bit moreTimo Drick
08/29/2020, 4:33 PMAdam Powell
08/29/2020, 4:35 PMDavide Giuseppe Farella
08/29/2020, 4:35 PMcollectAsState
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 stuffAdam Powell
08/29/2020, 4:37 PM.collectAsState()
remembers the input flow, the property delegate just acts on the returned State<T>
Davide Giuseppe Farella
08/29/2020, 4:37 PMAdam Powell
08/29/2020, 4:37 PMcollectAsState
either. If you move a flow assembly to a caller, for example, it's just as wrong:
MyComposable(flow.map { mapper(it) })
will exhibit the same pathological problem even if MyComposable
does everything rightTimo Drick
08/29/2020, 4:38 PMAdam Powell
08/29/2020, 4:39 PMDavide Giuseppe Farella
08/29/2020, 4:42 PMoverride val x = y
against override val x get() = y
It’s a very easy thing and I would spot it fairly easily now