Pivotal

@Target([AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION]) annotation class Pivotal
androidx.compose.Pivotal

Pivotal can be applied to the parameters of a composable to indicate that the parameter contributes to the "identity" of the composable. Pivotal parameters are used when calculating the composable's key, and the composable's key is used to determine whether or not the previous results and state of the composable have moved or can be reused.

By setting a parameter to a composable function as Pivotal, you are ensuring that for the lifetime of that composable in the composition, that parameter will remain unchanged. If it does change (as in, the previous and current values passed into Any.equals evaluate to false), then the composable will start a new life time, as if it had been removed and recreated.

As a result, the Pivotal annotation can be used to simplify component logic, as well as improve performance in some cases.

Let's consider the following example, where we have a list of users being displayed from a list of user ids:

import androidx.compose.onActive
import androidx.compose.onDispose
import androidx.compose.state
import androidx.ui.core.Text

@Composable
fun UserList(userIds: List<Int>) {
    for (id in userIds) {
        UserRow(userId = id)
    }
}

@Composable
fun UserRow(userId: Int) {
    var user by +state<User?> { null }
    +onActive {
        val dispose = Api.getUserAsync(userId) { user = it }
        onDispose(dispose)
    }

    if (user == null) {
        LoadingIndicator()
        return
    }
    ProfileIcon(src = user!!.profilePhotoUrl)
    Text(text = user!!.name)
}

This example has a bug in it. If the list of user ids is reordered in any way, Compose will reuse previous instances of the UserRow that were created with previous ids. This means that user requests that had previously come in will show up in the wrong position. Semantically, the author of this code had intended UserRow to move with the user id, but had not written it to do so.

One way to fix this would be to change onActive to onCommit(userId):

import androidx.compose.onCommit
import androidx.compose.onDispose
import androidx.compose.state
import androidx.ui.core.Text

@Composable
fun UserRow(userId: Int) {
    var user by +state<User?> { null }
    +onCommit(userId) {
        val dispose = Api.getUserAsync(userId) { user = it }
        onDispose(dispose)
    }

    if (user == null) {
        LoadingIndicator()
        return
    }
    ProfileIcon(src = user!!.profilePhotoUrl)
    Text(text = user!!.name)
}

In this rendition, the proper users will show up in the proper places, however, in the case where the list of user Ids is shuffled, it is likely that the program will have to execute every API request again, despite not needing to.

The reason for this is because despite the intention of the author being that a UserRow component's "identity" is determined by which user it is composing (and thus, which userId), Compose has no way of knowing this. The Pivotal annotation is meant for exactly this purpose.

The ideal and correct implementation of the above UserRow component is thus as follows:

import androidx.compose.onActive
import androidx.compose.onDispose
import androidx.compose.state
import androidx.ui.core.Text

@Composable
fun UserRow(@Pivotal userId: Int) {
    var user by +state<User?> { null }
    +onActive {
        val dispose = Api.getUserAsync(userId) { user = it }
        onDispose(dispose)
    }

    if (user == null) {
        LoadingIndicator()
        return
    }
    ProfileIcon(src = user!!.profilePhotoUrl)
    Text(text = user!!.name)
}

Summary

Public constructors

Pivotal can be applied to the parameters of a composable to indicate that the parameter contributes to the "identity" of the composable.

Public constructors

<init>

Pivotal()

Pivotal can be applied to the parameters of a composable to indicate that the parameter contributes to the "identity" of the composable. Pivotal parameters are used when calculating the composable's key, and the composable's key is used to determine whether or not the previous results and state of the composable have moved or can be reused.

By setting a parameter to a composable function as Pivotal, you are ensuring that for the lifetime of that composable in the composition, that parameter will remain unchanged. If it does change (as in, the previous and current values passed into Any.equals evaluate to false), then the composable will start a new life time, as if it had been removed and recreated.

As a result, the Pivotal annotation can be used to simplify component logic, as well as improve performance in some cases.

Let's consider the following example, where we have a list of users being displayed from a list of user ids:

import androidx.compose.onActive
import androidx.compose.onDispose
import androidx.compose.state
import androidx.ui.core.Text

@Composable
fun UserList(userIds: List<Int>) {
    for (id in userIds) {
        UserRow(userId = id)
    }
}

@Composable
fun UserRow(userId: Int) {
    var user by +state<User?> { null }
    +onActive {
        val dispose = Api.getUserAsync(userId) { user = it }
        onDispose(dispose)
    }

    if (user == null) {
        LoadingIndicator()
        return
    }
    ProfileIcon(src = user!!.profilePhotoUrl)
    Text(text = user!!.name)
}

This example has a bug in it. If the list of user ids is reordered in any way, Compose will reuse previous instances of the UserRow that were created with previous ids. This means that user requests that had previously come in will show up in the wrong position. Semantically, the author of this code had intended UserRow to move with the user id, but had not written it to do so.

One way to fix this would be to change onActive to onCommit(userId):

import androidx.compose.onCommit
import androidx.compose.onDispose
import androidx.compose.state
import androidx.ui.core.Text

@Composable
fun UserRow(userId: Int) {
    var user by +state<User?> { null }
    +onCommit(userId) {
        val dispose = Api.getUserAsync(userId) { user = it }
        onDispose(dispose)
    }

    if (user == null) {
        LoadingIndicator()
        return
    }
    ProfileIcon(src = user!!.profilePhotoUrl)
    Text(text = user!!.name)
}

In this rendition, the proper users will show up in the proper places, however, in the case where the list of user Ids is shuffled, it is likely that the program will have to execute every API request again, despite not needing to.

The reason for this is because despite the intention of the author being that a UserRow component's "identity" is determined by which user it is composing (and thus, which userId), Compose has no way of knowing this. The Pivotal annotation is meant for exactly this purpose.

The ideal and correct implementation of the above UserRow component is thus as follows:

import androidx.compose.onActive
import androidx.compose.onDispose
import androidx.compose.state
import androidx.ui.core.Text

@Composable
fun UserRow(@Pivotal userId: Int) {
    var user by +state<User?> { null }
    +onActive {
        val dispose = Api.getUserAsync(userId) { user = it }
        onDispose(dispose)
    }

    if (user == null) {
        LoadingIndicator()
        return
    }
    ProfileIcon(src = user!!.profilePhotoUrl)
    Text(text = user!!.name)
}

See Also