I am trying to create a infinite scrollable carousel of cards using modified version of this solution
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(Int.MAX_VALUE / 2)
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = Int.MAX_VALUE,
itemContent = {
val index = it % items.size // line X
Text(text = items[index])
}
)
}
}
However, this crashes with NPE:
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter it
at androidx.compose.foundation.lazy.LazyListItemProviderImplKt$generateKeyToIndexMap$1$1.invoke(Unknown Source:2)
This will also crash, if i use a smaller index of 500 for example, but it doesn't crash, if i use 50
val listState = rememberLazyListState(500) // Crashes
val listState = rememberLazyListState(50) // Doesn't crash
What could possible cause this?
EDIT1: I have tried debugging it with breakpoints and it crashes before it stops on line X. Items list is not empty, usual test size is 2-6 objects.
EDIT2: Also tried using scrollToItem() instead to set the initial index, but that returned the same exception
I never did understand why this problem occurred, but here's a solution (or workaround/hack) how to solve it:
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(Int.MAX_VALUE / 4) // Use smaller index for smaller count
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = Int.MAX_VALUE / 2, // Use smaller count, somewhere around 1B
itemContent = {
val index = it % items.size
Text(text = items[index])
}
)
}
}
For some reason, this worked without a problem.
Related
I am using this component and I was having a problem, as I don't get the same result like this:
HorizontalPager(count = tabTitles.size, state = pageState) { page ->
Log.i("Polaris", "Hi, I'm page $page")
Log.i("Polaris", "Hi, I'm currentPage $currentPage")
}
The result I get is:
2022-02-05 17:16:06.461 31070-31070/com.polaris I/Polaris: Hi, I'm page 0
2022-02-05 17:16:06.461 31070-31070/com.polaris I/Polaris: Hi, I'm currentPage 0
2022-02-05 17:16:06.464 31070-31070/com.polaris I/Polaris: Hi, I'm page 1
2022-02-05 17:16:06.464 31070-31070/com.polaris I/Polaris: Hi, I'm currentPage 0
Why does currentPage always return me the current page I'm on and page returns me the current and next page?
I edit the publication to add the rest of the code. The problem is that given the result that the page parameter gives me, the call that I have inside the Prediction component, when going from one page to another, replicates the same information that I get from the service in each one, instead of putting the one that corresponds to each one:
#ExperimentalPagerApi
#Composable
fun Tabs(zodiacName: String?, modifier: Modifier = Modifier) {
val viewModel = getViewModel<DetailViewModel>()
val tabTitles = listOf("Yesterday", "Today", "Tomorrow")
val coroutineScope = rememberCoroutineScope()
val pageState = rememberPagerState()
Column(
modifier
) {
TabRow(
modifier = Modifier.fillMaxWidth(),
selectedTabIndex = pageState.currentPage,
backgroundColor = Color.Transparent,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
color = Color.White,
modifier = Modifier.pagerTabIndicatorOffset(
pageState,
tabPositions
),
height = 2.dp
)
}) {
tabTitles.forEachIndexed { index, title ->
Tab(selected = pageState.currentPage == index,
onClick = {
coroutineScope.launch {
pageState.animateScrollToPage(index)
}
},
text = {
Text(
text = title,
color = Color.White,
fontSize = 14.sp,
fontFamily = Helvetica
)
})
}
}
HorizontalPager(count = tabTitles.size, state = pageState) {
when (currentPage) {
0 -> Prediction(
viewModel = viewModel,
zodiacName = zodiacName,
day = "yesterday"
)
1 -> Prediction(
viewModel = viewModel,
zodiacName = zodiacName,
day = "today"
)
2 -> Prediction(
viewModel = viewModel,
zodiacName = zodiacName,
day = "tomorrow"
)
}
}
}
}
The content of the Prediction component is as follows:
#Composable
fun Prediction(viewModel: DetailViewModel, zodiacName: String?, day: String?) {
val errorMessage = viewModel.errorMessage.value
val horoscope = viewModel.horoscopeResponse.value
if (errorMessage.isEmpty()) {
LazyColumn(
modifier = Modifier
.padding(top = 20.dp, bottom = 10.dp, start = 10.dp, end = 10.dp)
) {
item {
LaunchedEffect(Unit) {
viewModel.getHoroscopeDetail(zodiacName, day)
}
if (horoscope.checkDescriptionContent()) {
PredictionCard(horoscope = horoscope)
} else {
ShimmerCard()
}
}
}
} else {
ErrorComponent(
viewModel = viewModel,
sign = zodiacName,
day = day
)
}
}
Why then if I use currentPage it works fine, it makes the calls and places all the information correctly, but if I use the page parameter contained in the lambda, it duplicates the information?
The HorizontalPager pre-draws a pair of adjacent pages, so that when you start scrolling, you don't have to wait for rendering.
This is exactly what happens in your logs: content is called for the first page and for the second, but currentPage is 0 in both cases.
You must always use the page parameter when creating a view within content, otherwise the view will be built on the wrong value during scrolling. When scrolling is over, the view will be recomposed with the new currentPage value, so it may look fine when scrolling fast, but if you scroll slowly, you will se the problem.
You can easily see how this works in the following example. See how I first scroll through the page without releasing my finger and currentPage shows the "wrong" number, which is updated when I release my finger and the animation ends.
HorizontalPager(
count = 10,
) { page ->
Column {
Text("page $page")
Text("currentPage $currentPage")
}
}
You should review the documentation on how to get your page index current.
enter link description here
val pagerState = rememberPagerState()
LaunchedEffect(pagerState) {
// Collect from the pager state a snapshotFlow reading the currentPage
snapshotFlow { pagerState.currentPage }.collect { page ->
Log.d("abc","currentPage : $page")
}
}
Starting to Learn JetPack Compose. I'm struggling now with State hosting. I have this simple example to press a Button to show in a Text component the content of an array. I'm able to make it work if the variable is inside my #Composable. When applying State Hoisting (taking the variable for my composable) I'm finding some issues.
This is the code that is working fine
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
NextValue(listadoNumeros)
#Composable
fun NextValue(listado: List<Int>) {
var position by rememberSaveable {mutableStateOf (0)}
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
){
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
This is the code that is not working correctly
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by rememberSaveable {mutableStateOf (0)}
NextValue(position = n,listadoNumeros)
#Composable
fun NextValue(position:Int,listado: List<Int>) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
Error message, as you can expect, is "position can't be reassigned". I see why, but don't know how to fix it. I read about onValueChanged in TextField, etc, but don't know if it's applicable here.
position += 1 is equivalent to position = position + 1. In Kotlin, function arguments are val and cannot be reassigned within the scope of the function. That is why the compiler will complain and prevent you from doing that.
What you want to do is to add an extra event callback within the function and perform this addition at the function call site.
#Composable
fun NextValue(position: Int, listado: List<Int>, onPositionChange: (Int) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { onPositionChange(position + 1) }) {
Text(text = "Next")
}
}
}
You can use the above composable as
val listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by remember { mutableStateOf(0) }
NextValue(position = n, listadoNumeros, onPositionChange = { newPosition -> n = newPosition})
Whenever the user clicks on the button, an event is sent back up to the caller and the caller decides what to do with the updated information. In this case, recreate the composable with an updated value.
I'm working on a search page made in Compose with LazyColumn, everything works fine except for the wanted behavior of LazyColumn returing to first item when data changes.
This is my actual implementation of lazy column:
#Composable
fun <DataType : Any> GenericListView(
itemsList: SnapshotStateList<DataType>, // this list comes from the search page viewmodel
modifier: Modifier = Modifier.fillMaxSize(),
spacing: Dp = 24.dp,
padding: PaddingValues = PaddingValues(0.dp),
item: #Composable (DataType) -> Unit
) {
val listState: LazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing),
state = listState,
modifier = modifier.padding(padding)
) {
items(itemsList) {
item(it)
}
}
SideEffect {
Log.i("->->->->->->->", "side effect launched")
coroutineScope.launch {
listState.scrollToItem(0)
}
}
}
As docs says, SideEffect should be called everytime the function is recomposed,
but it appear to be working only in debug mode with breakpoints in SideEffect, otherwise, it works only when the whole page is first created.
I've already tried with LaunchedEffect instead of SideEffect, using itemsList as key, but nothing happened.
Why my code works only in debug mode ?
Or better, an already made working solution to reset position when new data are set ?
SideEffect doesn't work because Compose is not actually recomposing the whole view when the SnapshotStateList is changed: it sees that only LazyColumn is using this state value so only this function needs to be recomposed.
To make it work you can change itemsList to List<DataType> and pass plain list, like itemsList = mutableStateList.toList() - it'll force whole view recomposition.
LaunchedEffect with passed SnapshotStateList doesn't work for kind of the same reason: it compares the address of the state container, which is not changed. To compare the items itself, you again can convert it to a plain list: in this case it'll be compared by items hash.
LaunchedEffect(itemsList.toList()) {
}
You can achieve the mentioned functionality with SideEffect, remember and with some kind of identificator (listId) of the list items. If this identificator changes, the list will scroll to the top, otherwise not.
I have extended your code. (You can choose any type for listId.)
#Composable
fun <DataType : Any> GenericListView(
itemsList: SnapshotStateList<DataType>, // this list comes from the search page viewmodel
modifier: Modifier = Modifier.fillMaxSize(),
spacing: Dp = 24.dp,
padding: PaddingValues = PaddingValues(0.dp),
listId: String? = null,
item: #Composable (DataType) -> Unit
) {
var lastListId: String? by remember {
mutableStateOf(null)
}
val listState: LazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing),
state = listState,
modifier = modifier.padding(padding)
) {
items(itemsList) {
item(it)
}
}
SideEffect {
Log.i("->->->->->->->", "side effect launched")
coroutineScope.launch {
if (lastListId != listId) {
lastListId = listId
listState.scrollToItem(0)
}
}
}
}
I'm trying to build a scrollable column (preferably LazyColumn) that will start re-showing the first items again after I scroll to the end. For example, see this alarm clock that will cycle from 00..59 and then will smoothly keep scrolling from 0 again.
I've tried a normal LazyColumn that will show 58,59,00..59,00,01 and snap to start after I'm done scrolling (reaching 59) but it looks "cheap".
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
isEndless: Boolean = false,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(
if (isEndless) Int.MAX_VALUE / 2 else 0
)
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = if (isEndless) Int.MAX_VALUE else items.size,
itemContent = {
val index = it % items.size
Text(text = items[index]) // item composable
}
)
}
}
How to achieve infinite like list in Lazycolumn/LazyRow.When scrolled to the end, I would like to views to be visible while the displaying data from the top of the list or when scrolled to the top of the list I would display data from the bottom of the list.
I think something like this can work:
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(Int.MAX_VALUE / 2)
LazyColumn(
state = listState,
modifier = modifier
) {
items(Int.MAX_VALUE, itemContent = {
val index = it % items.size
Text(text = items[index]) // item composable
})
}
}
In addition to the previous answer, you can make it customizable to support both variants of the list.
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
isEndless: Boolean = false,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(
if (isEndless) Int.MAX_VALUE / 2 else 0
)
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = if (isEndless) Int.MAX_VALUE else items.size,
itemContent = {
val index = it % items.size
Text(text = items[index]) // item composable
}
)
}
}