When I update the state it seems that the whole composable recomposes and lazy column seems to be created anew (it blinks and shows new content from the very beginning) At the same time layout inspector shows that recompositions for lazycolumn are skipped. Composable is added through XML in fragment. Full minimal app can be found here simple test app which reproduces that issue
搜查委员会
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun 搜查委员会(
state: State<SearchCityState>, onTextChanged: (String) -> Unit, onItemSelected: (String) -> Unit
) {
Box(
Modifier
.background(Color.White)
.fillMaxSize(), contentAlignment = Alignment.TopCenter
) {
Column(
Modifier
.fillMaxHeight()
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
.widthIn(max = 640.dp)
) {
Row(
Modifier
.height(46.dp)
.background(
color = Color.White, shape = RoundedCornerShape(4.dp)
), verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = (Modifier.width(8.dp)))
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = "Search city input box",
Modifier
.size(40.dp)
.alpha(if (state.value.query.isEmpty()) 0.5f else 1f),
tint = Color.Black
)
Spacer(modifier = (Modifier.width(8.dp)))
val textStyle = TextStyle(
color = Color.Black, textAlign = TextAlign.Start
)
BasicTextField(
value = state.value.query,
decorationBox = { innerTextField ->
if (state.value.query.isEmpty()) {
Text(
text = "Search",
style = textStyle,
modifier = Modifier.alpha(0.5f),
maxLines = 1,
)
}
innerTextField()
},
textStyle = textStyle,
maxLines = 1,
singleLine = true,
onValueChange = {
onTextChanged(it)
},
modifier = Modifier.weight(1f),
)
}
LazyColumn(
Modifier
.fillMaxWidth()
.weight(1f),
contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
) {
itemsIndexed(
state.value.searchCities.fromServer,
) { index, city ->
val bgType by remember {
derivedStateOf {
getBgType(index, state.value.searchCities.fromServer.size)
}
}
CityItem(modifier = Modifier.animateItemPlacement(),
value = city,
bgType = bgType,
onClick = { onItemSelected(it) })
}
}
}
}
}
private fun getBgType(index: Int, size: Int): BgType {
return when {
size == 1 -> BgType.SINGLE
index == 0 && size > 1 -> BgType.TOP
index == size - 1 && size > 0 -> BgType.BOTTOM
else -> BgType.MIDDLE
}
}
enum class BgType { TOP, MIDDLE, BOTTOM, SINGLE }
@Composable
private fun CityItem(
modifier: Modifier, value: String, bgType: BgType, onClick: (String) -> Unit
) {
val textStyle = TextStyle(
color = Color.Black, textAlign = TextAlign.Start
)
Box(
modifier
.height(46.dp)
.fillMaxWidth()
.background(Color.White)
.clickable { onClick(value) }, contentAlignment = Alignment.CenterStart
) {
Text(
text = value, style = textStyle, modifier = Modifier.padding(start = 16.dp, end = 16.dp)
)
}
}
ViewModel
@HiltViewModel
class SearchCityViewModel @Inject constructor(
private val serverUseCase: 2. 搜索和救援,
) : ViewModel() {
private var queryState = MutableStateFlow("")
private val _state = MutableStateFlow(SearchCityState())
val state: StateFlow<SearchCityState> = _state.asStateFlow()
private var job: Job? = null
init {
viewModelScope.launch {
queryState.debounce(350L).collect {
job?.cancel()
job = viewModelScope.launch {
serverUseCase.invoke(it).collect {
_state.update { state -> state.copy(searchCities = it) }
}
}
}
}
}
fun searchCities(query: String) {
_state.update { state -> state.copy(query = query) }
queryState.value = query
}
}
2. 搜索和救援
class 2. 搜索和救援 @Inject constructor(
) {
suspend fun invoke(query: String): Flow<SearchCity> {
val recents = cities.take(5)
val flow = callbackFlow<SearchCity> {
trySend(SearchCity(recent = recents))
delay(300L)
val c = if (query.isEmpty()) {
cities
} else {
cities.filter { it.lowercase().contains(query.lowercase()) }
}
trySend(
SearchCity(
recent = recents,
fromServer = c)
)
awaitClose()
}
return flow
}
val cities = listOf(
"New York",
"Los Angeles",
"Chicago",
"Houston",
"Phoenix",
"Philadelphia",
"San Antonio",
"San Diego",
"Dallas",
"San Jose",
"Austin",
"Jacksonville",
"San Francisco",
"Indianapolis",
"Columbus",
"Fort Worth",
"Charlotte",
"Seattle",
"Denver",
"Washington, D.C."
)
}