Android compose how to paging with Columns inside HorizontalPager? - android

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?

Related

Hide LaodingView when search have no result

I have implemented search functionality in my app which display result as a verticalGridView with pagination : https://github.com/alirezaeiii/TMDb-Compose
I have following logic for refresh load state that works as I wish :
#Composable
fun <T : TMDbItem> PagingScreen(
viewModel: BasePagingViewModel<T>,
onClick: (TMDbItem) -> Unit,
) {
val lazyTMDbItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyTMDbItems.loadState.refresh) {
is LoadState.Loading -> {
TMDbProgressBar()
}
is LoadState.Error -> {
val message =
(lazyTMDbItems.loadState.refresh as? LoadState.Error)?.error?.message ?: return
lazyTMDbItems.apply {
ErrorScreen(
message = message,
modifier = Modifier.fillMaxSize(),
refresh = { retry() }
)
}
}
else -> {
LazyTMDbItemGrid(lazyTMDbItems, onClick)
}
}
}
In LazyTMDbItemGrid, I try to manage append load state as follow :
#Composable
private fun <T : TMDbItem> LazyTMDbItemGrid(
lazyTMDbItems: LazyPagingItems<T>,
onClick: (TMDbItem) -> Unit,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
repeat(COLUMN_COUNT) {
item {
Spacer(
Modifier.windowInsetsTopHeight(
WindowInsets.statusBars.add(WindowInsets(top = 56.dp))
)
)
}
}
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
}
The problem is when there is no result for search, or when result items is shorter than screen size, it displays LoadingRow. My expectation is when we are in this state, LoadingRow does not display, but how can I detect this state?
Correct me if I'm wrong but these should be dictated by the PagingSource.LoadResult.Page
Documentation :
Success result object for PagingSource.load. Params: data - Loaded
data prevKey - Key for previous page if more data can be loaded in
that direction, null otherwise. nextKey - Key for next page if more
data can be loaded in that direction, null otherwise.
So if you reached the pagination end (in either direction) :
PagingSource.LoadResult.Page(
data = loadedData,
prevKey = null,
nextKey = null)

JetpackCompose LazyList extension function stale data

I want to make without paging lib load more functionality with JetpackCompose LazyColumn. All works fine except I am sending from parent isLoading value that is coming from ViewModel. But in my extension function this value never change. Any advice? Here is my code:
TileListView
#Composable
fun LazyListState.OnBottomReached(
onEndReachedThreshold : Int = 0,
onLoadMore : suspend () -> Unit,
isLoading: Boolean = false
) {
require(onEndReachedThreshold >= 0) { "buffer cannot be negative, but was $onEndReachedThreshold" }
// Is Loading does not update from parent
val shouldLoadMore = remember {
derivedStateOf {
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
?:
return#derivedStateOf true
lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - onEndReachedThreshold
}
}
LaunchedEffect(shouldLoadMore){
snapshotFlow { shouldLoadMore.value}
.collect { if (it) {
onLoadMore()
} }
}
}
#Composable
fun TileListView(tiles: List<Tile>,
loadMore: suspend () -> Unit,
isLoading: Boolean,
onEndReachedThreshold: Int = 0,) {
val listState = rememberLazyListState()
val tileViewModel: TileViewModel = hiltViewModel()
LazyColumn(state = listState) {
items(tiles) { tile ->
tileViewModel.tileViewFactory.constructTileView(tile)
}
}
listState.OnBottomReached(onEndReachedThreshold, {
loadMore()
}, isLoading )
}
And here is where I render it
#Composable
fun Feed() {
val feedViewModel: FeedViewModel = hiltViewModel()
val tiles = feedViewModel.state.collectAsState()
val isLoading = feedViewModel.isLoading.collectAsState()
val coroutineScope = rememberCoroutineScope()
TileListView(
tiles = tiles.value,
{
coroutineScope.launch {
feedViewModel.loadMore()
}
}, isLoading = isLoading.value, onEndReachedThreshold = 0
)
}

Screen scrolls to the top (Jetpack Compose Pagination)

I am trying to do pagination in my application. First, I'm fetching 20 item from Api (limit) and every time i scroll down to the bottom of the screen, it increase this number by 20 (nextPage()). However, when this function is called, the screen goes to the top, but I want it to continue where it left off. How can I do that?
Here is my code:
CharacterListScreen:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val state = characterListViewModel.state.value
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
val listState = rememberLazyListState()
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
itemsIndexed(state.characters) { index, character ->
characterListViewModel.onChangeRecipeScrollPosition(index)
if ((index + 1) >= limit) {
characterListViewModel.nextPage()
}
CharacterListItem(character = character)
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
CharacterListViewModel:
#HiltViewModel
class CharacterListViewModel #Inject constructor(
private val characterRepository: CharacterRepository
) : ViewModel() {
val state = mutableStateOf(CharacterListState())
val limit = mutableStateOf(20)
var recipeListScrollPosition = 0
init {
getCharacters(limit.value, Constants.HEADER)
}
private fun getCharacters(limit : Int, header : String) {
characterRepository.getCharacters(limit, header).onEach { result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
private fun incrementLimit() {
limit.value = limit.value + 20
}
fun onChangeRecipeScrollPosition(position: Int){
recipeListScrollPosition = position
}
fun nextPage() {
if((recipeListScrollPosition + 1) >= limit.value) {
incrementLimit()
characterRepository.getCharacters(limit.value, Constants.HEADER).onEach {result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
}
CharacterListState:
data class CharacterListState(
val isLoading : Boolean = false,
var characters : List<Character> = emptyList(),
val error : String = ""
)
I think the issue here is that you are creating CharacterListState(isLoading = true) while loading. This creates an object with empty list of elements. So compose renders an empty LazyColumn here which resets the scroll state. The easy solution for that could be state.value = state.value.copy(isLoading = true). Then, while loading, the item list can be preserved (and so is the scroll state)
Not sure if you are using the LazyListState correctly. In your viewmodel, create an instance of LazyListState:
val lazyListState: LazyListState = LazyListState()
Pass that into your composable and use it as follows:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), state = characterListViewModel.lazyListState) {
itemsIndexed(state.characters) { index, character ->
}
}
}
}

Update Jetpack Compose list with scroll position reset

In my app I have list of items shown with LazyColumn. My list can update fields and sometime reorder items. Whenever I have reorder of items I need to show list with first item at the top. I came up with setup like the one below. List scrolls to the top only on the first reorder event. On consecutive list update with reorder = true, my list not scrolling to the top. What is the better approach in Compose to reset list scroll position (or even force rebuilding compose list) by sending state from ViewModel?
class ViewItemsState(val items: List<ViewItem>, val scrollToTop: Boolean)
class ExampleViewModel() : ViewModel() {
var itemsLiveData = MutableLiveData<ViewItemsState>()
}
#Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val itemState by viewModel.itemsLiveData.observeAsState()
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
if (itemState.scrollToTop) {
LaunchedEffect(coroutineScope) {
Log.e("TAG", "TopCoinsScreen: scrollToTop" )
listState.scrollToItem(0)
}
}
LazyColumn(state = listState) {
items(itemState.items) { item ->
ItemCompose(
item.name,
item.value
)
}
}
}
Recomposition is triggered only when a state changes.
Two things to fix in this,
Reset scrollToTop to false once scrolling completes.
Store scrollToTop in the view model as a MutableState, LiveData, Flow, or any other reactive element.
Currently scrollToTop is stored as a boolean in a data class object. Resetting the value will not trigger a Composable recomposition.
Example code with sample data,
class ViewItemsState(
val items: List<String>,
)
class ExampleViewModel : ViewModel() {
private var _itemsLiveData =
MutableLiveData(
ViewItemsState(
items = Array(20) {
it.toString()
}.toList(),
)
)
val itemsLiveData: LiveData<ViewItemsState>
get() = _itemsLiveData
private var _scrollToTop = MutableLiveData(false)
val scrollToTop: LiveData<Boolean>
get() = _scrollToTop
fun updateScrollToTop(scroll: Boolean) {
_scrollToTop.postValue(scroll)
}
}
#Composable
fun ExampleScreen(
viewModel: ExampleViewModel = ExampleViewModel(),
) {
val itemState by viewModel.itemsLiveData.observeAsState()
val scrollToTop by viewModel.scrollToTop.observeAsState()
val listState = rememberLazyListState()
LaunchedEffect(
key1 = scrollToTop,
) {
if (scrollToTop == true) {
listState.scrollToItem(0)
viewModel.updateScrollToTop(false)
}
}
Column {
LazyColumn(
state = listState,
modifier = Modifier.weight(1f),
) {
items(itemState?.items.orEmpty()) { item ->
Text(
text = item,
modifier = Modifier.padding(16.dp),
)
}
}
Button(
onClick = {
viewModel.updateScrollToTop(true)
},
) {
Text(text = "Scroll")
}
}
}

Jetpack Compose not recomposing composable on setValue

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
}

Categories

Resources