Android NumberPicker widget is not working properly in Jetpack Compose HorizontalPager

1.1k Views Asked by At

NumberPicker (android.widget.NumberPicker) is not working properly while placed in Jetpack Compose HorizontalPager

Middle number disappears after scrolling away 3 pages and then return to a picker page:

picker bug

It happens hence pager keeps composed only 3 screens at time (current, previous and next). When we scroll up to 3rd page and then scroll back, the picker page is being recomposed and that's why we can observe such bug.

I have tried invalidate method in update scope of Picker's AndroidView composable, but it doesn't help.

Any suggestions how to resolve the issue? Thanks!

Code to reproduce the issue:

App's buildSrc:

//ACCOMPANIST
api "com.google.accompanist:accompanist-pager:0.27.0"
api "com.google.accompanist:accompanist-pager-indicators:0.27.0"

//COMPOSE
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material:material'
implementation 'androidx.activity:activity-compose:1.6.1'

MainActivity:

import android.os.Bundle
import android.widget.NumberPicker
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.example.composepagerpickerissue.ui.theme.ComposePagerPickerIssueTheme
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalPagerApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposePagerPickerIssueTheme {
                val coroutineScope = rememberCoroutineScope()

                val pages = listOf(
                    Page.PickerPage,
                    Page.EmptyPage,
                    Page.EmptyPage,
                    Page.EmptyPage,
                    Page.PickerPage,
                )

                val pagerState = rememberPagerState(0)

                val onNextPage: () -> Unit = {
                    coroutineScope.launch { pagerState.animateScrollToPage(page = pagerState.currentPage + 1) }
                }
                val onPreviousPage: () -> Unit = {
                    coroutineScope.launch {
                        pagerState.animateScrollToPage(
                            page = (pagerState.currentPage - 1).takeIf { it >= 0 } ?: 0
                        )
                    }
                }

                Column {

                    HorizontalPager(
                        count = pages.size,
                        state = pagerState,
                        modifier = Modifier.weight(1f, true)
                    ) { page ->
                        pages[page].screen()
                    }

                    Row(
                        modifier = Modifier.padding(16.dp),
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        OutlinedButton(
                            modifier = Modifier.weight(1f),
                            onClick = onPreviousPage
                        ) { Text("Back") }
                        Button(
                            modifier = Modifier.weight(1f),
                            onClick = onNextPage
                        ) { Text("Next") }
                    }
                }
            }
        }
    }
}

@Composable
fun SimpleNumberPicker(
    value: Int,
    min: Int = 0,
    max: Int = Int.MAX_VALUE,
    onValueChange: (Int) -> Unit
) {
    AndroidView(
        modifier = Modifier.fillMaxWidth(),
        factory = { context ->
            NumberPicker(context).apply {
                setOnValueChangedListener { numberPicker, i, i2 ->
                    onValueChange(i)
                }
                minValue = min
                maxValue = max
                this.value = value
            }
        },
        update = {}
    )
}

sealed class Page(var screen: @Composable () -> Unit) {
    object EmptyPage : Page(
        screen = { Box(modifier = Modifier.fillMaxSize()) { Text(text = "Empty screen") } })

    object PickerPage : Page(
        screen = { Column(modifier = Modifier.fillMaxSize()) { SimpleNumberPicker(150) {} } })
}

enter image description here

1

There are 1 best solutions below

1
On

I had the exact same problem too. I solved it by updating the Jetpack compose dependencies to version 1.3.0.

You can also just add the current Compose BOM (Bill of Materials) and omit the version numbers for the compose dependencies, like this:

implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.material:material"
implementation ...

You can read more about the Compose BOM here: https://developer.android.com/jetpack/compose/setup#using-the-bom