Trying to build my first custom modifier using the...
# compose
s
Trying to build my first custom modifier using the Modifier.Node() APIs, and I could use some input regarding how to ensure that some coroutine is working on the latest input and is not stuck on the first thing that was input instead. More in thread 🧵
I have a element/node combination like this:
Copy code
private data class OverlaidIndicationConnectionElement(
  val overlaidIndicationState: OverlaidIndicationState,
  val interactionSource: MutableInteractionSource,
) : ModifierNodeElement<OverlaidIndicationConnectionNode>() {
  override fun create(): OverlaidIndicationConnectionNode {
    return OverlaidIndicationConnectionNode(overlaidIndicationState, interactionSource)
  }

  override fun update(node: OverlaidIndicationConnectionNode) {
    node.overlaidIndicationState = overlaidIndicationState
    node.interactionSource = interactionSource
  }
}

private class OverlaidIndicationConnectionNode(
  var overlaidIndicationState: OverlaidIndicationState,
  var interactionSource: MutableInteractionSource,
) : LayoutAwareModifierNode, Modifier.Node() {
  private var offset = IntOffset(0, 0)

  override fun onAttach() {
    coroutineScope.launch {
      interactionSource.interactions.collect {
        overlaidIndicationState.offset = offset
        overlaidIndicationState.interactionSource.tryEmit(it)
      }
    }
  }

  override fun onPlaced(coordinates: LayoutCoordinates) {
    offset = coordinates.positionInParent().round()
  }
}
But the problem here is that I am doing
interactionSource.interactions.collect
on
onAttatch
, but that won't be re-run if the interaction source were to ever change.
I fiddled with this a bunch, and I got down to this approach instead, where I will force the element to use a new
update
function on my node by making the vars private, and in there I will have to do some bookkeeping regarding cancelling the old collection and starting a new one, all by manually working on a
Job
object.
Copy code
private data class OverlaidIndicationConnectionElement(
  val overlaidIndicationState: OverlaidIndicationState,
  val interactionSource: MutableInteractionSource,
) : ModifierNodeElement<OverlaidIndicationConnectionNode>() {
  override fun create(): OverlaidIndicationConnectionNode {
    return OverlaidIndicationConnectionNode(overlaidIndicationState, interactionSource)
  }

  override fun update(node: OverlaidIndicationConnectionNode) {
    node.update(overlaidIndicationState, interactionSource)
  }
}

private class OverlaidIndicationConnectionNode(
  private var overlaidIndicationState: OverlaidIndicationState,
  private var interactionSource: MutableInteractionSource,
) : LayoutAwareModifierNode, Modifier.Node() {
  private var offset = IntOffset(0, 0)
  private var collectingJob: Job? = null

  override fun onAttach() {
    startCollection()
  }

  fun update(overlaidIndicationState: OverlaidIndicationState, interactionSource: MutableInteractionSource) {
    this.overlaidIndicationState = overlaidIndicationState
    this.interactionSource = interactionSource
    startCollection()
  }

  private fun startCollection() {
    collectingJob?.cancel()
    collectingJob = coroutineScope.launch {
      interactionSource.interactions.collect {
        overlaidIndicationState.offset = offset
        overlaidIndicationState.interactionSource.tryEmit(it)
      }
    }
  }

  override fun onPlaced(coordinates: LayoutCoordinates) {
    offset = coordinates.positionInParent().round()
  }
}
I haven't found enough such nodes online to try to compare my solution against, most are doing their own thing and I haven't found one which needs to do such a coroutine collection etc. If anyone sees anything wrong with my implementation or has some other input on this I would appreciate it!
OverlaidIndicationState is just an interface marked as this
Copy code
@Stable
internal interface OverlaidIndicationState {
  var offset: IntOffset
  val interactionSource: MutableInteractionSource
}

private class OverlaidIndicationStateImpl() : OverlaidIndicationState {
  override var offset by mutableStateOf(IntOffset(0, 0))
  override val interactionSource: MutableInteractionSource = MutableInteractionSource()
}
The details of this are not important here I think, I just thought I'd put it here since it might be confusing without it otherwise.
e
Yeah the implementation looks solid / like what I’d do. I’ve written a few nodes that work just like that.
thank you color 1
s
Thanks, really appreciate you taking the time to read this! I just couldn't find enough samples out there that did something like this to compare them with what I do as a sanity check 😅 Perhaps my searching skills just failed me though.