Flutter中基于流式数据的单可展开列表两种实现方案性能对比及选型疑问
Great question! Let’s break down the performance differences between your two approaches, fix the key issue you’re facing with ExpansionTile, and help you pick the best option for your use case.
Core Performance Differences
First, let’s clarify how each approach handles rendering and state changes:
1. ListView.builder + ExpansionTile (Original Implementation)
Your initial code has a critical flaw with the PageStorageKey: using DateTime.now().millisecondsSinceEpoch means every time the UI rebuilds (e.g., when selecting a new item), every existing ExpansionTile gets a new unique key. This forces Flutter to completely destroy and re-create all visible tiles instead of just updating their expanded state. This wipes out the lazy-loading benefit of ListView.builder (which only builds items as they enter the viewport) and causes unnecessary performance overhead.
That said, when implemented correctly (with stable keys), this approach is the most performant option—especially for large datasets.
2. ExpansionPanelList + ExpansionPanel
This approach has a fundamental limitation: it does not support lazy loading. It builds all ExpansionPanel widgets upfront, even if they’re not visible on the screen. For small datasets (e.g., 10-20 items), this is manageable, but for larger lists, the initial build time and memory usage will increase significantly.
Additionally, every time the selected item changes, you’re re-generating the entire listData array. While Flutter’s diffing algorithm will optimize some of this, it still means more work for the framework compared to updating individual tiles.
Fixing the ExpansionTile Key Issue
You mentioned that setting a unique key breaks animations, but that’s only because you’re using an unstable key. The fix is simple: use a stable, item-specific key instead of a timestamp. For example, use your item’s unique id:
return ListView.builder( padding: EdgeInsets.all(10), itemCount: data.length, itemBuilder: ((context, index) { final item = data[index]; return ExpansionTile( key: PageStorageKey(item.id.toString()), // Stable key tied to the item initiallyExpanded: selectedItem == item.id.toString(), onExpansionChanged: ((value) => ref.read(selectedProductListItem.notifier).state = value ? item.id.toString() : null), tilePadding: EdgeInsets.all(10), leading: const Icon(Icons.account_box), title: Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)), childrenPadding: EdgeInsets.all(10), expandedAlignment: Alignment.center, children: [ Text(item.description), const SizedBox(height: 10), ElevatedButton(onPressed: () {}, child: const Text('Apply Now')), ], ); }), );
With this change:
- Flutter can track each
ExpansionTile’s state correctly, so animations will work as expected. - When the selected item changes, Flutter only updates the expanded state of the relevant tiles (instead of rebuilding all of them).
- You retain the lazy-loading benefit of
ListView.builder, keeping performance high even for large datasets.
Final Selection Guidance
Based on performance and scalability:
- For large datasets (10+ items): Use the corrected
ListView.builder + ExpansionTileapproach. It’s lazy-loaded, minimizes unnecessary rebuilds, and scales far better thanExpansionPanelList. - For small datasets (10 items or fewer):
ExpansionPanelListis acceptable, but you can optimize it by memoizing thelistDataarray (e.g., usingmemoizedor Riverpod’scomputedprovider) to avoid re-generating it on every build.
The corrected ExpansionTile approach is almost always the better choice unless you have a very small list and prefer the default styling of ExpansionPanelList (though even then, you can customize ExpansionTile to match your needs).
内容的提问来源于stack exchange,提问作者Sasa

