Hi all, it'd be great if some of you could proofre...
# compose
m
Hi all, it'd be great if some of you could proofread my article https://okmanideep.github.io/understanding-composition-and-side-effects It tries to shed some light on the order of execution of @Composable functions and Side Effects
a
nice article! It may be a bit much for your intended audience, but it may be useful to note that both
LaunchedEffect
and
DisposableEffect
are implemented using the same underlying primitive:
RememberObserver
. This might make an interesting addition to your samples:
Copy code
remember {
  object : RemembeObserver {
    override fun onRemembered() {}
    override fun onForgotten() {}
    override fun onAbandoned() {}
  }
}
πŸ‘πŸΌ 2
πŸ’― 3
πŸ‘ 4
today i learned 4
πŸ™ 3
the difference in timing between
DisposableEffect
and
LaunchedEffect
comes from coroutine dispatch.
LaunchedEffect
is doing a
CoroutineScope.launch
in response to
onRemembered
, and it doesn't start running immediately, it runs once the associated dispatcher schedules and runs it. On Android that means on the main thread after the current frame is done being processed
the launched
Job
is `cancel()`ed in
onForgotten
, just as
onDispose
runs in
onForgotten
πŸ‘ 1
you noted that recomposition is what computes what left and entered the composition and then those are resolved after recomposition is complete in a footnote but imo that's kind of an important part of the mental model; pitching it as a strange behavior that you just have to memorize in the takeaway sections is kind of a disservice, I think, since it's that mental model of, "compute changes, then apply changes" that leads to the ordering: forget all outgoing things in LIFO order to provide deterministic destruction semantics, initialize all incoming things in FIFO order to provide deterministic init semantics, then run side effects to apply changes to anything fully initialized by RememberObservers
πŸ‘ 1
πŸ‘πŸΌ 1
m
@Adam Powell Totally agree with your point. Felt the same but was also concerned about readers being able to understand. Will try rephrasing that section.
a
the idea is, if you have an object that doesn't play by RAII-like semantics that you need to treat like this:
Copy code
val obj = MyObject()
obj.init(someInitParams)
obj.property = someValue
obj.dispose()
then with compose you can drive that object like this:
Copy code
val obj = remember { MyObject() }
DisposableEffect(obj)
  obj.init(someInitParams)
  onDispose {
    obj.dispose()
  }
}
SideEffect {
  obj.property = someValue
}
πŸ‘ 1
and you can create nested chains of them:
Copy code
val inner = remember { InnerObject() }
DisposableEffect(inner) {
  inner.init()
  onDispose { inner.dispose() }
}
val outer = remember(inner) { OuterObject(inner) }
DisposableEffect(outer) {
  outer.init()
  onDispose { outer.dispose() }
}
since the
onForgotten
calls run in LIFO order,
outer
will be disposed before `inner`; if
outer.dispose()
requires
inner
to still be valid - a fairly common setup - then everything Just Works and survives refactor/extract function on the code above. The ordering is based on order in the composition, not within a single composable function.
as a consequence of that, the recommendation in the article to push effects to the end of a composable can be kind of dangerous, as it means your effects will be ordered to init after child composables you call and dispose before them
πŸ‘ 1
πŸ’― 1
m
Oh yes. Didn't think of this
a
instead, try to place effects as close as possible to the things they act on with as little in between as possible, since things in between will run in between according to this ordering
πŸ‘ 1
πŸ‘πŸΌ 1
now one of the reasons we don't talk a lot about
RememberObserver
is that its semantics can be kind of unexpected if you aren't familiar with how other parts of compose works
πŸ‘ 1
in particular, RememberObservers are more or less refcounted within a single composition. Something gets
onRemember
when it is remembered in one or more places in a single composition, and
onForgotten
when it was previously remembered in a composition but no longer appears anywhere within it.
This gets affected by a few things: 1) it's not always clear to a developer where they're dealing with one composition or several. If the same object is remembered in multiple compositions, it will get multiple calls to
onRemembered
2) it's not always clear to a developer where compose is remembering something on their behalf, independent of specific
remember
calls. For example, compose internally remembers the parameters to composable function invocations - it has to, otherwise how would it be able to check new parameters vs. old to determine whether a composable can skip? This counts toward that refcount within a composition.
because this can get complicated, it's generally best practice not to let references to objects that implement
RememberObserver
escape from where they're deliberately `remember {}`ed. Especially on Android, developers are used to optimizing object allocations by having objects do multiple duties, implementing several interfaces, but passing a reference to one of those interfaces elsewhere. When working with
RememberObserver
that can have consequences.
If I have the following:
Copy code
interface Thing {
  fun doStuff()
}

private class ThingImpl : Thing, RememberObserver {
  // ...
}
then as a developer, if I have a
Thing
I don't know that it's going to have some potentially strange behavior if I ever pass it as an argument to a composable function.
so we stick with, "probably don't do that unless you really, really know what you're doing" πŸ™‚
😁 2
and the intro guidance focuses on
DisposableEffect
and
LaunchedEffect
instead, since it's much harder to get into these cases when using those higher level APIs
m
@Adam Powell thanks for the feedback. Will make the necessary changes to create the right kind of mental model in the reader.
a
I think it's not too far off; you could probably skip all of the
RememberObserver
stuff and save it for a later article, but the LIFO ordering for init/dispose is pretty important in terms of understanding why it works the way it does
βœ… 2
(and we should probably put something about that more front and center on d.android.com too)
☝️ 1
☝🏼 1
πŸ’― 3
t
I'd like to suggest reorganizing the text so that the output of the example is closer to the logic about effects. I had to keep scrolling back and forth to walk through your logic for the effects.
That suggestion is mainly for the first run of the code.
πŸ’― 1