Skip to content

fix(android): stable template view types and no dropped data changes - #104

Open
vallemar wants to merge 2 commits into
masterfrom
fix/android-stable-template-types
Open

vallemar wants to merge 2 commits into
masterfrom
fix/android-stable-template-types

Conversation

@vallemar

@vallemar vallemar commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Three related Android issues that show up as cells rendered with the wrong template (or an empty list) when a CollectionView uses several templates through itemTemplateSelector. Found while debugging a chat screen (day separators, incoming and outgoing bubbles, typing row) built with nativescript-vue 3 on @nativescript/core 9.1.

  1. Vue 3 component: new itemTemplateSelector function on every render. The render function passed a fresh arrow function each time, so every re-render of the component (for example when any bound attribute of the <CollectionView> changes, like :opacity) changed the native itemTemplateSelector property. On Android that handler calls clearTemplateTypes() and refresh() (a full notifyDataSetChanged()). The selector is now created once in setup().

  2. Android: view types were numbered in request order. templateKeyToNativeItem assigned the next free number to whatever key was asked first (the branch that numbered templates from itemTemplates was dead code: the maps are always initialized in the constructor). After clearTemplateTypes() the numbering started again from 0, so the same view type could now mean a different template while the RecyclerView kept reusing the holders created with the old numbering. Result: holders (and the Vue cells rendered inside them) bound to items of another template. Named templates now always get their index in itemTemplates as view type (unknown keys start at 100, as the original comment intended), and getKeyByValue derives the key the same way when the reverse map is empty.

  3. Android: onSourceCollectionChanged dropped changes silently. When the adapter did not exist yet, updates were suspended, or the RecyclerView was computing its layout, the ObservableArray change was simply ignored. Data pushed into the array before the adapter existed left the list empty until some unrelated refresh happened (which, until fix 1, was the spurious refresh caused by the selector). A deferred refresh() is now scheduled instead. While updates are suspended nothing changes: resumeUpdates(true) is still the caller's responsibility.

How to reproduce (before)

Vue 3 app, CollectionView with an itemTemplateSelector returning e.g. header / in / out, items in an ObservableArray, and any dynamic attribute bound on the component. Fill the array shortly after the page loads and toggle the attribute: some cells come out rendered with a different template than the one the selector returned for their item (or the list stays empty when the data arrived before the adapter was created).

Verification

Tested on an Android emulator (Pixel 8, API 35) with @nativescript/core 9.1.1 and nativescript-vue 3.0.3, dumping the native view tree with uiautomator while scrolling and while items were inserted, removed and replaced. Before: cells with the wrong template and an empty list on first load. After: every cell matches its item's template and the first page renders as soon as it is pushed. tsc reports no new errors for the two changed files. iOS is not affected by 2 and 3 (it reuses cells by template name), but it benefits from 1 (no needless refresh on every render).

🤖 Generated with Claude Code

- vue3: pass one stable `itemTemplateSelector` function instead of a new
  arrow function on every render. Each render changed the native property,
  which on Android cleared the template type map and ran a full
  `notifyDataSetChanged()`.
- android: number template view types by their index in `itemTemplates`
  instead of request order, so `clearTemplateTypes()` can no longer make an
  existing view type mean a different template while the RecyclerView keeps
  reusing the holders created before (cells rendered with the wrong
  template, e.g. a list item rendered with a header template or an incoming
  chat message rendered with the outgoing one).
- android: when an ObservableArray change cannot be applied (no adapter yet,
  updates suspended or the RecyclerView is computing its layout), schedule a
  refresh instead of dropping the change silently. Data pushed before the
  adapter existed left the list empty until some unrelated refresh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vallemar

Copy link
Copy Markdown
Contributor Author

@copilot check

Co-authored-by: vallemar <15719383+vallemar@users.noreply.github.com>

Copilot AI commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@copilot check

Addressed a Vue 3 follow-up regression in 0d55a39: real itemTemplateSelector prop changes now update the native selector again without reintroducing the per-render Android refresh. The packages/collectionview TypeScript build passed, and final review + CodeQL checks were clean.

}));

const getSlotName = (item: any, index: number, items: ListItem[]) => props.itemTemplateSelector?.(item, index, items) ?? 'default';
// Keep ONE stable selector function. Passing a new arrow function on every render

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those AI comments are far too long. Keep them only 2/3 lines and ONLY if necessary!

// data). While updates are suspended, `resumeUpdates(true)` is the caller's job.
if (!this._dataUpdatesSuspended && !this._pendingRefresh) {
this._pendingRefresh = true;
setTimeout(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTimeout is never a good solution. Plus what does this fix? when does this happen? a reproducible example? i wont add timeout maybe fixes if we cant reproduce. And we should instead call refresh in onLayout if it is a refresh while computing layout

if (key !== undefined) {
return key;
}
// After `clearTemplateTypes()` the reverse map is empty until each key is requested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not a fix. The question is why the getKeyByValue is called with templateStringTypeNumber empty?
This should not happen

@farfromrefug

Copy link
Copy Markdown
Member

@vallemar you can update your PR after #106 which adds a way to defer updates

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants