-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/viewability configs #301
Conversation
As for the performance, I have not found any performance regression when using viewability callbacks using this PR - the average FPS was 45.5 +/- 1. |
Called when the viewability of rows changes, as defined by the `viewabilityConfig` prop. Array of `changed` includes `ViewToken`s that both visible and non-visible items. You can use the `isViewable` flag to filter the items. | ||
|
||
:::note | ||
If you are tracking the time a view becomes (non-)visible, use the `timestamp` property. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A short explanation can be helpful here
|
||
#### itemVisiblePercentThreshold | ||
|
||
Similar to `viewAreaCoveragePercentThreshold`, but considers the percent of the item that is visible, rather than the fraction of the viewable area it covers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the rule if I specify both viewAreaCoveragePercentThreshold
and itemVisiblePercentThreshold
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, we should throw an exception in that case, as FlatList
does.
recordInteraction(); | ||
``` | ||
|
||
Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. if `waitForInteractions` is true and the user has not scrolled. This is typically called by taps on items or by navigation actions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we be more explicit in stating that devs have to actually call this method? After reading the last line I felt that list will detect this automatically which, I'm assuming, is not the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I will rephrase this - it's from the React Native documentation but it certainly can be a bit more clear.
fixture/src/twitter/Twitter.tsx
Outdated
@@ -56,6 +56,14 @@ const Twitter = () => { | |||
ItemSeparatorComponent={Divider} | |||
data={tweets} | |||
initialScrollIndex={debugContext.initialScrollIndex} | |||
viewabilityConfig={{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FlashList is a pure component so ideally we should avoid sending in new objects. It may be relevant if someone tries to draw inspiration from here. Something good to have but not a necessity if we look at these as maintainer only samples.
src/FlashList.tsx
Outdated
@@ -120,6 +130,14 @@ export interface FlashListProps<T> extends FlatListProps<T> { | |||
* If you're using ListEmptyComponent, this event is raised as soon as ListEmptyComponent is rendered. | |||
*/ | |||
onLoad?: (info: { elapsedTimeInMs: number }) => void; | |||
|
|||
/** | |||
* Called when the viewability of rows changes, as defined by the viewablePercentThreshold prop. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the word row
is not relevant to FlashList. Even with numColumns > 1 we treat everything as individual items
src/FlashList.tsx
Outdated
* Creates a new `ViewabilityHelper` instance with `onViewableItemsChanged` callback and `ViewabilityConfig` | ||
* @returns `ViewabilityHelper` instance | ||
*/ | ||
private createViewabilityHelper = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There really seems to be a need for a manager class that can setup all the viewability helpers and can forward right events. It would be better if FlashList
doesn't do that. It will also reduce the additions required in this file.
src/FlashList.tsx
Outdated
windowCorrectionConfig={this.getUpdatedWindowCorrectionConfig()} | ||
/> | ||
</StickyHeaderContainer> | ||
); | ||
} | ||
|
||
private onVisibleIndicesChanged = (all: number[]) => { | ||
this.viewabilityHelpers.forEach( | ||
(viewabilityHelper) => (viewabilityHelper.possiblyViewableIndices = all) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateViewableItems
method in viewabilityHelper
can also accept and new array of items. It will make this cleaner.
src/FlashList.tsx
Outdated
this.viewabilityHelpers.forEach((viewabilityHelper) => { | ||
viewabilityHelper.hasInteracted = true; | ||
}); | ||
this.updateViewableItems(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also call this.props.onScrollBeginDrag
incase the developer has sent it
src/FlashList.tsx
Outdated
this.viewabilityHelpers.forEach((viewabilityHelper) => { | ||
viewabilityHelper.updateViewableItems( | ||
this.props.horizontal ?? false, | ||
this.rlvRef?.getCurrentScrollOffset() ?? 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may not be enough. Check your calculation with a header. You may need to subtract distanceFromWindow
here.
src/ViewabilityHelper.ts
Outdated
this.timers.forEach(clearTimeout); | ||
} | ||
|
||
updateViewableItems( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you mark public methods as public
. Makes it easier to read
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fortmarek I've left some comments. The tests look awesome btw.
Overall, some changes in design can be helpful. Like FlashList and viewablity helper do too much together and FlashList is acting as a manager of multiple instances of viewability helper.
Can we create a manager that takes minimum input from FlashList (a ref also works) and abstracts all the management away? Even better if the manager can have a generic interface.
Also, let's see the impact of this on performance (if any)
@naqvitalha I have created a new component |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tophatted, a couple of nits and questions 👍
isViewable: boolean; | ||
item: string; | ||
key: string; | ||
timestamp: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FlatList
's ViewToken
also has section
prop. We don't support it yet, but maybe add a note about it?
`viewabilityConfig` is a default configuration for determining whether items are viewable. | ||
|
||
:::warning | ||
Changing viewabilityConfig on the fly is not supported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does on the fly
mean specifically, what's not possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you cannot change it after the first render. It's how FlatList phrases it - do you think there's a better way to explain this?
@@ -39,6 +45,12 @@ export const mountFlashList = (props?: { | |||
) => void; | |||
estimatedItemSize?: number; | |||
ListEmptyComponent?: FlashListProps<string>["ListEmptyComponent"]; | |||
viewabilityConfig?: ViewabilityConfig | null; | |||
onViewableItemsChanged?: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: since onViewableItemsChanged
is optional, is it necessary to define it's type as .. | null | undefined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is - for example, in the test utils (mountFlashList
) we have:
onViewableItemsChanged={props?.onViewableItemsChanged}
This expression would be invalid if undefined
is not specified in the type. Defining a property as xxx?: string | null | undefined
means you don't have to specify it (in which case it is undefined
) but if you do, it has to be equal to the type after :
.
Therefore, we should definitely keep undefined
, null
might not be that useful but I think it's better to keep it as there's not a really strong reason to constrain users in defaulting to undefined
.
/** | ||
* Viewable indices regardless of the viewability config | ||
*/ | ||
possiblyViewableIndices: number[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: possiblyViewableIndices
is not well explanatory without the comment since, it's actually viewed, but unfiltered items
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
those items are visible but may not fit the criteria - do you have a suggestion for a better name? I'd argue unfilteredViewableIndices
is not self-explanatory, too, and can't think of anything better.
constructor(flashListRef: FlashList<T>) { | ||
this.flashListRef = flashListRef; | ||
if ( | ||
flashListRef.props.onViewableItemsChanged !== null && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should onViewableItemsChanged
be both null | undefined
? It could be reduced to check for undefined
, no? Or it there any specific reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained here, I'd rather keep it.
updateViewableItems({ viewabilityHelper }); | ||
// Initial call | ||
expect(viewableIndicesChanged).toHaveBeenCalledWith( | ||
[0, 1, 2], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why [0, 1, 2]
is expected twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the first array is what's viewable now whereas the second array defines what is newly viewable. The third then is for what is newly not viewable.
); | ||
|
||
updateViewableItems({ viewabilityHelper, scrollOffset: 50 }); | ||
expect(viewableIndicesChanged).toHaveBeenCalledWith([0, 1, 2, 3], [3], []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you please add comments about expected results? 🙌
It's a bit hard to understand on the go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👌
}; | ||
|
||
private onScroll = () => { | ||
this.viewabilityManager.updateViewableItems(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call this.props.onScroll too
}; | ||
|
||
public recordInteraction = () => { | ||
this.viewabilityHelpers.forEach((viewabilityHelper) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will run everytime onScrollBeginDrag
happens. After the first call we can ignore it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to call updateViewableItems
also after first interaction
src/FlashList.tsx
Outdated
@@ -414,12 +435,27 @@ class FlashList<T> extends React.PureComponent< | |||
} | |||
initialOffset={initialOffset} | |||
onItemLayout={this.onItemLayout} | |||
onScroll={this.onScroll} | |||
onVisibleIndicesChanged={ | |||
this.viewabilityManager.onVisibleIndicesChanged |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid listening to this if devs don't want callbacks?
}; | ||
|
||
public updateViewableItems = (newViewableIndices?: number[]) => { | ||
const listSize = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no registered viewability helper we can skip all this code. Check for length in the beginning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, awesome work @fortmarek
Description
This PR partly implements #268.
It's currently missing:
FlatList
does not support.The API itself closely copies
FlatList
as there was no real reason for us to deviate and by default, we try to stick whatFlatList
has.One thing touched on in the original issue was the following:
The current state of the PR does not defer callbacks in any way - rather, it invokes them synchronously. This copies the behavior
FlatList
has. The current implementation does mean viewability callbacks will potentially worsen performance if JS thread is busy, user is scrolling quickly, and theminimumViewTime
is high. The performance drop also depends on the users' implementation.The idea to defer viewability callbacks when JS thread is not busy (using
requestIdleCallback
) is still on the table but it has its drawbacks:FlatList
timestamp
field that would be used to track when the viewability event actually occurredAdditionally, having a sensible
minimumViewTime
already ensures that the number of viewability callback invocations will stay low - which is why I have added a default of 250 ms if user does not specify it. But even if user does specify a lowerminimumViewTime
, even 0, I have not observed any noticeable drop in performance when using and not using viewability callbacks.One thing we could do is to add
timestamp
field to theViewToken
object (sent in the viewability callback) now and instruct users to use it. This will enable us making those callbacks deferred in the future without breaking users' implementations.Update:
We have decided to add the
timestamp
and instruct people to use it. This enables us to defer the callbacks in the future without breaking users' implementation: 50cb39aReviewers’ hat-rack 🎩
Twitter
andTwitterFlatList
and observe viewability callbacks which are currently logged to the console (I suggest commenting out the blank area log inApp.tsx
Checklist