Thread
#compose
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Is this the right way to use a ViewModel in compose?
    @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
    Timo Drick

    Timo Drick

    2 years ago
    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.
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    SearchMovie( ... ) {
        launchInComposition(0) {
            Text(text = "hello") 
        }
    }
    Timo Drick

    Timo Drick

    2 years ago
    no and you do not need to add this 0 as a parameter
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Ok, that’s cool. Anyway I’m struggling a lot about how to manage dependencies 🙂
    Timo Drick

    Timo Drick

    2 years ago
    You should change the state variables in you coroutine. You could use the remember or mutableStateOf functions to define states
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    viewModel.result
    is a
    Flow
    , so I guess I don’t need more than that
    Timo Drick

    Timo Drick

    2 years ago
    Yes.
    val state by vm.result.collectAsState()
    👍
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    Timo Drick

    Timo Drick

    2 years ago
    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.
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    🙏 Thank you a lot!!
    Timo Drick

    Timo Drick

    2 years ago
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    Javier

    Javier

    2 years ago
    With an actual/expect you can get a common ViewModel which uses Android real ViewModel in Android target
    but without duplicate the code
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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 😛
    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
    Timo Drick

    Timo Drick

    2 years ago
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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 🙂
    Timo Drick

    Timo Drick

    2 years ago
    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.
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    I was thinking that actually I could just:
    MyComposable(viewModel: (CoroutineScope) -> ViewModel) {
    
      val vm = remember {
        viewModel(CoroutineScope(Job()))
      }
      
      ...
    }
    Timo Drick

    Timo Drick

    2 years ago
    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:
    val scope = rememberCoroutineScope()
    val vm = remember {
        viewModel(scope)
    }
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Wow! Even better!! Thank you!!
    Timo Drick

    Timo Drick

    2 years ago
    Not really used this stuff yet. So i hope it works as expected 😄
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    It should not make a lot of different from my solution above, but better to use native stuff 🙂
    Timo Drick

    Timo Drick

    2 years ago
    It does make a difference. Because the coroutine will be canceled when the composable is disposed. So e.g. downloads will be canceled
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Ouch, you’re right 😄 This totally different way of thinking is confusing me 🙂
    Timo Drick

    Timo Drick

    2 years ago
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Yes yes, but thinking at this states and stuff makes me forgot some parts of my usual development pattern 😄
    Javier

    Javier

    2 years ago
    @Davide Giuseppe Farella take a look to Koin PRs, there is one for compose
    Adam Powell

    Adam Powell

    2 years ago
    Consider whether your ViewModel actually needs a CoroutineScope or if its methods should be suspend functions instead
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    They do need 🙂 I got complex concurrency under the hood and I don’t really wanna use something like
    suspend fun init() = coroutineScope {...
    Timo Drick

    Timo Drick

    2 years ago
    normally it would like this:
    suspend fun init() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {...
    If you want to do s.th. on io
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    But in that case you’d need
    launchInComposition {
      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

    Timo Drick

    2 years ago
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    It is more neat and also in some cases I need to kickstart more than 1 coroutines like
    scope.launch(Io) {
      while(...) {
        buffer.send(...)
      }
    }
    scope.launch(Io) {
      while(...) {
        result.data = buffer.receive()
      }
    }
    Yes, I need to “remember” only the VM 🙂
    Timo Drick

    Timo Drick

    2 years ago
    But yes for your button callbacks it is much easier than
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    Timo Drick

    Timo Drick

    2 years ago
    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
    Adam Powell

    Adam Powell

    2 years ago
    and the associated
    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
    launchInComposition(query) {
      viewmodel.search(query)
    }
    and rely on coroutines cancellation for all of this 🙂
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    @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?
    Adam Powell

    Adam Powell

    2 years ago
    those concepts do not map to onCreate and onResume
    The scope from
    rememberCoroutineScope()
    will be cancelled when the call to
    rememberCoroutineScope
    leaves the composition
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Yes, I was just trying to draw them in some way in my mind 🙂
    Adam Powell

    Adam Powell

    2 years ago
    but your query has a narrower scope than that; there can be many queries over time, the query can change, etc.
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Is there some detailed doc about that? I have so many doubts and I don’t wanna bother you forever 🙂
    Timo Drick

    Timo Drick

    2 years ago
    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.
    Adam Powell

    Adam Powell

    2 years ago
    Timo Drick

    Timo Drick

    2 years ago
    If you want to use data classes outside of the composable than you can use this:
    var searchTerm by mutableStateOf<String?>(null)
    You could use this in your VM and than pass this to your composable functions
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    Timo Drick

    Timo Drick

    2 years ago
    Yes better
    Better to use Flow or LiveData or s.th. similar
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    I deleted part of the message because I misread you comment 😄 Anyway I better look into the code-lab, rather go blind 🙂
    Adam Powell

    Adam Powell

    2 years ago
    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
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Yes, but in that way I gain the advantage to pre-load data, cache it in ram and general more flexibility
    Adam Powell

    Adam Powell

    2 years ago
    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
    which then gives you the benefits of standard cancellation signaling and knowing when operations complete without a side channel
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    Adam Powell

    Adam Powell

    2 years ago
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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
    flow {
      // business stuff here
    }
    Adam Powell

    Adam Powell

    2 years ago
    indeed, that can help put the code on this path
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    Thank you! That’s an interesting consideration 🙂
    Adam Powell

    Adam Powell

    2 years ago
    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
    Timo Drick

    Timo Drick

    2 years ago
    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 😄
    Adam Powell

    Adam Powell

    2 years ago
    but, for example, this code is wrong:
    @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
    the correct (if verbose) way to do the above is:
    val current by remember(flow) { flow.map { mapper(it) } }.collectAsState()
    so it will only recreate the operator chain and resubscribe if the input
    flow
    changes
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    This compose is a surely some sort of black magic 😄
    I will be fun to lose tons of hours of sleep 🙂
    Adam Powell

    Adam Powell

    2 years ago
    it's one of the gotchas for
    .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 more
    Timo Drick

    Timo Drick

    2 years ago
    Really we should try to avoid possible wrong usage so better to deprecated collectAsState
    Adam Powell

    Adam Powell

    2 years ago
    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
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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?
    Adam Powell

    Adam Powell

    2 years ago
    .collectAsState()
    remembers the input flow, the property delegate just acts on the returned
    State<T>
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    @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
    Adam Powell

    Adam Powell

    2 years ago
    it's not unique to
    collectAsState
    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 right
    Timo Drick

    Timo Drick

    2 years ago
    Yea right. I think it will take some time until the developers get familiar with compose 😄
    Adam Powell

    Adam Powell

    2 years ago
    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 🙂
    Davide Giuseppe Farella

    Davide Giuseppe Farella

    2 years ago
    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