I'm using Accompanist HorizontalPager in an Android Jetpack Compose project to show a dynamically changing list from Firebase Firestore. It works well if the list is initially empty or has items, but once it has some items and then becomes empty, the app crashes with NullPointerException from HorizontalPager.
Below are the relevant dependencies used.
compose_version = '1.0.1'
hilt_version = '2.38.1'
kotlin_version = '1.5.21'
// Accompanist
def accompanistVersion = "0.16.1"
implementation("com.google.accompanist:accompanist-pager:$accompanistVersion")
implementation("com.google.accompanist:accompanist-pager-indicators:$accompanistVersion")
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
implementation("com.google.accompanist:accompanist-insets:$accompanistVersion")
implementation("com.google.accompanist:accompanist-flowlayout:$accompanistVersion")
implementation("com.google.accompanist:accompanist-swiperefresh:$accompanistVersion")
def coil = "1.3.2"
implementation("io.coil-kt:coil:$coil")
implementation("io.coil-kt:coil-compose:$coil")
Below is the code snippet.
val pagerList: List<PagerDomain> by viewModel.pagerList.collectAsState()
val pagerState: PagerState = rememberPagerState(pageCount = pagerList.size)
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
HorizontalPager(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
state = pagerState,
) { page: Int ->
ProfileCarouselItem(
modifier = Modifier
.graphicsLayer {
// Calculate the absolute offset for the current page from the
// scroll position. We use the absolute value which allows us to mirror
// any effects for both directions
val pageOffset =
calculateCurrentOffsetForPage(page).absoluteValue
// We animate the scaleX + scaleY, between 85% and 100%
lerp(
start = 0.85f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
).also { scale ->
scaleX = scale
scaleY = scale
}
// We animate the alpha, between 50% and 100%
alpha = lerp(
start = 0.5f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
}
.fillMaxWidth(0.8f)
.aspectRatio(0.5f),
pagerDomain = pagerList[page],
onConnectClick = onConnectClick,
showConnectLoading = showConnectLoading
)
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.padding(16.dp),
)
}
Below is the stack trace.
Fatal Exception: java.lang.NullPointerException
com.google.accompanist.pager.PagerState.getCurrentPageOffset (PagerState.kt:745)
com.google.accompanist.pager.PagerIndicatorKt$HorizontalPagerIndicator$1$2$1.invoke-Bjo55l4 (PagerIndicator.kt:95)
com.google.accompanist.pager.PagerIndicatorKt$HorizontalPagerIndicator$1$2$1.invoke (PagerIndicator.kt:94)
androidx.compose.foundation.layout.OffsetPxModifier$measure$1.invoke (Offset.kt:202)
androidx.compose.foundation.layout.OffsetPxModifier$measure$1.invoke (Offset.kt:201)
androidx.compose.ui.layout.MeasureScope$layout$1.placeChildren (MeasureScope.kt:68)
androidx.compose.ui.node.DelegatingLayoutNodeWrapper.placeAt-f8xVGno (DelegatingLayoutNodeWrapper.kt:111)
androidx.compose.ui.layout.Placeable.access$placeAt-f8xVGno (Placeable.kt:31)
androidx.compose.ui.layout.Placeable$PlacementScope.place-70tqf50 (Placeable.kt:370)
androidx.compose.ui.node.OuterMeasurablePlaceable.placeAt-f8xVGno (OuterMeasurablePlaceable.kt:149)
androidx.compose.ui.layout.Placeable.access$placeAt-f8xVGno (Placeable.kt:31)
androidx.compose.ui.layout.Placeable$PlacementScope.place-70tqf50 (Placeable.kt:370)
androidx.compose.ui.layout.Placeable$PlacementScope.place-70tqf50$default (Placeable.kt:203)
androidx.compose.foundation.layout.BoxKt.placeInBox (Box.kt:186)
androidx.compose.foundation.layout.BoxKt.access$placeInBox (Box.kt:1)
androidx.compose.foundation.layout.BoxKt$boxMeasurePolicy$1$measure$5.invoke (Box.kt:167)
androidx.compose.foundation.layout.BoxKt$boxMeasurePolicy$1$measure$5.invoke (Box.kt:163)
androidx.compose.ui.layout.MeasureScope$layout$1.placeChildren (MeasureScope.kt:68)
androidx.compose.ui.node.LayoutNode$layoutChildren$1.invoke (LayoutNode.kt:925)
androidx.compose.ui.node.LayoutNode$layoutChildren$1.invoke (LayoutNode.kt:915)
androidx.compose.runtime.snapshots.Snapshot$Companion.observe (Snapshot.kt:1776)
androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads (SnapshotStateObserver.kt:123)
androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release (OwnerSnapshotObserver.kt:75)
androidx.compose.ui.node.OwnerSnapshotObserver.observeLayoutSnapshotReads$ui_release (OwnerSnapshotObserver.kt:56)
androidx.compose.ui.node.LayoutNode.layoutChildren$ui_release (LayoutNode.kt:915)
androidx.compose.ui.node.LayoutNode.onNodePlaced$ui_release (LayoutNode.kt:901)
androidx.compose.ui.node.InnerPlaceable.placeAt-f8xVGno (InnerPlaceable.kt:94)
androidx.compose.ui.layout.Placeable.access$placeAt-f8xVGno (Placeable.kt:31)
androidx.compose.ui.layout.Placeable$PlacementScope.placeRelative (Placeable.kt:359)
androidx.compose.ui.layout.Placeable$PlacementScope.placeRelative$default (Placeable.kt:179)
androidx.compose.foundation.layout.PaddingModifier$measure$1.invoke (Padding.kt:370)
androidx.compose.foundation.layout.PaddingModifier$measure$1.invoke (Padding.kt:368)
androidx.compose.ui.layout.MeasureScope$layout$1.placeChildren (MeasureScope.kt:68)
androidx.compose.ui.node.DelegatingLayoutNodeWrapper.placeAt-f8xVGno (DelegatingLayoutNodeWrapper.kt:111)
androidx.compose.ui.layout.Placeable.access$placeAt-f8xVGno (Placeable.kt:31)
androidx.compose.ui.layout.Placeable$PlacementScope.place-70tqf50 (Placeable.kt:370)
androidx.compose.ui.node.OuterMeasurablePlaceable.placeAt-f8xVGno (OuterMeasurablePlaceable.kt:149)
androidx.compose.ui.node.OuterMeasurablePlaceable.replace (OuterMeasurablePlaceable.kt:161)
androidx.compose.ui.node.LayoutNode.replace$ui_release (LayoutNode.kt:811)
androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout (MeasureAndLayoutDelegate.kt:215)
androidx.compose.ui.platform.AndroidComposeView.measureAndLayout (AndroidComposeView.android.kt:510)
androidx.compose.ui.platform.AndroidComposeView.dispatchDraw (AndroidComposeView.android.kt:666)
android.view.View.draw (View.java:23904)
android.view.View.updateDisplayListIfDirty (View.java:22776)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:5320)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:5292)
android.view.View.updateDisplayListIfDirty (View.java:22731)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:5320)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:5292)
android.view.View.updateDisplayListIfDirty (View.java:22731)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:5320)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:5292)
android.view.View.updateDisplayListIfDirty (View.java:22731)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:5320)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:5292)
android.view.View.updateDisplayListIfDirty (View.java:22731)
android.view.ThreadedRenderer.updateViewTreeDisplayList (ThreadedRenderer.java:579)
android.view.ThreadedRenderer.updateRootDisplayList (ThreadedRenderer.java:585)
android.view.ThreadedRenderer.draw (ThreadedRenderer.java:662)
android.view.ViewRootImpl.draw (ViewRootImpl.java:5042)
android.view.ViewRootImpl.performDraw (ViewRootImpl.java:4749)
android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:3866)
android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:2618)
android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:9965)
android.view.Choreographer$CallbackRecord.run (Choreographer.java:1010)
android.view.Choreographer.doCallbacks (Choreographer.java:809)
android.view.Choreographer.doFrame (Choreographer.java:744)
android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:995)
android.os.Handler.handleCallback (Handler.java:938)
android.os.Handler.dispatchMessage (Handler.java:99)
android.os.Looper.loop (Looper.java:246)
android.app.ActivityThread.main (ActivityThread.java:8506)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:602)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1130)
First crash in your code is the result of the accompanist bug. While it's not fixed you just need to wrap HorizontalPagerIndicator
with if
:
if (pagerState.pageCount != 0) {
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.padding(16.dp),
)
}
After you fix this crash you will face an other one. It happens because you're calling calculateCurrentOffsetForPage
inside graphicsLayer
, which gets called after pagerList
change
You can easily solve this by moving this calculation out of the modifier:
// Calculate the absolute offset for the current page from the
// scroll position. We use the absolute value which allows us to mirror
// any effects for both directions
val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue
ProfileCarouselItem(
modifier = Modifier
.graphicsLayer {
// We animate the scaleX + scaleY, between 85% and 100%
lerp(
start = 0.85f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
).also { scale ->
scaleX = scale
scaleY = scale
}
// We animate the alpha, between 50% and 100%
alpha = lerp(
start = 0.5f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
}
...
)