flosch

    flosch

    2 years ago
    Hey, I need a CoroutineScope that lives as long as the current composition. is this valid?
    @Composable
    internal fun composeCoroutineScope(
        coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
    ): CoroutineScope {
        val scope = remember { CoroutineScope(coroutineContext) }
        onDispose { scope.cancel() }
        return scope
    }
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    2 years ago
    Looks reasonable to me. I think there’s also a helper coming in dev12 for doing pretty much exactly this. Edit: See Adam’s message.
    There is a
    CoroutineContextAmbient
    that you might want to incorporate, although I’m not sure if there’s any advantage to doing so since I don’t know what it would provide other than a parent job, which you don’t care about here anyway since you’re not bubbling errors up and cancelling it yourself.
    Adam Powell

    Adam Powell

    2 years ago
    Just getting a coroutine scope from a composable is kind of dangerous as the only safe way to launch something into it is from an onCommit or other event that can only occur after the composition successfully commits
    You generally want something that looks like the API just merged above, where you launch with onCommit semantics and cancel that job if the launch call leaves the composition
    Then use structured concurrency constructs from within that scope
    Oops, didn't mean to send that to channel
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    2 years ago
    Ah, that’s what I was thinking of. Just out of curiosity, what could go wrong if you launch before the composition commits?
    raulraja

    raulraja

    2 years ago
    @Adam Powell would @RestricSuspension and making Composable
    suspend
    solve those issues?
    Adam Powell

    Adam Powell

    2 years ago
    in what way? Composable functions don't suspend and resume, they fully restart.
    (Or skip, as the case may be)
    @Zach Klippenstein (he/him) [MOD] Composition is transactional; it can happen speculatively and it can fail. By performing side effects only in onCommit-like constructs you can avoid having to detect these situations and attempt to roll back.
    Today there aren't many situations where this happens, but we're in the process of setting things up so that composition can occur off the main thread. We'll be able to allow large recompositions that spend a lot of time in app code to span multiple frames, while the existing app UI can still continue animating by whatever mechanism, etc.
    This will also allow all sorts of background prefetching of content in recyclerview-like scenarios
    raulraja

    raulraja

    2 years ago
    RestrictSuspension restricts effects to a receiver which means you can selectively say which methods of a Composable would take state changing or potentially async code and which parts don’t. the std library provides the intrinsics to run suspension without dependencies in KotlinX Coroutines which would allow you run Composable functions with any semantics you want. You control how the continuation proceeds and what suspension points do. Also unlocks the feature you mention since everything mentioned above in terms of optimisations, executions or caching is just a particular runtime for a delimited continuation.
    suspend has nothing to do with async, just an op that can fail or succeed and needs to be resumed blocking or async
    flosch

    flosch

    2 years ago
    The
    launchInCompose
    looks cool 👍 but it’s not really what I need. Let’s say I have a presenter that is bound to the life of a
    CoroutineScope
    (and also created via
    CoroutineScope.createPresenter
    extension function) and I want to use that presenter for a Composable. Could I use the
    effectCoroutineScope
    ?
    raulraja

    raulraja

    2 years ago
    otherwise in Kotlin how are you gonna be able to control effects that can go async and scape the main thread. At the moment Kotlin allows those anywhere unless you provide your own restricted suspended implementation
    the kind of async power and optimizations over binding of views you mentioned as new features can be implemented because there is in the std lib startCoroutineUniterceptedOrReturn and COROUTINE_SUSPENDED
    and those interoperate with all libs that have a suspend runtime like KotlinX or Arrow Fx
    same reason why the sequence builder uses restricted suspension to yield without actually doing anything async
    Adam Powell

    Adam Powell

    2 years ago
    yep, I'm aware, I spent last weekend using exactly those primitives to do gesture detection with compose 🙂 it's quite nice
    raulraja

    raulraja

    2 years ago
    so the question is why Composable functions are not suspended? what is the reason to make them regular functions?
    Adam Powell

    Adam Powell

    2 years ago
    they're not regular functions either 🙂
    suspended continuations can only be resumed once, and we need to be able to restart from the beginning multiple times
    raulraja

    raulraja

    2 years ago
    suspended continuation can be resumed multiple times
    Adam Powell

    Adam Powell

    2 years ago
    so the resulting structure ends up different
    raulraja

    raulraja

    2 years ago
    just need to reset the stack labels if your structure is supposed to be immutable
    Arrow Fx does this for all types, multi shot continuations for non deterministic monads
    just need to add a new method in ContinuationImpl to reset the stack after resumption
    easier than replicating suspend, currently requires a reflection hack
    they can be rewinded and restrict suspension ensures you can perform the optiizations on each suspension point
    like autocancelllation of jobs or fibers etc
    or auto stack-safety for long tree composition by trampolining in suspension points
    Adam Powell

    Adam Powell

    2 years ago
    We evaluated it and it wasn't quite what we were looking for, and we spent some time with Roman and his team discussing the parallels between
    suspend
    and
    @Composable
    . There are certainly similarities, but ultimately we decided that they were different enough to keep separate
    which isn't to take away from how awesome
    suspend
    is 🙂
    I find that the two complement each other extremely well and form a sort of feedback loop between each other
    @flosch Can you post an example of the code that uses such a presenter from compose?
    flosch

    flosch

    2 years ago
    Sure
    @Composable
    private fun AppScreen() {
        MaterialTheme(colors = AppColors.currentColorPalette) {
            val controller = /*coroutineScope*/.createCounterController()
            val controllerState by controller.state.collectAsState()
            CounterScreen(counterState = controllerState, action = controller::dispatch)
        }
    }
    Adam Powell

    Adam Powell

    2 years ago
    How about the presenter code itself? I tend to prefer keeping objects from owning CoroutineScopes and instead preferring exposing
    suspend
    functions as their API surface, which ensures the caller of those functions is what brings a relevant scope along
    this often leads to having
    foo.runBar()
    suspend functions that get launched from elsewhere to act as consumers or workers, which can seem a bit cumbersome until you realize that you can try/catch around those and have some very nice error reporting and recovery, rather than dealing with SupervisorJobs and CoroutineExceptionHandlers that have to put things together after the fact
    flosch

    flosch

    2 years ago
    Well the Controller approach is somewhat of a MVI situation where the Controller lives in by itself (as long as the CoroutineScope) and collectors dispatch actions and receive a state as Flow. Now this was not particularly made for Compose and I am trying to get this to run smoothly with Compose 🙂
    Adam Powell

    Adam Powell

    2 years ago
    Strictly speaking what you're doing will mostly work, though you're over-remembering the scope. You might want to pass the
    coroutineContext
    supplied as a parameter to
    remember
    as a key param, so that if the caller changes the context for some reason, you can cancel the old scope and get a new one. That makes the
    onDispose
    dance you're doing a bit different though
    For app code I probably wouldn't worry about it, but it's the sort of thing I would want to be very precise about if I were writing a library for wider consumption
    If an object implements
    CompositionLifecycleObserver
    it will get callbacks when it enters or leaves a composition. In other words, if you
    remember
    an object that implements that interface, compose will call its
    onEnter
    and
    onLeave
    methods automatically for you, and you won't need a separate
    onDispose
    flosch

    flosch

    2 years ago
    For app code I probably wouldn’t worry about it, but it’s the sort of thing I would want to be very precise about if I were writing a library for wider consumption
    Yes, I am currently trying to figure out if using my lib with compose is possible out of the box or if I should provide a way to create a Controller for @Composables without a specific
    CoroutineScope
    Thanks for your help though! 🙂
    c

    codeslubber

    2 years ago
    Much as I see utility in coroutines, I still would say if someone could really solve composability and concurrency (scoping), they should be entitled to a Genius award. (We should have them in tech: fix this intractable problem and we’ll give you a huge plaque and a million bucks…)