Jetpack Compose not recomposing composable on setValue - android

I'm trying to do a quick pagination example with Jetpack compose in where I load the first 6 items of the list, then I load another 6 and so on until the end of the list.
I'm aware of the error in the for loop that will cause an IndexOutOfBounds since is not well designed to iterate until the last element of the array
My problem is two, first the for loop one, I don't know how to take from 0 - 6 , 6 - 12 - 12 - list.size
Then my other problem is that every time I use setList it should recompose the LazyColumnForIndex and its not, causing only to render the first 6 items
What I'm doing wrong here ?
val longList = mutableListOf("Phone","Computer","TV","Glasses","Cup",
"Stereo","Music","Furniture","Air","Red","Blue","Yellow",
"White","Black","Pink","Games","Car","Motorbike")
#Composable
fun PaginationDemo() {
val maxItemsPerPage = 6
val list = mutableListOf<String>()
var (page,setValue) = remember { mutableStateOf(1) }
val (paginatedList,setList) = remember { mutableStateOf(getPaginatedList(page,maxItemsPerPage, list)) }
LazyColumnForIndexed(items = paginatedList ) { index, item ->
Text(text = item,style = TextStyle(fontSize = 24.sp))
if(paginatedList.lastIndex == index){
onActive(callback = {
setValue(page++)
setList(getPaginatedList(page,maxItemsPerPage,list))
})
}
}
}
private fun getPaginatedList(page:Int,maxItemsPerPage:Int,list:MutableList<String>): MutableList<String> {
val startIndex = maxItemsPerPage * page
for(item in 0 until startIndex){
list.add(longList[item])
}
return list
}

This seems to work.
#Composable
fun PaginationDemo() {
val maxItemsPerPage = 6
val list = mutableStateListOf<String>()
var (page,setValue) = remember { mutableStateOf(1) }
val (paginatedList,setList) = remember { mutableStateOf(getPaginatedList(page,maxItemsPerPage, list)) }
LazyColumnForIndexed(
items = paginatedList
) { index, item ->
Text(
text = item,
style = TextStyle(fontSize = 24.sp),
modifier = Modifier
.fillParentMaxWidth()
.height((ConfigurationAmbient.current.screenHeightDp / 3).dp)
.background(color = Color.Blue)
)
Divider(thickness = 2.dp)
Log.d("MainActivity", "lastIndex = ${paginatedList.lastIndex} vs index = $index")
if(paginatedList.lastIndex == index){
onActive(callback = {
setValue(++page)
setList(getPaginatedList(page,maxItemsPerPage,list))
})
}
}
}
private fun getPaginatedList(page:Int,maxItemsPerPage:Int,list:MutableList<String>): MutableList<String> {
val maxSize = longList.size
val startIndex = if (maxItemsPerPage * page >= maxSize) maxSize else maxItemsPerPage * page
list.clear()
for(item in 0 until startIndex){
list.add(longList[item])
}
return list
}

Related

Android compose how to paging with Columns inside HorizontalPager?

I want paginated with paging3 libaray in jetpack compose.
I can use the paging3 library on one column. but, i want to use it in multiple columns.
Like LazyColumn in HorizontalPager.
The model used for paging is the same.
ex.
Usermodel
data class UserModel(
val name:String,
val grade:Int
)
ViewModel
val userItemPager = _page.flatMapLatest {
Pager(PagingConfig(pageSize = 20)) {
UserItemPagingSource(repoSearchImpl, it)
}.flow
}.cachedIn(viewModelScope)
PagingSource
class UserItemPagingSource(
private val repo: RepoSearch,
private val page:Int
) : PagingSource<Int, UserModel>() {
private val STARTING_KEY: Int = 0
override fun getRefreshKey(state: PagingState<Int, UserModel>): Int {
return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2).coerceAtLeast(0)
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserModel> {
val position = params.key ?: 0
val response = repo.load(position)
val realItem = response.filter { it.grade == page }
return try {
LoadResult.Page(
data = realItem ,
prevKey = null,
nextKey = position + realItem.size
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
HorizontalPager
val userPagingItems= viewModel.userItemPager.collectAsLazyPagingItems()
HorizontalPager(
count = Grade.values().size,
state = pagerState,
modifier = Modifier.fillMaxWidth()
) { page ->
val reaItems = remember(page) {
derivedStateOf {
userPagingItems.itemSnapshotList.filter { it?.grade == page }
}
}
GridPage(
items = reaItems,
page = page,
)
}
Inside columns
#Composable
fun GridPage(
items : State<List<UserModel?>>,
page: Int,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
) {
items(items.value) { model ->
model?.let {
Text(text = "${model.name,model.grade}")
}
}
}
}
Divide tabs according to userModel grade(1,2,3 -> total three tap).
I want to do paging differently according to the grade.
The above code separates the tabs, but does not load.
another code
#Composable
fun GridPage(
items : LazyPagingItems<UserModel>,
page: Int,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
) {
items(items.count) { index ->
val model = items[index]
model?.let {
Text(text = "${model.name,model.grade}")
}
}
}
}
This code loads when I scroll, but all the grades are displayed in one tab, and I don't know how to divide the tabs according to the grades.
How can I classify and paginate according to tabs?

Jetpack compose Lazy Column item state does not change when state in viewModel changes

I have debugged the app and I saw that the data in UIState changes when I try to add or remove the item, especially the isAdded field. However, even though the isAdded changes, the AddableItem does not recompose. Additionally, when I try to sort items, or try to write a query THAT WILL NOT SEND ANY API REQUEST, JUST CHANGES THE STRING IN TEXTFIELD, the UI recomposes. So UI reacts to changes in UIState. I have searched for similar issues but cannot find anything. I believe that the framework must recompose when the pointer of the filed changes, however, it does not. Any idea why this happens or solve that?
This is the viewModel:
#HiltViewModel
class AddableItemScreenViewModel#Inject constructor(
val getAddableItemsUseCase: GetItems,
val getItemsFromRoomUseCase: GetRoomItems,
val updateItemCase: UpdateItem,
savedStateHandle: SavedStateHandle) : ViewModel() {
private val _uiState = mutableStateOf(UIState())
val uiState: State<UIState> = _uiState
private val _title = mutableStateOf("")
val title: State<String> = _title
private var getItemsJob: Job? = null
init {
savedStateHandle.get<String>(NavigationConstants.TITLE)?.let { title ->
_title.value = title
}
savedStateHandle.get<Int>(NavigationConstants.ID)?.let { id ->
getItems(id = id.toString())
}
}
fun onEvent(event: ItemEvent) {
when(event) {
is ItemEvent.UpdateEvent -> {
val modelToUpdate = UpdateModel(
id = event.source.id,
isAdded = event.source.isAdded,
name = event.source.name,
index = event.source.index
)
updateUseCase(modelToUpdate).launchIn(viewModelScope)
}
is ItemEvent.QueryChangeEvent -> {
_uiState.value = _uiState.value.copy(
searchQuery = event.newQuery
)
}
is ItemEvent.SortEvent -> {
val curSortType = _uiState.value.sortType
_uiState.value = _uiState.value.copy(
sortType = if(curSortType == SortType.AS_IT_IS)
SortType.ALPHA_NUMERIC
else
SortType.AS_IT_IS
)
}
}
}
private fun getItems(id: String) {
getItemsJob?.cancel()
getItemsJob = getItemsUseCase(id)
.combine(
getItemsFromRoomUseCase()
){ itemsApiResult, roomData ->
when (itemsApiResult) {
is Resource.Success -> {
val data = itemsApiResult.data.toMutableList()
// Look the api result, if the item is added on room, make it added, else make it not added. This ensures API call is done once and every state change happens because of room.
for(i in data.indices) {
val source = data[i]
val itemInRoomData = roomData.find { it.id == source.id }
data[i] = data[i].copy(
isAdded = itemInRoomData != null
)
}
_uiState.value = _uiState.value.copy(
data = data,
isLoading = false,
error = "",
)
}
is Resource.Error -> {
_uiState.value = UIState(
data = emptyList(),
isLoading = false,
error = itemsApiResult.message,
)
}
is Resource.Loading -> {
_uiState.value = UIState(
data = emptyList(),
isLoading = true,
error = "",
)
}
}
}.launchIn(viewModelScope)
}
}
This it the composable:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun AddableItemsScreen(
itemsViewModel: AddableItemScreenViewModel = hiltViewModel()
) {
val state = itemsViewModel.uiState.value
val controller = LocalNavigationManager.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val mainScrollState = rememberLazyListState()
val focusRequester = remember { FocusRequester() }
// Screen UI
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.BackgroundColor)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
focusManager.clearFocus()
},
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
state = mainScrollState,
) {
item {
WhiteSpacer(
whiteSpacePx = 200,
direction = SpacerDirections.VERTICAL
)
}
if (state.isLoading) {
item {
ProgressIndicator()
}
}
if (state.error.isNotEmpty() && state.error.isNotBlank()) {
item {
ErrorText()
}
}
if (state.data.isNotEmpty()) {
val data = if (state.sortType == SortType.ALPHA_NUMERIC)
state.data.sortedBy { it.name }
else
state.data
data.forEach { source ->
if((state.searchQuery.isEmpty() && state.searchQuery.isBlank()) ||
(source.name != null && source.name.contains(state.searchQuery, ignoreCase = true))) {
item {
AddableItem(
modifier = Modifier
.padding(
vertical = dimManager.heightPxToDp(20)
),
text = source.name ?: "",
isAdded = source.isAdded ?: false,
onItemPressed = {
controller.navigate(
Screens.ItemPreviewScreen.route +
"?title=${source.name}" +
"&id=${source.categoryId}" +
"&isAdded=${source.isAdded}"
)
},
onAddPressed = {
itemsViewModel.onEvent(ItemEvent.UpdateEvent(source))
}
)
}
}
}
}
}
Column(
modifier = Modifier
.align(Alignment.TopStart)
.background(
MaterialTheme.colors.BackgroundColor
),
) {
ItemsScreenAppBar(
title = itemsViewModel.title.value,
onSortPressed = {
itemsViewModel.onEvent(ItemEvent.SortEvent)
}
) {
controller.popBackStack()
}
SearchBar(
query = state.searchQuery,
focusRequester = focusRequester,
placeholder = itemsViewModel.title.value,
onDeletePressed = {
itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(""))
},
onValueChanged = {
itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(it))
},
onSearch = {
keyboardController!!.hide()
}
)
WhiteSpacer(
whiteSpacePx = 4,
direction = SpacerDirections.VERTICAL
)
}
}
}
And finally this is the UIState:
data class UIState(
val data: List<ItemModel> = emptyList(),
val isLoading: Boolean = false,
val error: String = "",
val searchQuery: String = "",
val sortType: SortType = SortType.AS_IT_IS,
)
#Parcelize
data class ItemModel (
val id: Int? = null,
var isAdded: Boolean? = null,
val name: String? = null,
val index: Int? = null,
#SerializedName("someSerializedNameForApi")
var id: Int? = null
): Parcelable
Finally, I have a similar issue with almost the same viewModel with the same UI structure. The UI contains an Add All button and when everything is added, it turns to Remove All. I also hold the state of the button in UIState for that screen. When I try to add all items or remove all items, the UI recomposes. But when I try to add or remove a single item, the recomposition does not happen as same as the published code above. Additionally, when I remove one item when everything is added on that screen, the state of the button does change but stops to react when I try to add more. I can also share that code if you people want. I still do not understand why the UI recomposes when I try to sort or try to add-remove all on both screens but does not recompose when the data changes, even though I change the pointer address of the list.
Thanks for any help.
I could not believe that the answer can be so simple but here are the solutions:
For the posted screen, I just changed _uiState.value = _uiState.value.copy(...) to _uiState.value = UIState(...copy and replace everything with old value...) as
_uiState.value = UIState(
data = data,
isLoading = false,
error = "",
searchQuery = _uiState.value.searchQuery,
sortType = _uiState.value.sortType
)
For the second screen, I was just double changing the isAdded value by sending the data directly without copying. As the api call changes the isAdded value again, and the read from room flow changes it again, the state were changed twice.
However, I still wonder why compose didn't recompose when I changed the memory location of data in UIState.

Get last visible item index in jetpack compose LazyColumn

I want to check if the list is scrolled to end of the list. How ever the lazyListState does not provide this property
Why do I need this? I want to show a FAB for "scrolling to end" of the list, and hide it if last item is already visible
(Note: It does, but it's internal
/**
* Non-observable way of getting the last visible item index.
*/
internal var lastVisibleItemIndexNonObservable: DataIndex = DataIndex(0)
no idea why)
val state = rememberLazyListState()
LazyColumn(
state = state,
modifier = modifier.fillMaxSize()
) {
// if(state.lastVisibleItem == logs.length - 1) ...
items(logs) { log ->
if (log.level in viewModel.getShownLogs()) {
LogItemScreen(log = log)
}
}
}
So, how can I check if my LazyColumn is scrolled to end of the dataset?
Here is a way for you to implement it:
Extension function to check if it is scrolled to the end:
fun LazyListState.isScrolledToTheEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
Example usage:
val listState = rememberLazyListState()
val listItems = (0..25).map { "Item$it" }
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
items(listItems) { item ->
Text(text = item, modifier = Modifier.padding(16.dp))
}
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) {
if (!listState.isScrolledToTheEnd()) {
ExtendedFloatingActionButton(
modifier = Modifier.padding(16.dp),
text = { Text(text = "Go to Bottom") },
onClick = { /* Scroll to the end */}
)
}
}
I am sharing my solution in case it helps anyone.
It provides the info needed to implement the use case of the question and also avoids infinite recompositions by following the recommendation of https://developer.android.com/jetpack/compose/lists#control-scroll-position.
Create these extension functions to calculate the info needed from the list state:
val LazyListState.isLastItemVisible: Boolean
get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
val LazyListState.isFirstItemVisible: Boolean
get() = firstVisibleItemIndex == 0
Create a simple data class to hold the information to collect:
data class ScrollContext(
val isTop: Boolean,
val isBottom: Boolean,
)
Create this remember composable to return the previous data class.
#Composable
fun rememberScrollContext(listState: LazyListState): ScrollContext {
val scrollContext by remember {
derivedStateOf {
ScrollContext(
isTop = listState.isFirstItemVisible,
isBottom = listState.isLastItemVisible
)
}
}
return scrollContext
}
Note that a derived state is used to avoid recompositions and improve performance.
The function needs the list state to make the calculations inside the derived state. Read the link I shared above.
Glue everything in your composable:
#Composable
fun CharactersList(
state: CharactersState,
loadNewPage: (offset: Int) -> Unit
) {
// Important to remember the state, we need it
val listState = rememberLazyListState()
Box {
LazyColumn(
state = listState,
) {
items(state.characters) { item ->
CharacterItem(item)
}
}
// We use our remember composable to get the scroll context
val scrollContext = rememberScrollContext(listState)
// We can do what we need, such as loading more items...
if (scrollContext.isBottom) {
loadNewPage(state.characters.size)
}
// ...or showing other elements like a text
AnimatedVisibility(scrollContext.isBottom) {
Text("You are in the bottom of the list")
}
// ...or a button to scroll up
AnimatedVisibility(!scrollContext.isTop) {
val coroutineScope = rememberCoroutineScope()
Button(
onClick = {
coroutineScope.launch {
// Animate scroll to the first item
listState.animateScrollToItem(index = 0)
}
},
) {
Icon(Icons.Rounded.ArrowUpward, contentDescription = "Go to top")
}
}
}
}
Cheers!
Starting from 1.4.0-alpha03 you can use LazyListState#canScrollForward to check if you can scroll forward or if you are at the end of the list.
val state = rememberLazyListState()
if (!state.canScrollForward){ /* ... */ }
Before you can use
the LazyListState#layoutInfo that contains information about the visible items. You can use it to retrieve information if the list is scrolled at the bottom.
Since you are reading the state you should use derivedStateOf to avoid redundant recompositions.
Something like:
val state = rememberLazyListState()
val isAtBottom by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
false
} else {
val lastVisibleItem = visibleItemsInfo.last()
val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
(lastVisibleItem.index + 1 == layoutInfo.totalItemsCount &&
lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
}
}
}
It's too late, but maybe it would be helpful to others.
seeing the above answers, The layoutInfo.visibleItemsInfo.lastIndex will cause recomposition many times, because it is composed of state.
So I recommend to use this statement like below with derivedState and itemKey in item(key = "lastIndexKey").
val isFirstItemFullyVisible = remember {
derivedStateOf {
listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
}
}
val isLastItemFullyVisible by remember {
derivedStateOf {
listState.layoutInfo
.visibleItemsInfo
.any { it.key == lastIndexKey }.let { _isLastIndexVisible ->
if(_isLastIndexVisible){
val layoutInfo = listState.layoutInfo
val lastItemInfo = layoutInfo.visibleItemsInfo.lastOrNull() ?: return#let false
return#let lastItemInfo.size+lastItemInfo.offset == layoutInfo.viewportEndOffset
}else{
return#let false
}
}
}
}
if (isFirstItemFullyVisible.value || isLastItemFullyVisible) {
// TODO
}
Current solution that I have found is:
LazyColumn(
state = state,
modifier = modifier.fillMaxSize()
) {
if ((logs.size - 1) - state.firstVisibleItemIndex == state.layoutInfo.visibleItemsInfo.size - 1) {
println("Last visible item is actually the last item")
// do something
}
items(logs) { log ->
if (log.level in viewModel.getShownLogs()) {
LogItemScreen(log = log)
}
}
}
The statement
lastDataIndex - state.firstVisibleItemIndex == state.layoutInfo.visibleItemsInfo.size - 1
guesses the last item by subtracting last index of dataset from first visible item and checking if it's equal to visible item count
Just wanted to build upon some of the other answers posted here.
#Tuan Chau mentioned in a comment that this will cause infinite compositions, here is something I tried using his idea to avoid this, and it seems to work ok. Open to ideas on how to make it better!
#Composable
fun InfiniteLoadingList(
modifier: Modifier,
items: List<Any>,
loadMore: () -> Unit,
rowContent: #Composable (Int, Any) -> Unit
) {
val listState = rememberLazyListState()
val firstVisibleIndex = remember { mutableStateOf(listState.firstVisibleItemIndex) }
LazyColumn(state = listState, modifier = modifier) {
itemsIndexed(items) { index, item ->
rowContent(index, item)
}
}
if (listState.shouldLoadMore(firstVisibleIndex)) {
loadMore()
}
}
Extension function:
fun LazyListState.shouldLoadMore(rememberedIndex: MutableState<Int>): Boolean {
val firstVisibleIndex = this.firstVisibleItemIndex
if (rememberedIndex.value != firstVisibleIndex) {
rememberedIndex.value = firstVisibleIndex
return layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
}
return false
}
Usage:
InfiniteLoadingList(
modifier = modifier,
items = listOfYourModel,
loadMore = { viewModel.populateMoreItems() },
) { index, item ->
val item = item as YourModel
// decorate your row
}
Try this:
val lazyColumnState = rememberLazyListState()
val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.lastIndex + state.firstVisibleItemIndex

How to make an endless list in Jetpack Compose

When make an xml endless list need create RecyclerView and add RecyclerViewOnScrollListener. How to do it in Jetpack Compose?
You can use androidx.ui.foundation.AdapterList for this.
It will only composes and lays out the currently visible items.
#Composable
fun CustomerListView(list: List<Customer>) {
AdapterList(data = list) { customer->
Text("name:${customer.name}")
}
}
You could use LazyColumnFor like explained here:
#Composable
fun LazyColumnForDemo() {
LazyColumnFor(items = listOf(
"A", "B", "C", "D"
) + ((0..100).map { it.toString() }),
modifier = Modifier,
itemContent = { item ->
Log.d("COMPOSE", "This get rendered $item")
when (item) {
"A" -> {
Text(text = item, style = TextStyle(fontSize = 80.sp))
}
"B" -> {
Button(onClick = {}) {
Text(text = item, style = TextStyle(fontSize = 80.sp))
}
}
"C" -> {
//Do Nothing
}
"D" -> {
Text(text = item)
}
else -> {
Text(text = item, style = TextStyle(fontSize = 80.sp))
}
}
})
}
Explained by google here: https://youtu.be/SMOhl9RK0BA 23:18
Similar to endless list in RecyclerView with some tunes:
#Parcelize
data class PagingController(
var loading: Boolean = false,
var itemsFromEndToLoad: Int = 5,
var lastLoadedItemsCount: Int = 0,
) : Parcelable {
fun reset() {
loading = false
itemsFromEndToLoad = 5
lastLoadedItemsCount = 0
}
}
#Composable
fun LazyGridState.endlessOnScrollListener(
pagingController: PagingController,
itemsCount: Int, // provide real collection size to not have collisions if list contains another view types
loadMore: () -> Unit
) {
if (!isScrollInProgress) return
val lastVisiblePosition = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
pagingController.run {
if (loading) {
if (itemsCount > lastLoadedItemsCount) {
loading = false
Timber.v("loaded, lastVisiblePosition: $lastVisiblePosition, itemsCount: $itemsCount")
lastLoadedItemsCount = itemsCount
}
} else {
if (itemsCount < lastLoadedItemsCount) {
Timber.v("list reset")
reset()
}
val needToLoad = lastVisiblePosition + itemsFromEndToLoad >= itemsCount
if (needToLoad) {
Timber.v("loading, lastVisiblePosition: $lastVisiblePosition, itemsCount: $itemsCount")
loading = true
loadMore()
}
}
}
}
val gridState = rememberLazyGridState()
LazyVerticalGrid(
...
state = gridState,
) { ...
}
val pagingController by rememberSaveable { mutableStateOf(PagingController()) }
gridState.endlessOnScrollListener(pagingController, dataList.size) {
... // loadNextPage
}

Jetpack Compose model list becomes jumbled when adding new items

I have an issue with Jetpack compose displaying a model containing a ModelList of items. When new items are added, the order of the UI elements becomes incorrect.
Here's a very simple CounterModel containing a ModelList of ItemModels:
#Model
data class CounterModel(
var counter: Int = 0,
var name: String = "",
val items: ModelList<ItemModel> = ModelList()
)
#Model
data class ItemModel(
var name: String
)
The screen shows two card rows for each ItemModel: RowA and RowB.
When I create this screen initialised with the following CounterModel:
val model = CounterModel()
model.name="hi"
model.items.add(ItemModel("Item 1"))
model.items.add(ItemModel("Item 2"))
CounterModelScreen(model)
...it displays as expected like this:
Item 1
Row A
Item 1
Row B
Item 2
Row A
Item 2
Row B
When I click my 'add' button, to insert a new ItemModel, I simply expect to see
Item 3
Row A
Item 3
Row B
At the bottom. But instead, the order is jumbled, and I see two rowAs then two rowBs:
Item 1
Row A
Item 1
Row B
Item 2
Row A
Item 3
Row A
Item 3
Row B
Item 2
Row B
I don't really understand how this is possible. The UI code is extremely simple: loop through the items and emit RowA and RowB for each one:
for (i in counterModel.items.indices) {
RowA(counterModel, i)
RowB(counterModel, i)
}
Using Android Studio 4.0C6
Here's the complete code:
#Composable
fun CounterModelScreen(counterModel: CounterModel) {
Column {
TopAppBar(title = {
Text(
text = "Counter Model"
)
})
CounterHeader(counterModel)
for (i in counterModel.items.indices) {
RowA(counterModel, i)
RowB(counterModel, i)
}
Button(
text = "Add",
onClick = {
counterModel.items.add(ItemModel("Item " + (counterModel.items.size + 1)))
})
}
}
#Composable
fun CounterHeader(counterModel: CounterModel) {
Text(text = counterModel.name)
}
#Composable
fun RowA(counterModel: CounterModel, index: Int) {
Padding(padding = 8.dp) {
Card(color = Color.White, shape = RoundedCornerShape(4.dp)) {
Column(crossAxisSize = LayoutSize.Expand) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row A")
}
}
}
}
#Composable
fun RowB(counterModel: CounterModel, index: Int) {
Padding(padding = 8.dp) {
Card(color = Color.Gray, shape = RoundedCornerShape(4.dp)) {
Column(crossAxisSize = LayoutSize.Expand) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row B")
}
}
}
}
I have tested it using compose-1.0.0-alpha07 and making some changes to adapt the code to the changed APIs. Everything works flawlessly so my guess is that something was broken in an older version of compose as the code looks correct and works in more recent versions with the mentioned changes.
I have also modified your code to use states as recommended in the docs and added a ViewModel that will help you decouple the Views from the data management:
ViewModel
class CounterModelViewModel : ViewModel() {
private val myBaseModel = CounterModel().apply {
name = "hi"
items.add(ItemModel("Item 1"))
items.add(ItemModel("Item 2"))
}
private val _modelLiveData = MutableLiveData(myBaseModel)
val modelLiveData: LiveData<CounterModel> = _modelLiveData
fun addNewItem() {
val oldCounterModel = modelLiveData.value ?: CounterModel()
// Items is casted to a new MutableList because the new state won't be notified if the new
// counter model content is the same one as the old one. You can also change any other
// properties instead like the name or the counter
val newItemsList = oldCounterModel.items.toMutableList()
newItemsList.add(ItemModel("Item " + (newItemsList.size + 1)))
// Pass a new instance of CounterModel to the LiveData
val newCounterModel = oldCounterModel.copy(items = newItemsList)
_modelLiveData.value = newCounterModel
}
}
Composable Views updated:
#Composable
fun CounterModelScreen(counterModel: CounterModel, onAddNewItem: () -> Unit) {
ScrollableColumn {
TopAppBar(title = {
Text(
text = "Counter Model"
)
})
CounterHeader(counterModel)
counterModel.items.forEachIndexed { index, item ->
RowA(counterModel, index)
RowB(counterModel, index)
}
Button(
onClick = onAddNewItem
) {
Text(text = "Add")
}
}
}
#Composable
fun CounterHeader(counterModel: CounterModel) {
Text(text = counterModel.name)
}
#Composable
fun RowA(counterModel: CounterModel, index: Int) {
Card(
backgroundColor = Color.White,
shape = RoundedCornerShape(4.dp),
modifier = Modifier.padding(8.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row A")
}
}
}
#Composable
fun RowB(counterModel: CounterModel, index: Int) {
Card(
backgroundColor = Color.Gray,
shape = RoundedCornerShape(4.dp),
modifier = Modifier.padding(8.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row B")
}
}
}
This previous code is called from another composable function that contains the instance of the ViewModel, however you can change this to an activity or a fragment with an instance of the mentioned ViewModel, it's up to your preference.
#Composable
fun MyCustomScreen(viewModel: CounterModelViewModel = viewModel()) {
val modelState: CounterModel by viewModel.modelLiveData.observeAsState(CounterModel())
CounterModelScreen(
counterModel = modelState,
onAddNewItem = {
viewModel.addNewItem()
}
)
}

Categories

Resources