Applier
@JvmDefaultWithCompatibility
public interface Applier<N>
An Applier is responsible for applying the tree-based operations that get emitted during a
composition. Every Composer
has an Applier
which it uses to emit a ComposeNode
.
A custom Applier
implementation will be needed in order to utilize Compose to build and
maintain a tree of a novel type.
Functions
public fun onBeginChanges()
Called when the Composer
is about to begin applying changes using this applier.
onEndChanges
will be called when changes are complete.
public fun onEndChanges()
Called when the Composer
is finished applying changes using this applier. A call to
onBeginChanges
will always precede a call to onEndChanges
.
public fun down(node: N)
Indicates that the applier is getting traversed "down" the tree. When this gets called,
node
is expected to be a child of current
, and after this operation, node
is expected
to be the new current
.
public fun up()
Indicates that the applier is getting traversed "up" the tree. After this operation
completes, the current
should return the "parent" of the current
node at the beginning of
this operation.
public fun insertTopDown(index: Int, instance: N)
Indicates that instance
should be inserted as a child to current
at index
. An applier
should insert the node into the tree either in insertTopDown
or insertBottomUp
, not both.
The insertTopDown
method is called before the children of instance
have been created and
inserted into it. insertBottomUp
is called after all children have been created and
inserted.
Some trees are faster to build top-down, in which case the insertTopDown
method should be
used to insert the instance
. Other trees are faster to build bottom-up in which case
insertBottomUp
should be used.
To give example of building a tree top-down vs. bottom-up consider the following tree,
where the node B
is being inserted into the tree at R
. Top-down building of the tree
first inserts B
into R
, then inserts A
into B
followed by inserting C
into B`. For
example,
A bottom-up building of the tree starts with inserting A
and C
into B
then inserts B
tree into R
.
To see how building top-down vs. bottom-up can differ significantly in performance consider a tree where whenever a child is added to the tree all parent nodes, up to the root, are notified of the new child entering the tree. If the tree is built top-down,
R
is notified ofB
entering.B
is notified ofA
entering,R
is notified ofA
entering.B
is notified ofC
entering,R
is notified ofC
entering.
for a total of 5 notifications. The number of notifications grows exponentially with the number of inserts.
For bottom-up, the notifications are,
B
is notifiedA
entering.B
is notifiedC
entering.R
is notifiedB
entering.
The notifications are linear to the number of nodes inserted.
If, on the other hand, all children are notified when the parent enters a tree, then the notifications are, for top-down,
B
is notified it is enteringR
.A
is notified it is enteringB
.C
is notified it is enteringB
.
which is linear to the number of nodes inserted.
For bottom-up, the notifications look like,
A
is notified it is enteringB
.C
is notified it is enteringB
.B
is notified it is enteringR
,A
is notified it is enteringR
,C
is notified it is enteringR
. which exponential to the number of nodes inserted.
public fun insertBottomUp(index: Int, instance: N)
Indicates that instance
should be inserted as a child of current
at index
. An applier
should insert the node into the tree either in insertTopDown
or insertBottomUp
, not both.
See the description of insertTopDown
to which describes when to implement insertTopDown
and when to use insertBottomUp
.
public fun remove(index: Int, count: Int)
Indicates that the children of current
from index
to index
+ count
should be removed.
public fun move(from: Int, to: Int, count: Int)
Indicates that count
children of current
should be moved from index from
to index to
.
The to
index is relative to the position before the change, so, for example, to move an
element at position 1 to after the element at position 2, from
should be 1
and to
should be 3
. If the elements were A B C D E, calling move(1, 3, 1)
would result in the
elements being reordered to A C B D E.
public fun clear()
Move to the root and remove all nodes from the root, preparing both this Applier
and its
root to be used as the target of a new composition in the future.
public fun apply(block: N.(Any?) -> Unit, value: Any?)
Apply a change to the current node.
public fun reuse()
Notify current
is is being reused in reusable content.
Code Examples
CustomTreeComposition
@Suppress("unused")
fun CustomTreeComposition() {
// Provided we have a tree with a node base type like the following
abstract class Node {
val children = mutableListOf<Node>()
}
// We would implement an Applier class like the following, which would teach compose how to
// manage a tree of Nodes.
class NodeApplier(root: Node) : AbstractApplier<Node>(root) {
override fun insertTopDown(index: Int, instance: Node) {
current.children.add(index, instance)
}
override fun insertBottomUp(index: Int, instance: Node) {
// Ignored as the tree is built top-down.
}
override fun remove(index: Int, count: Int) {
current.children.remove(index, count)
}
override fun move(from: Int, to: Int, count: Int) {
current.children.move(from, to, count)
}
override fun onClear() {
root.children.clear()
}
}
// A function like the following could be created to create a composition provided a root Node.
fun Node.setContent(parent: CompositionContext, content: @Composable () -> Unit): Composition {
return Composition(NodeApplier(this), parent).apply { setContent(content) }
}
// assuming we have Node sub-classes like "TextNode" and "GroupNode"
class TextNode : Node() {
var text: String = ""
var onClick: () -> Unit = {}
}
class GroupNode : Node()
// Composable equivalents could be created
@Composable
fun Text(text: String, onClick: () -> Unit = {}) {
ComposeNode<TextNode, NodeApplier>(::TextNode) {
set(text) { this.text = it }
set(onClick) { this.onClick = it }
}
}
@Composable
fun Group(content: @Composable () -> Unit) {
ComposeNode<GroupNode, NodeApplier>(::GroupNode, {}, content)
}
// and then a sample tree could be composed:
fun runApp(root: GroupNode, parent: CompositionContext) {
root.setContent(parent) {
var count by remember { mutableStateOf(0) }
Group {
Text("Count: $count")
Text("Increment") { count++ }
}
}
}
}