blob: b4620be326e99df41f67066032d71c47ba9ff5df [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose
/**
* @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
* life time of that composable in the composition, that parameter will remain unchanged. If it does
* change (as in, the previous and current values passed into [Object.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:
*
* @Composable
* fun UserList(userIds: List<Int>) {
* for (id in userIds) {
* UserRow(userId=id)
* }
* }
*
* @Composable
* fun UserRow(userId: Int) {
* val user = +state<User?> { null }
* +onActive {
* val dispose = Api.getUserAsync(userId) { user = it }
* onDispose { dispose() }
* }
* if (user == null) {
* LoadingIndicator()
* return
* }
* Image(src=user.profileImage)
* 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)`:
*
* @Composable
* fun UserRow(userId: Int) {
* val user = +state<User?> { null }
* +onCommit(userId) {
* val dispose = Api.getUserAsync(userId) { user = it }
* onDispose { dispose() }
* }
* if (user == null) {
* LoadingIndicator()
* return
* }
* Image(src=user.profileImage)
* 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:
*
* @Composable
* fun UserRow(@Pivotal userId: Int) {
* val user = +state<User?> { null }
* +onActive {
* val dispose = Api.getUserAsync(userId) { user = it }
* onDispose { dispose() }
* }
* if (user == null) {
* LoadingIndicator()
* return
* }
* Image(src=user.profileImage)
* Text(text=user.name)
* }
*
* @see Key
* @see key
*/
@MustBeDocumented
@Target(
// composable function paramters
AnnotationTarget.VALUE_PARAMETER,
// component class properties
AnnotationTarget.PROPERTY,
// component class setter functions
AnnotationTarget.FUNCTION
)
annotation class Pivotal