It seems that when adding items to a LazyColumn, all elements are being recomposed. The inner elements are skipped, but I’m curious if this recomposition behavior is correct. If it’s not, what would be the proper way to improve it?
This photo is a screenshot from the Layout Inspector, showing when 4 items were added to the LazyColumn through a FAB.
In HomeRoute, a ViewModel is created and the uiState is remembered. This uiState is passed to HomeScreen, where the data is stored in an ImmutableList. On HomeScreen, a composable function called InnerViewList is invoked, which creates a LazyColumn internally.
I’m unsure if the recomposition is functioning correctly. In the Layout Inspector, it shows that each element is being recomposed, while only the inner text is skipped. Is this the correct behavior?
Here is my code
data class InnerView(
val id: Int,
val title: String,
val type: InnerViewType,
val createdAt: ZonedDateTime,
)
@Stable
sealed interface HomeUiState {
@Immutable
data object Loading : HomeUiState
@Immutable
data class UiState(
val innerViews: ImmutableList<InnerView> = persistentListOf(),
) : HomeUiState
}
@HiltViewModel
class HomeViewModel @Inject constructor(
getInnerViewUseCase: GetInnerViewUseCase,
private val addInnerViewUseCase: AddInnerViewUseCase
) : ViewModel() {
private val _homeUiState =
MutableStateFlow<HomeUiState>(HomeUiState.Loading)
val homeUiState = _homeUiState.asStateFlow()
init {
getInnerViewUseCase()
.onEach { innerViews ->
_homeUiState.value = HomeUiState.UiState(
innerViews = innerViews.toPersistentList()
)
println(_homeUiState.value)
}.launchIn(viewModelScope)
}
fun addInnerView() {
viewModelScope.launch {
addInnerViewUseCase("innerView title", InnerViewType.YEAR)
}
}
}
@Composable
internal fun HomeRoute(
padding: PaddingValues,
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onInnerViewClick: (Int) -> Unit,
viewModel: HomeViewModel = hiltViewModel(),
) {
val homeUiState by viewModel.homeUiState.collectAsStateWithLifecycle()
LaunchedEffect(true) {
viewModel.errorFlow.collectLatest { throwable -> onShowErrorSnackBar(throwable) }
}
HomeScreen(
homeUiState = homeUiState,
padding = padding,
onInnerViewClick = onInnerViewClick,
addInnerView = { viewModel.addInnerView() }
)
}
@Composable
private fun HomeScreen(
homeUiState: HomeUiState,
padding: PaddingValues,
onInnerViewClick: (Int) -> Unit,
addInnerView: () -> Unit
) {
Box(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
...
Box(
modifier = Modifier
.padding(top = appBarSize)
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
when (val uiState = homeUiState) {
is HomeUiState.Loading -> Loading()
is HomeUiState.UiState -> {
InnerViewList(
homeUiState = uiState,
onInnerViewClick = onInnerViewClick
)
InnerViewFloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = Paddings.large, bottom = Paddings.large),
iconImageVector = Icons.Filled.Add,
text = stringResource(R.string.feature_home_innerview_create),
onClick = { addInnerView() }
)
}
}
}
}
}
@Composable
private fun InnerViewList(
homeUiState: HomeUiState.UiState,
onInnerViewClick: (Int) -> Unit,
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(Paddings.large),
verticalArrangement = Arrangement.spacedBy(Paddings.large)
) {
items(homeUiState.innerViews, key = { it.id }) { innerView ->
InnerViewContent(
innerView = innerView,
onInnerViewClick = onInnerViewClick
)
}
}
}