布局進(jìn)階:LazyListState控制與性能優(yōu)化實(shí)戰(zhàn))
1. 項(xiàng)目概述從“能用”到“好用”的滾動(dòng)體驗(yàn)在移動(dòng)應(yīng)用開發(fā)中滾動(dòng)布局是用戶交互的基石。無論是瀏覽社交動(dòng)態(tài)、查看商品列表還是閱讀長篇文章流暢、自然的滾動(dòng)體驗(yàn)直接決定了用戶對(duì)應(yīng)用的第一印象和留存意愿。Jetpack Compose作為現(xiàn)代Android UI開發(fā)的聲明式框架其滾動(dòng)系統(tǒng)的設(shè)計(jì)哲學(xué)與傳統(tǒng)的View體系截然不同。很多開發(fā)者初次接觸Compose的滾動(dòng)時(shí)常常會(huì)陷入一個(gè)誤區(qū)認(rèn)為只要把內(nèi)容放進(jìn)LazyColumn或LazyRow滾動(dòng)功能就“自動(dòng)”實(shí)現(xiàn)了。這沒錯(cuò)但僅僅是“能用”的層面。當(dāng)我們需要實(shí)現(xiàn)復(fù)雜的交互效果如吸頂標(biāo)題、視差滾動(dòng)、智能預(yù)加載或者僅僅是解決一個(gè)列表在快速滑動(dòng)時(shí)的微小卡頓時(shí)才會(huì)發(fā)現(xiàn)滾動(dòng)布局的“好用”背后藏著大量的細(xì)節(jié)與門道。這個(gè)系列文章我們聚焦于Compose的滾動(dòng)布局而今天這一篇我想深入聊聊那些在官方文檔里可能一筆帶過但在實(shí)際項(xiàng)目中卻至關(guān)重要的“高級(jí)”話題。我們將超越基礎(chǔ)的LazyColumn使用探討如何精準(zhǔn)控制滾動(dòng)狀態(tài)、實(shí)現(xiàn)復(fù)雜的聯(lián)動(dòng)效果、優(yōu)化性能以避免掉幀以及處理那些令人頭疼的邊緣情況。如果你已經(jīng)熟悉了Compose滾動(dòng)的基礎(chǔ)但渴望讓你的列表滾動(dòng)得像原生系統(tǒng)應(yīng)用一樣絲滑且功能強(qiáng)大那么接下來的內(nèi)容正是為你準(zhǔn)備的。無論你是正在優(yōu)化現(xiàn)有項(xiàng)目的性能還是設(shè)計(jì)一個(gè)全新的、交互豐富的列表界面這里分享的經(jīng)驗(yàn)和代碼都可能成為你的“解藥”。2. 核心設(shè)計(jì)思路狀態(tài)驅(qū)動(dòng)與精細(xì)控制2.1 理解LazyListState滾動(dòng)的“大腦”在Compose中滾動(dòng)不是一種被動(dòng)的事件而是一個(gè)可以被觀察和主動(dòng)控制的狀態(tài)。LazyColumn和LazyRow的核心是LazyListState。這個(gè)狀態(tài)對(duì)象是連接UI與滾動(dòng)行為的橋梁它內(nèi)部維護(hù)著當(dāng)前滾動(dòng)的位置、首個(gè)可見項(xiàng)的信息、布局信息等關(guān)鍵數(shù)據(jù)。// 創(chuàng)建一個(gè)可被共享和觀察的滾動(dòng)狀態(tài) val listState rememberLazyListState() // 在LazyColumn中使用它 LazyColumn(state listState) { // ... items }為什么狀態(tài)如此重要因?yàn)樗沟谩绊憫?yīng)式”滾動(dòng)成為可能。我們可以基于listState來驅(qū)動(dòng)UI的變化。例如我們可以監(jiān)聽listState.firstVisibleItemIndex首個(gè)可見項(xiàng)的索引來實(shí)現(xiàn)一個(gè)隨著滾動(dòng)逐漸顯示或隱藏的頂部應(yīng)用欄TopAppBar。val showAppBar by remember { derivedStateOf { listState.firstVisibleItemIndex 0 || listState.firstVisibleItemScrollOffset 0 } } TopAppBar( modifier Modifier.alpha(if (showAppBar) 1f else 0f), // ... )這里的關(guān)鍵是derivedStateOf。它用于創(chuàng)建一個(gè)依賴于其他狀態(tài)這里是listState的計(jì)算狀態(tài)。Compose會(huì)智能地只在listState.firstVisibleItemIndex或listState.firstVisibleItemScrollOffset發(fā)生變化時(shí)重新計(jì)算showAppBar而不是在每一幀都計(jì)算這是性能優(yōu)化的關(guān)鍵點(diǎn)。注意直接在一個(gè)非derivedStateOf或remember包裹的代碼塊中讀取listState的屬性可能會(huì)導(dǎo)致不必要的重組Recomposition。因?yàn)閘istState的屬性變化會(huì)觸發(fā)讀取它的Composable函數(shù)重組。使用derivedStateOf可以將多個(gè)狀態(tài)變化合并為一個(gè)輸出信號(hào)減少重組次數(shù)。2.2 滾動(dòng)控制的三種模式程序化滾動(dòng)的藝術(shù)除了響應(yīng)狀態(tài)我們經(jīng)常需要主動(dòng)控制滾動(dòng)比如點(diǎn)擊按鈕跳轉(zhuǎn)到指定項(xiàng)或者加載更多數(shù)據(jù)后自動(dòng)滾動(dòng)到新內(nèi)容的位置。LazyListState提供了三種主要的程序化滾動(dòng)方法理解它們的區(qū)別至關(guān)重要animateScrollToItem(index: Int): 這是最常用、體驗(yàn)最好的方法。它會(huì)執(zhí)行一個(gè)平滑的動(dòng)畫滾動(dòng)到指定索引的項(xiàng)。適用于用戶觸發(fā)的跳轉(zhuǎn)如點(diǎn)擊目錄跳轉(zhuǎn)到章節(jié)。scope.launch { listState.animateScrollToItem(index 50) }scrollToItem(index: Int): 立即跳轉(zhuǎn)到指定索引的項(xiàng)沒有動(dòng)畫。適用于需要在UI更新后瞬間定位的場(chǎng)景比如在數(shù)據(jù)刷新后快速回到頂部但通常結(jié)合動(dòng)畫使用體驗(yàn)更佳。scope.launch { listState.scrollToItem(index 0) }animateScrollBy(value: Float): 相對(duì)當(dāng)前滾動(dòng)位置平滑地滾動(dòng)一段距離單位為像素Dp需要轉(zhuǎn)換。適用于實(shí)現(xiàn)自定義的滾動(dòng)手勢(shì)或微調(diào)。val density LocalDensity.current scope.launch { listState.animateScrollBy(with(density) { 100.dp.toPx() }) }實(shí)操心得絕大多數(shù)情況下優(yōu)先使用animateScrollToItem。它不僅提供了流暢的視覺反饋其內(nèi)部實(shí)現(xiàn)還考慮了中斷處理——如果用戶在動(dòng)畫過程中再次滑動(dòng)新的手勢(shì)會(huì)平滑地接管滾動(dòng)避免了生硬的沖突。而scrollToItem的立即性有時(shí)會(huì)帶來突兀的視覺跳躍。3. 高級(jí)交互實(shí)現(xiàn)超越基礎(chǔ)列表3.1 實(shí)現(xiàn)吸頂效果Sticky Header吸頂效果是許多應(yīng)用列表的標(biāo)配如通訊錄按字母分組時(shí)字母標(biāo)題會(huì)停留在頂部。Compose本身沒有提供原生的吸頂組件但我們可以利用LazyListState和item的stickyHeader參數(shù)結(jié)合自定義布局來實(shí)現(xiàn)。一種常見且高效的實(shí)現(xiàn)思路是在LazyColumn的items或itemsIndexed函數(shù)中為需要吸頂?shù)捻?xiàng)使用stickyHeader。但更靈活的方式是結(jié)合LazyListState計(jì)算出來的偏移量手動(dòng)控制一個(gè)始終位于列表頂部的Header的顯示與位置。下面是一個(gè)簡(jiǎn)化的核心邏輯示例展示如何根據(jù)滾動(dòng)狀態(tài)動(dòng)態(tài)調(diào)整一個(gè)吸頂標(biāo)題的透明度與位置Composable fun StickyHeaderExample() { val listState rememberLazyListState() val scope rememberCoroutineScope() // 假設(shè)我們有一個(gè)分組列表每個(gè)組有一個(gè)標(biāo)題 val groupedItems remember { /* ... 你的分組數(shù)據(jù) ... */ } Box { LazyColumn(state listState) { groupedItems.forEachIndexed { sectionIndex, (header, items) - // 為每個(gè)分組插入一個(gè)可被檢測(cè)的Header Item item(key header_$sectionIndex) { // 這是一個(gè)普通的Header用于在列表中占位 HeaderContent(title header) } items(items) { item - ItemContent(item item) } } } // 懸浮在頂部的吸頂Header val currentSection // 根據(jù)listState.firstVisibleItemIndex計(jì)算當(dāng)前所在的section val firstVisibleItemInfo listState.layoutInfo.visibleItemsInfo.firstOrNull() val offset if (/* 判斷下一個(gè)Header正在頂上來 */) { // 計(jì)算下一個(gè)Header將當(dāng)前Header頂走的偏移量 val nextHeaderTop // ... 計(jì)算邏輯 nextHeaderTop.coerceAtMost(0) } else { 0 } TopAppBar( modifier Modifier .offset(y offset.dp) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)), title { Text(text currentSection.header) } ) } }注意精確計(jì)算吸頂Header的偏移量需要獲取到下一個(gè)Header項(xiàng)在布局中的位置信息LazyListLayoutInfo。這部分邏輯相對(duì)復(fù)雜需要仔細(xì)處理LazyListState.layoutInfo.visibleItemsInfo。一個(gè)常見的“坑”是忘記處理快速滑動(dòng)時(shí)布局信息更新的延遲導(dǎo)致吸頂Header出現(xiàn)閃爍或位置錯(cuò)誤。建議將計(jì)算邏輯封裝到一個(gè)穩(wěn)定的remember或ViewModel中并做好防抖處理。3.2 視差滾動(dòng)效果Parallax Scrolling視差滾動(dòng)通過讓背景和前景以不同速度滾動(dòng)創(chuàng)造出深度的錯(cuò)覺常用于精美的詳情頁或橫幅。在Compose中我們可以通過修改不同層級(jí)的Composable的滾動(dòng)速度來實(shí)現(xiàn)。核心原理是利用GraphicsLayer的translationY或translationX屬性并使其變化速度與LazyListState的滾動(dòng)偏移量成比例。val listState rememberLazyListState() val imageHeight 200.dp Box { // 背景層圖片滾動(dòng)速度較慢例如0.5倍速 Image( painter painterResource(id R.drawable.parallax_bg), contentDescription null, contentScale ContentScale.Crop, modifier Modifier .fillMaxWidth() .height(imageHeight * 2) // 圖片高度設(shè)為顯示區(qū)域的兩倍為滾動(dòng)留出空間 .graphicsLayer { // 關(guān)鍵translationY的變化量是滾動(dòng)偏移量的一半負(fù)號(hào)表示反向移動(dòng) translationY listState.firstVisibleItemScrollOffset * 0.5f } ) // 前景層列表正常速度滾動(dòng) LazyColumn(state listState) { item { Spacer(modifier Modifier.height(imageHeight)) } items(50) { index - Card(modifier Modifier.padding(16.dp)) { // ... 列表項(xiàng)內(nèi)容 } } } }實(shí)操心得計(jì)算translationY時(shí)通常使用firstVisibleItemScrollOffset。這個(gè)值是第一個(gè)可見項(xiàng)頂部被滾出屏幕外的像素?cái)?shù)。為了讓背景圖片看起來是“附著”在內(nèi)容上但移動(dòng)更慢我們讓它的translationY以更小的系數(shù)如0.5f跟隨這個(gè)偏移量變化。同時(shí)需要確保背景圖片的原始高度大于其容器高度這樣在滾動(dòng)時(shí)才有移動(dòng)的空間否則會(huì)露出空白。4. 性能優(yōu)化與問題排查4.1 項(xiàng)的重用與鍵Key的妙用LazyColumn的性能優(yōu)勢(shì)來自于它只組合Compose和布局Layout當(dāng)前可見項(xiàng)及少量緩沖項(xiàng)。當(dāng)滾動(dòng)時(shí)離開屏幕的項(xiàng)會(huì)被放入復(fù)用池新的項(xiàng)會(huì)從池中取出并復(fù)用。為了確保復(fù)用正確且狀態(tài)不被意外保留為每個(gè)項(xiàng)提供一個(gè)穩(wěn)定且唯一的key至關(guān)重要。// 錯(cuò)誤示范使用可能不穩(wěn)定的索引作為key items(items myList) { item - MyItem(item) } // 正確示范使用項(xiàng)數(shù)據(jù)中的唯一標(biāo)識(shí)符 items( items myList, key { item - item.id } // 假設(shè)item有一個(gè)唯一id ) { item - MyItem(item) }如果沒有提供keyCompose會(huì)默認(rèn)使用項(xiàng)在列表中的索引作為key。這會(huì)導(dǎo)致一個(gè)問題如果在列表中間插入或刪除項(xiàng)其后所有項(xiàng)的索引都變了Compose會(huì)認(rèn)為它們都是“新項(xiàng)”導(dǎo)致不必要的重組和狀態(tài)丟失比如一個(gè)可展開項(xiàng)在數(shù)據(jù)更新后自動(dòng)閉合了。提供一個(gè)基于數(shù)據(jù)本身的穩(wěn)定key可以保證項(xiàng)的身份在數(shù)據(jù)變化時(shí)保持不變從而正確復(fù)用并保持其內(nèi)部狀態(tài)。4.2 避免在項(xiàng)內(nèi)容中執(zhí)行耗時(shí)操作LazyColumn的item或items的lambda作用域內(nèi)應(yīng)該只包含UI描述和簡(jiǎn)單的狀態(tài)讀取。任何可能耗時(shí)的操作如網(wǎng)絡(luò)請(qǐng)求、大型數(shù)據(jù)轉(zhuǎn)換、復(fù)雜計(jì)算或IO操作都必須在進(jìn)入這個(gè)作用域之前完成或者使用LaunchedEffect、remember等副作用API在后臺(tái)執(zhí)行。// 錯(cuò)誤示范在項(xiàng)內(nèi)容中直接進(jìn)行耗時(shí)數(shù)據(jù)轉(zhuǎn)換 items(items rawDataList, key { it.id }) { rawData - val processedData expensiveDataTransformation(rawData) // 阻塞UI線程 MyItem(processedData) } // 正確示范預(yù)先處理數(shù)據(jù)或使用衍生狀態(tài) val processedList by remember(rawDataList) { derivedStateOf { rawDataList.map { expensiveDataTransformation(it) } } } // 或者在ViewModel中處理 items(items viewModel.processedList, key { it.id }) { item - MyItem(item) }如果每個(gè)項(xiàng)都需要獨(dú)立加載圖片應(yīng)使用像Coil或Glide這樣的異步圖片加載庫它們內(nèi)部會(huì)處理好后臺(tái)加載和緩存。4.3 處理快速滑動(dòng)時(shí)的空白與卡頓有時(shí)在極快速滑動(dòng)時(shí)可能會(huì)短暫看到空白區(qū)域或感到卡頓。這通常有幾個(gè)原因和解決方案緩沖不足LazyColumn默認(rèn)會(huì)在可見區(qū)域前后各預(yù)留一個(gè)項(xiàng)作為緩沖。對(duì)于高度不固定的復(fù)雜項(xiàng)可以適當(dāng)增加contentPadding或通過LazyListState的配置來增大緩沖區(qū)域給Compose更多時(shí)間提前準(zhǔn)備即將進(jìn)入視野的項(xiàng)。LazyColumn( state listState, contentPadding PaddingValues(vertical 8.dp), // 上下增加內(nèi)邊距相當(dāng)于擴(kuò)展了緩沖區(qū)域 flingBehavior rememberLazyListFlingBehavior(listState) // 使用自定義的fling行為 ) { ... }項(xiàng)的高度變化如果項(xiàng)的高度在加載數(shù)據(jù)后發(fā)生變化如圖片加載完成會(huì)導(dǎo)致列表在滾動(dòng)過程中不斷重新計(jì)算布局造成跳動(dòng)。盡量為項(xiàng)指定固定高度或使用SubcomposeLayout等高級(jí)布局先占位。對(duì)于圖片使用Modifier.aspectRatio()或固定尺寸。過度組合Over-composition檢查項(xiàng)內(nèi)容中是否有導(dǎo)致不必要重組的代碼。使用Modifier.drawWithCache、LaunchedEffect等減少重組范圍。對(duì)穩(wěn)定的參數(shù)使用Stable注解或immutable的數(shù)據(jù)類。4.4 常見問題排查速查表問題現(xiàn)象可能原因排查與解決思路滾動(dòng)時(shí)項(xiàng)的狀態(tài)丟失如輸入框內(nèi)容清空未正確設(shè)置key或key不穩(wěn)定。為items()提供基于數(shù)據(jù)唯一ID的穩(wěn)定key。快速滑動(dòng)時(shí)出現(xiàn)空白緩沖不足或項(xiàng)內(nèi)容組合太慢。1. 增加contentPadding。2. 優(yōu)化項(xiàng)內(nèi)容的組合性能避免耗時(shí)操作。3. 考慮使用Placeholder占位符庫。滾動(dòng)不跟手有延遲感UI線程被阻塞。1. 使用Android Studio的Profiler工具檢查主線程。2. 確保所有耗時(shí)操作都在后臺(tái)協(xié)程執(zhí)行。3. 檢查是否有在組合階段進(jìn)行網(wǎng)絡(luò)請(qǐng)求或數(shù)據(jù)庫查詢。吸頂Header位置計(jì)算錯(cuò)誤或閃爍滾動(dòng)狀態(tài)監(jiān)聽與布局更新不同步。1. 使用derivedStateOf合并滾動(dòng)狀態(tài)計(jì)算減少重組。2. 在計(jì)算偏移量時(shí)考慮使用LazyListState.layoutInfo的visibleItemsInfo并處理好邊界情況。3. 為吸頂Header的顯示/隱藏添加交叉淡化動(dòng)畫以減少突兀感。程序化滾動(dòng)如scrollToItem無效1. 在組合完成前調(diào)用。2. 索引超出范圍。3. 協(xié)程作用域CoroutineScope未正確使用。1. 確保在LaunchedEffect或點(diǎn)擊事件回調(diào)等組合完成后的副作用中調(diào)用。2. 檢查目標(biāo)索引是否有效0 index itemCount。3. 使用rememberCoroutineScope獲取組合作用域。5. 實(shí)戰(zhàn)構(gòu)建一個(gè)帶智能預(yù)加載的圖片流讓我們綜合運(yùn)用以上知識(shí)構(gòu)建一個(gè)類似社交媒體的圖片流它需要1) 流暢滾動(dòng)2) 圖片視差效果3) 滾動(dòng)到底部自動(dòng)加載更多4) 智能預(yù)加載提前加載即將進(jìn)入視野的圖片。步驟1基礎(chǔ)結(jié)構(gòu)與狀態(tài)管理首先我們定義ViewModel來管理圖片列表數(shù)據(jù)和加載狀態(tài)。class ImageFeedViewModel : ViewModel() { private val _imageItems mutableStateListOfImageItem() val imageItems: ListImageItem _imageItems private val _isLoading mutableStateOf(false) val isLoading: Boolean get() _isLoading.value private var currentPage 0 init { loadMoreImages() } fun loadMoreImages() { if (_isLoading.value) return viewModelScope.launch { _isLoading.value true val newItems imageRepository.loadImages(page currentPage) // 模擬網(wǎng)絡(luò)請(qǐng)求 _imageItems.addAll(newItems) _isLoading.value false } } }步驟2主界面與滾動(dòng)監(jiān)聽在Composable中我們?cè)O(shè)置列表并監(jiān)聽滾動(dòng)位置以實(shí)現(xiàn)加載更多和預(yù)加載。Composable fun ImageFeedScreen(viewModel: ImageFeedViewModel viewModel()) { val listState rememberLazyListState() val scope rememberCoroutineScope() // 監(jiān)聽是否接近底部用于觸發(fā)加載更多 val isAtBottom by remember { derivedStateOf { val layoutInfo listState.layoutInfo val totalItems layoutInfo.totalItemsCount val lastVisibleItem layoutInfo.visibleItemsInfo.lastOrNull() // 如果最后一個(gè)可見項(xiàng)是列表的倒數(shù)第二項(xiàng)則觸發(fā)加載 lastVisibleItem?.index ! null lastVisibleItem.index totalItems - 3 } } // 當(dāng)接近底部時(shí)觸發(fā)加載更多 LaunchedEffect(isAtBottom) { if (isAtBottom !viewModel.isLoading) { viewModel.loadMoreImages() } } Box(modifier Modifier.fillMaxSize()) { LazyColumn( state listState, modifier Modifier.fillMaxSize(), contentPadding PaddingValues(horizontal 8.dp, vertical 4.dp) ) { items( items viewModel.imageItems, key { it.id } // 關(guān)鍵使用唯一ID作為key ) { imageItem - ImageFeedItem( item imageItem, listState listState // 傳入狀態(tài)用于視差計(jì)算 ) } // 加載更多指示器 if (viewModel.isLoading) { item { Box( modifier Modifier .fillMaxWidth() .padding(16.dp), contentAlignment Alignment.Center ) { CircularProgressIndicator() } } } } } }步驟3實(shí)現(xiàn)帶視差效果的圖片項(xiàng)每個(gè)圖片項(xiàng)自身實(shí)現(xiàn)一個(gè)簡(jiǎn)單的視差效果并且集成智能預(yù)加載。Composable fun ImageFeedItem(item: ImageItem, listState: LazyListState) { val density LocalDensity.current // 簡(jiǎn)單的視差效果根據(jù)該項(xiàng)在列表中的大致位置微調(diào)圖片的垂直偏移 // 這是一個(gè)簡(jiǎn)化示例更精確的可以計(jì)算該項(xiàng)相對(duì)于視口的精確位置 val itemIndex // ... 需要通過上下文或參數(shù)傳遞該項(xiàng)的索引實(shí)際項(xiàng)目中需計(jì)算 val parallaxOffset by remember(itemIndex) { derivedStateOf { // 假設(shè)我們希望前幾項(xiàng)有較強(qiáng)的視差 val baseOffset listState.firstVisibleItemScrollOffset.toFloat() if (itemIndex 5) -baseOffset * 0.2f else 0f } } Card( modifier Modifier .fillMaxWidth() .padding(vertical 8.dp) .graphicsLayer { translationY parallaxOffset }, elevation CardDefaults.cardElevation(defaultElevation 4.dp) ) { Column { // 圖片部分使用AsyncImage預(yù)加載 AsyncImage( model ImageRequest.Builder(LocalContext.current) .data(item.imageUrl) .crossfade(true) // 關(guān)鍵設(shè)置預(yù)加載參數(shù)。Coil支持根據(jù)滾動(dòng)狀態(tài)預(yù)加載。 // 在實(shí)際項(xiàng)目中可以結(jié)合listState計(jì)算哪些項(xiàng)即將進(jìn)入視野然后預(yù)加載。 .build(), contentDescription item.description, contentScale ContentScale.Crop, modifier Modifier .fillMaxWidth() .aspectRatio(16f / 9f) ) // ... 文字描述等其他內(nèi)容 } } }步驟4集成更智能的預(yù)加載上面的AsyncImage使用了Coil的基礎(chǔ)功能。為了實(shí)現(xiàn)更激進(jìn)的預(yù)加載例如預(yù)加載當(dāng)前可見項(xiàng)前后各5項(xiàng)的圖片我們需要一個(gè)更全局的機(jī)制。這通常可以通過在ViewModel中維護(hù)一個(gè)預(yù)加載隊(duì)列并根據(jù)listState.layoutInfo.visibleItemsInfo的索引范圍來觸發(fā)預(yù)加載請(qǐng)求來實(shí)現(xiàn)。由于涉及復(fù)雜的圖像庫集成這里提供思路在LaunchedEffect中監(jiān)聽listState的變化計(jì)算出一個(gè)預(yù)加載的索引范圍然后通知ImageLoader如Coil的ImageLoader去預(yù)加載這些URL。踩坑記錄與最終心得 在實(shí)現(xiàn)這個(gè)圖片流的過程中最大的挑戰(zhàn)是平衡流暢度與內(nèi)存占用。無限制的預(yù)加載會(huì)導(dǎo)致內(nèi)存暴漲特別是高清圖片。我們的解決方案是1) 設(shè)置預(yù)加載的窗口大小如前后各3項(xiàng)2) 根據(jù)網(wǎng)絡(luò)條件和設(shè)備內(nèi)存動(dòng)態(tài)調(diào)整預(yù)加載策略3) 使用圖片庫的磁盤緩存和內(nèi)存緩存有效管理資源。另一個(gè)細(xì)節(jié)是視差效果的計(jì)算不宜過于復(fù)雜避免在每一幀滾動(dòng)時(shí)都進(jìn)行大量計(jì)算使用derivedStateOf并依賴盡可能少的狀態(tài)是關(guān)鍵。最后永遠(yuǎn)記得為列表項(xiàng)設(shè)置正確的key這是保證所有高級(jí)交互和狀態(tài)保持正確的基石。經(jīng)過這些優(yōu)化最終的列表即使在快速滑動(dòng)和大量圖片加載下也能保持接近60fps的流暢體驗(yàn)。