Search code examples
androidandroid-jetpack-composejetpack-compose-accompanist

Android app crash due to NullPointerException in Accompanist HorizontalPager for Jetpack Compose


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)

Solution

  • 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)
                )
            }
            ...
    )